chrono_parse
Loading...
Searching...
No Matches
parse.hpp
1/*
2 * MIT License
3 *
4 * (c) 2023 Muhammed Galib Uludag
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25#ifndef MGUTILITY_CHRONO_PARSE_HPP
26#define MGUTILITY_CHRONO_PARSE_HPP
27
28// trunk-ignore-all(clang-format)
29
30#include "mgutility/std/charconv.hpp"
31#include "mgutility/std/string_view.hpp"
32
33#include <cctype>
34#include <chrono>
35#include <cstddef>
36#include <cstdint>
37#include <ratio>
38#include <stdexcept>
39#include <type_traits>
40
41// NOLINTBEGIN(modernize-concat-nested-namespaces)
42namespace mgutility {
43namespace chrono {
44namespace detail {
45// NOLINTEND(modernize-concat-nested-namespaces)
46
50struct tm : std::tm {
51 uint32_t tm_ms;
52};
53
64template <typename T>
65MGUTILITY_CNSTXPR auto parse_integer(T &result, mgutility::string_view str,
66 uint32_t len, uint32_t &next,
67 uint32_t begin_offset = 0) -> std::errc {
68 auto error = mgutility::from_chars(str.data() + next + begin_offset,
69 str.data() + len + next, result);
70
71 next += ++len;
72
73 return error.ec;
74}
75
82constexpr int32_t abs(int32_t value) noexcept {
83 return value >= 0 ? value : -value;
84}
85
86// NOLINTNEXTLINE
87template <typename T> constexpr auto pow(T base, T exp) noexcept -> T {
88 // NOLINTNEXTLINE
89 return exp < 0 ? 0 : exp == 0 ? 1 : base * pow(base, exp - 1);
90}
91
100template <typename T>
101MGUTILITY_CNSTXPR auto check_range(const T &value, const T &min, const T &max)
102 -> std::errc {
103 if (value < min || value > max) {
104 return std::errc::result_out_of_range;
105 }
106 return std::errc{};
107}
108
115auto MGUTILITY_CNSTXPR is_leap_year(int32_t year) -> bool {
116 return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0);
117}
118
126// NOLINTNEXTLINE
127auto MGUTILITY_CNSTXPR days_in_month(int32_t year, int32_t month) -> int32_t {
128 // NOLINTNEXTLINE
129 constexpr int days_per_month[] = {31, 28, 31, 30, 31, 30,
130 31, 31, 30, 31, 30, 31};
131 if (month < 0 || month > 11) {
132 return 0; // Invalid month
133 }
134 if (month == 1) { // February
135 return is_leap_year(year) ? 29 : 28;
136 }
137 // NOLINTNEXTLINE
138 return days_per_month[month];
139}
140
148MGUTILITY_CNSTXPR auto mktime(std::time_t &result, std::tm &time_struct)
149 -> std::errc {
150 result = 0;
151
152 // Check for out of range values in tm structure
153 if (time_struct.tm_mon > 12 || time_struct.tm_mon < 0 ||
154 time_struct.tm_mday > 31 || time_struct.tm_min > 60 ||
155 time_struct.tm_sec > 60 || time_struct.tm_hour > 24) {
156 return std::errc::result_out_of_range;
157 }
158
159 time_struct.tm_year += 1900;
160
161 if (days_in_month(time_struct.tm_year, time_struct.tm_mon) <
162 time_struct.tm_mday) {
163 return std::errc::result_out_of_range;
164 }
165
166 // Calculate the number of days since 1970
167 for (auto i{1970}; i < time_struct.tm_year; ++i) {
168 result += is_leap_year(i) ? 366 : 365;
169 }
170
171 // Add the days for the current year
172 for (auto i{0}; i < time_struct.tm_mon; ++i) {
173 // NOLINTNEXTLINE
174 result += days_in_month(time_struct.tm_year, i);
175 }
176
177 result += time_struct.tm_mday - 1; // nth day since 1970
178 result *= 24;
179 result += time_struct.tm_hour;
180 result *= 60;
181 result += time_struct.tm_min;
182 result *= 60;
183 result += time_struct.tm_sec;
184
185 return std::errc{};
186}
187
194MGUTILITY_CNSTXPR auto handle_timezone(tm &time_struct, int32_t offset)
195 -> std::errc {
196 // Validate offset: HHMM format, minutes 0-59, hours 0-23
197 const int32_t abs_offset = abs(offset);
198 const int32_t minutes = abs_offset % 100;
199 const int32_t hours = abs_offset / 100;
200 if (minutes > 59 || hours > 23) {
201 return std::errc::invalid_argument;
202 }
203
204 // Validate input tm structure (basic checks)
205 if (time_struct.tm_mon < 0 || time_struct.tm_mon > 11 ||
206 time_struct.tm_mday < 1 || time_struct.tm_year < 0) {
207 return std::errc::invalid_argument;
208 }
209
210 // Apply offset (positive or negative)
211 const int32_t total_minutes =
212 time_struct.tm_min + (offset >= 0 ? minutes : -minutes);
213 const int32_t total_hours =
214 time_struct.tm_hour + (offset >= 0 ? hours : -hours);
215
216 // Normalize minutes (-59 to 119 -> 0-59 with hour carry)
217 time_struct.tm_min = total_minutes % 60;
218 int minute_carry = total_minutes / 60;
219 if (total_minutes < 0 && total_minutes % 60 != 0) {
220 minute_carry -= 1;
221 time_struct.tm_min += 60;
222 }
223
224 // Normalize hours (-23 to 47 -> 0-23 with day carry)
225 time_struct.tm_hour = (total_hours + minute_carry) % 24;
226 int day_carry = (total_hours + minute_carry) / 24;
227 if (total_hours + minute_carry < 0 &&
228 (total_hours + minute_carry) % 24 != 0) {
229 day_carry -= 1;
230 time_struct.tm_hour += 24;
231 }
232
233 // Normalize days, months, and years
234 int days = time_struct.tm_mday + day_carry;
235 int months = time_struct.tm_mon;
236 int years = time_struct.tm_year;
237
238 // Handle negative days
239 while (days <= 0) {
240 months -= 1;
241 if (months < 0) {
242 months += 12;
243 years -= 1;
244 if (years < 0) {
245 return std::errc::result_out_of_range; // Year underflow
246 }
247 }
248 days += days_in_month(years, months);
249 }
250
251 // Handle day overflow
252 while (days > days_in_month(years, months)) {
253 days -= days_in_month(years, months);
254 months += 1;
255 if (months > 11) {
256 months -= 12;
257 years += 1;
258 if (years > 9999 - 1900) { // Avoid overflow (arbitrary limit)
259 return std::errc::result_out_of_range;
260 }
261 }
262 }
263
264 // Update tm structure
265 time_struct.tm_mday = days;
266 time_struct.tm_mon = months;
267 time_struct.tm_year = years;
268
269 return std::errc{};
270}
271
272// Free parsing functions
273MGUTILITY_CNSTXPR auto parse_year(detail::tm &result, string_view date_str,
274 uint32_t &next) -> std::errc {
275 auto error = parse_integer(result.tm_year, date_str, 4, next);
276 result.tm_year %= 1900;
277 return error;
278}
279
280MGUTILITY_CNSTXPR auto parse_month(detail::tm &result, string_view date_str,
281 uint32_t &next) -> std::errc {
282 auto error = parse_integer(result.tm_mon, date_str, 2, next);
283 result.tm_mon -= 1;
284 return error;
285}
286
287MGUTILITY_CNSTXPR auto parse_day(detail::tm &result, string_view date_str,
288 uint32_t &next) -> std::errc {
289 auto error = parse_integer(result.tm_mday, date_str, 2, next);
290 if (error != std::errc{}) {
291 return error;
292 }
293 error = check_range(result.tm_mday, 1,
294 days_in_month(result.tm_year, result.tm_mon));
295 return error;
296}
297
298MGUTILITY_CNSTXPR auto parse_hour(detail::tm &result, string_view date_str,
299 uint32_t &next) -> std::errc {
300 auto error = parse_integer(result.tm_hour, date_str, 2, next);
301 if (error != std::errc{}) {
302 return error;
303 }
304 error = check_range(result.tm_hour, 0, 23);
305 return error;
306}
307
308MGUTILITY_CNSTXPR auto parse_minute(detail::tm &result, string_view date_str,
309 uint32_t &next) -> std::errc {
310 auto error = parse_integer(result.tm_min, date_str, 2, next);
311 if (error != std::errc{}) {
312 return error;
313 }
314 error = check_range(result.tm_min, 0, 59);
315 return error;
316}
317
318MGUTILITY_CNSTXPR auto parse_second(detail::tm &result, string_view date_str,
319 uint32_t &next) -> std::errc {
320 auto error = parse_integer(result.tm_sec, date_str, 2, next);
321 if (error != std::errc{}) {
322 return error;
323 }
324 error = check_range(result.tm_sec, 0, 59);
325 return error;
326}
327
328MGUTILITY_CNSTXPR auto parse_fraction(detail::tm &result, string_view date_str,
329 uint32_t &next) -> std::errc {
330 uint32_t digits = 0;
331 while (next + digits < date_str.size() &&
332 mgutility::detail::is_digit(date_str[next + digits]) && digits < 9) {
333 ++digits;
334 }
335 auto error = parse_integer(result.tm_ms, date_str, digits, next);
336 if (error != std::errc{}) {
337 return error;
338 }
339 result.tm_ms *= pow<uint32_t>(10, 9 - digits);
340 error = check_range(result.tm_ms, 0U, 999999999U);
341 return error;
342}
343
344MGUTILITY_CNSTXPR auto parse_timezone_offset(detail::tm &result,
345 string_view date_str,
346 uint32_t &next) -> std::errc {
347 std::errc error{};
348 // NOLINTNEXTLINE [bugprone-inc-dec-in-conditions]
349 if (next < date_str.size() && date_str[next] == 'Z') {
350 error = handle_timezone(result, 0);
351 return error;
352 }
353
354 if (next >= date_str.size() ||
355 (date_str[next] != '+' && date_str[next] != '-')) {
356 return std::errc::invalid_argument;
357 }
358
359 const char sign = date_str[next++];
360 int32_t hour = 0;
361 int32_t minute = 0;
362
363 error = parse_integer(hour, date_str, 2, next);
364 if (error != std::errc{}) {
365 return error;
366 }
367
368 --next;
369
370 if (next < date_str.size() && date_str[next] == ':') {
371 ++next;
372 }
373
374 error = parse_integer(minute, date_str, 2, next);
375 if (error != std::errc{}) {
376 return error;
377 }
378
379 const int32_t offset = (hour * 100) + minute;
380 error = check_range(offset, 0, 1200);
381 if (error != std::errc{}) {
382 return error;
383 }
384
385 error = handle_timezone(result, sign == '+' ? -offset : offset);
386 return error;
387}
388
397MGUTILITY_CNSTXPR auto parse_am_pm(detail::tm &result, string_view date_str,
398 uint32_t &next) -> std::errc {
399 if (next + 2 > date_str.size() || result.tm_hour < 1 || result.tm_hour > 12) {
400 return std::errc::invalid_argument;
401 }
402 if (date_str.substr(next, 2) == "AM") {
403 if (result.tm_hour == 12) {
404 result.tm_hour = 0; // 12 PM = 12, 12 AM = 0
405 }
406 next += 2;
407 } else if (date_str.substr(next, 2) == "PM") {
408 if (result.tm_hour != 12) {
409 result.tm_hour += 12; // 1-11 PM = 13-23
410 }
411 next += 2;
412 } else {
413 return std::errc::invalid_argument;
414 }
415 ++next;
416 return std::errc{};
417}
418
428MGUTILITY_CNSTXPR auto get_time(detail::tm &result, string_view format,
429 string_view date_str) -> std::errc {
430 const std::size_t begin = format.find('{');
431 const std::size_t end = format.find('}');
432 if (begin == string_view::npos || end == string_view::npos || begin >= end) {
433 return std::errc::invalid_argument;
434 }
435
436 if (format[begin + 1] != ':' || (end - begin < 3)) {
437 return std::errc::invalid_argument;
438 }
439
440 uint32_t next = 0;
441 bool is_specifier = false;
442 std::errc error{};
443
444 for (std::size_t i = begin; i < end; ++i) {
445 switch (format[i]) {
446 case '%': {
447 if (i + 1 >= format.size()) {
448 return std::errc::invalid_argument;
449 }
450
451 if (is_specifier) {
452 --next;
453 }
454
455 switch (format[i + 1]) {
456 case 'Y':
457 error = parse_year(result, date_str, next);
458 break;
459 case 'm':
460 error = parse_month(result, date_str, next);
461 break;
462 case 'd':
463 error = parse_day(result, date_str, next);
464 break;
465 case 'F': {
466 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
467 error = parse_year(result, date_str, next);
468 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
469 error = parse_month(result, date_str, next);
470 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
471 error = parse_day(result, date_str, next);
472 } break;
473 case 'H':
474 error = parse_hour(result, date_str, next);
475 break;
476 case 'M':
477 error = parse_minute(result, date_str, next);
478 break;
479 case 'S':
480 error = parse_second(result, date_str, next);
481 break;
482 case 'T': {
483 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
484 error = parse_hour(result, date_str, next);
485 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
486 error = parse_minute(result, date_str, next);
487 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
488 error = parse_second(result, date_str, next);
489 } break;
490 case 'f':
491 error = parse_fraction(result, date_str, next);
492 break;
493 case 'z':
494 error = parse_timezone_offset(result, date_str, next);
495 break;
496 case 'p':
497 error = parse_am_pm(result, date_str, next);
498 break;
499 default:
500 return std::errc::invalid_argument;
501 }
502 if (error != std::errc{}) {
503 return error;
504 }
505 ++i;
506 is_specifier = true;
507 }
508 continue;
509 case ' ': // Space separator
510 case '-': // Dash separator
511 case '/': // Slash separator
512 case '.': // Dot separator
513 case ':': // Colon separator
514 case 'T': // 'T' separator
515 if (i > 1 && format[i] != date_str[next - 1]) {
516 return std::errc::invalid_argument;
517 }
518 break;
519 }
520 is_specifier = false;
521 }
522
523 return std::errc{};
524}
525
526} // namespace detail
527
538template <typename Clock = std::chrono::system_clock>
539auto parse(typename Clock::time_point &time_point, string_view format,
540 string_view date_str) -> std::error_code {
541 detail::tm time_struct{};
542 auto error = detail::get_time(time_struct, format, date_str);
543 if (error != std::errc{}) {
544 return std::make_error_code(error);
545 }
546 std::time_t time_t{};
547 error = detail::mktime(time_t, time_struct);
548 if (error != std::errc{}) {
549 return std::make_error_code(error);
550 }
551 time_point = std::chrono::time_point_cast<typename Clock::duration>(
552 Clock::from_time_t(time_t) + std::chrono::nanoseconds{time_struct.tm_ms});
553 return std::error_code{};
554}
555
566template <typename Clock = std::chrono::system_clock>
567auto parse(string_view format, string_view date_str) ->
568 typename Clock::time_point {
569 typename Clock::time_point time_point{};
570 auto error = parse<Clock>(time_point, format, date_str);
571 if (error) {
572 throw std::system_error(error);
573 }
574 return time_point;
575}
576
577} // namespace chrono
578} // namespace mgutility
579
580#endif // MGUTILITY_CHRONO_PARSE_HPP
Extended tm structure with milliseconds.
Definition parse.hpp:50
uint32_t tm_ms
Milliseconds.
Definition parse.hpp:51