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#include "mgutility/std/charconv.hpp"
29#include "mgutility/std/string_view.hpp"
30
31#include <array>
32#include <cctype>
33#include <chrono>
34#include <stdexcept>
35#include <type_traits>
36
37namespace mgutility {
38namespace chrono {
39namespace detail {
40
44struct tm : std::tm {
45 uint32_t tm_ms;
46};
47
58auto parse_integer(mgutility::string_view str, uint32_t len, uint32_t &next,
59 uint32_t begin_offset = 0) -> int32_t {
60 int32_t result{0};
61
62 auto error = mgutility::from_chars(str.data() + next + begin_offset,
63 str.data() + len + next, result);
64
65 next = ++len + next;
66
67 if (error.ec != std::errc()) {
68 throw std::invalid_argument("value is not convertible!");
69 }
70
71 return result;
72}
73
82MGUTILITY_CNSTXPR auto check_range(int32_t value, int32_t min, int32_t max)
83 -> void {
84 if (value < min || value > max) {
85 throw std::out_of_range("value is out of range!");
86 }
87}
88
95auto MGUTILITY_CNSTXPR is_leap_year(uint32_t year) -> bool {
96 return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0);
97}
98
106MGUTILITY_CNSTXPR auto mktime(std::tm &tm) -> std::time_t {
107 MGUTILITY_CNSTXPR std::array<std::array<uint32_t, 12>, 2> num_of_days{
108 {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
109 31}, // 365 days in a common year
110 {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30,
111 31}}}; // 366 days in a leap year
112
113 std::time_t result{0};
114
115 // Check for out of range values in tm structure
116 if (tm.tm_mon > 12 || tm.tm_mon < 0 || tm.tm_mday > 31 || tm.tm_min > 60 ||
117 tm.tm_sec > 60 || tm.tm_hour > 24) {
118 throw std::out_of_range("value is out of range!");
119 }
120
121 tm.tm_year += 1900;
122
123 // Calculate the number of days since 1970
124 for (auto i{1970}; i < tm.tm_year; ++i) {
125 result += is_leap_year(i) ? 366 : 365;
126 }
127
128 // Add the days for the current year
129 for (auto i{0}; i < tm.tm_mon; ++i) {
130 result += num_of_days[is_leap_year(tm.tm_year)][i];
131 }
132
133 result += tm.tm_mday - 1; // nth day since 1970
134 result *= 24;
135 result += tm.tm_hour;
136 result *= 60;
137 result += tm.tm_min;
138 result *= 60;
139 result += tm.tm_sec;
140
141 return result;
142}
143
150MGUTILITY_CNSTXPR auto handle_timezone(tm &tm, int32_t offset) -> void {
151 const auto minute = offset % 100;
152 const auto hour = offset / 100;
153
154 if (offset < 0) {
155 // Adjust minutes
156 if (tm.tm_min + minute < 0) {
157 tm.tm_min += 60 - minute;
158 tm.tm_hour -= 1;
159 if (tm.tm_hour < 0) {
160 tm.tm_hour += 24;
161 tm.tm_mday -= 1;
162 }
163 } else {
164 tm.tm_min += minute;
165 }
166
167 // Adjust hours
168 if (tm.tm_hour + hour < 0) {
169 tm.tm_hour += 24 + hour;
170 tm.tm_mday -= 1;
171 } else {
172 tm.tm_hour += hour;
173 }
174 } else {
175 // Adjust minutes
176 if (tm.tm_min + minute >= 60) {
177 tm.tm_min -= 60 - minute;
178 tm.tm_hour += 1;
179 if (tm.tm_hour >= 24) {
180 tm.tm_hour -= 24;
181 tm.tm_mday += 1;
182 }
183 } else {
184 tm.tm_min += minute;
185 }
186
187 // Adjust hours
188 if (tm.tm_hour + hour >= 24) {
189 tm.tm_hour += hour - 24;
190 tm.tm_mday += 1;
191 if (tm.tm_mon == 11 && tm.tm_mday > 31) { // Handle December overflow
192 tm.tm_mday = 1;
193 tm.tm_mon = 0;
194 } else if (tm.tm_mday > 30) { // Handle month overflow for other months
195 tm.tm_mday = 1;
196 tm.tm_mon += 1;
197 }
198 } else {
199 tm.tm_hour += hour;
200 }
201 }
202}
203
213MGUTILITY_CNSTXPR auto get_time(string_view format, string_view date_str)
214 -> detail::tm {
215 int32_t count{0};
216 uint32_t begin{0}, end{0};
217
218 // Find the positions of format specifiers
219 for (auto i{0}; i < format.size(); ++i) {
220 switch (format[i]) {
221 case '{':
222 begin = i;
223 ++count;
224 break;
225 case '}':
226 end = i;
227 --count;
228 break;
229 }
230 if (begin < end)
231 break;
232 else if (count != 0 && end < begin)
233 break;
234 }
235
236 if (format[begin + 1] != ':' || (end - begin < 3 || count != 0))
237 throw std::invalid_argument("invalid format string!");
238
239 detail::tm tm{};
240 uint32_t next{0};
241
242 // Parse the date and time string based on the format specifiers
243 for (auto i{begin}; i < end; ++i) {
244 switch (format[i]) {
245 case '%': {
246 if (i + 1 >= format.size())
247 throw std::invalid_argument("invalid format string!");
248 switch (format[i + 1]) {
249 case 'Y': // Year with century (4 digits)
250 tm.tm_year = parse_integer(date_str, 4, next) % 1900;
251 break;
252 case 'm': // Month (01-12)
253 tm.tm_mon = parse_integer(date_str, 2, next) - 1;
254 check_range(tm.tm_mon, 0, 11);
255 break;
256 case 'd': // Day of the month (01-31)
257 tm.tm_mday = parse_integer(date_str, 2, next);
258 check_range(tm.tm_mday, 1, 31);
259 break;
260 case 'F': { // Full date (YYYY-MM-DD)
261 tm.tm_year = parse_integer(date_str, 4, next) % 1900;
262 tm.tm_mon = parse_integer(date_str, 2, next) - 1;
263 tm.tm_mday = parse_integer(date_str, 2, next);
264 check_range(tm.tm_mon, 0, 11);
265 check_range(tm.tm_mday, 1, 31);
266 } break;
267 case 'H': // Hour (00-23)
268 tm.tm_hour = parse_integer(date_str, 2, next);
269 check_range(tm.tm_hour, 0, 23);
270 break;
271 case 'M': // Minute (00-59)
272 tm.tm_min = parse_integer(date_str, 2, next);
273 check_range(tm.tm_min, 0, 59);
274 break;
275 case 'S': // Second (00-59)
276 tm.tm_sec = parse_integer(date_str, 2, next);
277 check_range(tm.tm_sec, 0, 59);
278 break;
279 case 'T': { // Full time (HH:MM:SS)
280 tm.tm_hour = parse_integer(date_str, 2, next);
281 tm.tm_min = parse_integer(date_str, 2, next);
282 tm.tm_sec = parse_integer(date_str, 2, next);
283 check_range(tm.tm_hour, 0, 23);
284 check_range(tm.tm_min, 0, 59);
285 check_range(tm.tm_sec, 0, 59);
286 } break;
287 case 'f': // Milliseconds (000-999)
288 tm.tm_ms = parse_integer(date_str, 3, next);
289 check_range(tm.tm_ms, 0, 999);
290 break;
291 case 'z': { // Timezone offset (+/-HHMM)
292 if (*(date_str.begin() + next - 1) != 'Z') {
293 char sign{};
294 auto diff{0};
295 for (auto j{next - 1}; j < date_str.size(); ++j) {
296 if (date_str[j] == '-' || date_str[j] == '+') {
297 sign = date_str[j];
298 diff = j - next + 1;
299 break;
300 }
301 }
302 auto hour_offset_str =
303 string_view{date_str.data() + next + diff, date_str.size() - 1};
304 auto pos = hour_offset_str.find(':');
305 auto offset{0};
306 if (pos < 3 && pos > 0) {
307 next = 0;
308 auto hour_offset = parse_integer(hour_offset_str, 2, next);
309 auto min_offset = parse_integer(hour_offset_str, 2, next);
310 offset = hour_offset * 100 + min_offset;
311 } else {
312 if (date_str.size() - next > 4 + diff)
313 throw std::invalid_argument("value is not convertible!");
314 offset =
315 parse_integer(date_str, date_str.size() - next, next, diff);
316 }
317 check_range(offset, 0, 1200);
318 switch (sign) {
319 case '+':
320 handle_timezone(tm, offset * -1);
321 break;
322 case '-':
323 handle_timezone(tm, offset);
324 break;
325 }
326 }
327 } break;
328 default:
329 throw std::invalid_argument("unsupported format specifier!");
330 }
331 } break;
332 case ' ': // Space separator
333 case '-': // Dash separator
334 case '/': // Slash separator
335 case '.': // Dot separator
336 case ':': // Colon separator
337 if (i > 1 && format[i] != date_str[next - 1])
338 throw std::invalid_argument("value is not convertible!");
339 break;
340 }
341 }
342
343 return tm;
344}
345
346} // namespace detail
347
356auto parse(string_view format, string_view date_str)
357 -> std::chrono::system_clock::time_point {
358 auto tm = detail::get_time(format, date_str);
359 auto time_t = detail::mktime(tm);
360 std::chrono::system_clock::time_point clock =
361 std::chrono::system_clock::from_time_t(time_t);
362 clock += std::chrono::milliseconds(tm.tm_ms);
363 return clock;
364}
365
366} // namespace chrono
367} // namespace mgutility
368
369#endif // MGUTILITY_CHRONO_PARSE_HPP
A basic string view class template.
Definition string_view.hpp:61
Extended tm structure with milliseconds.
Definition parse.hpp:44
uint32_t tm_ms
Milliseconds.
Definition parse.hpp:45