DAW JSON Link
Loading...
Searching...
No Matches
daw_json_parse_iso8601_utils.h
Go to the documentation of this file.
1// Copyright (c) Darrell Wright
2//
3// Distributed under the Boost Software License, Version 1.0. (See accompanying
4// file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt)
5//
6// Official repository: https://github.com/beached/daw_json_link
7//
8
9#pragma once
10
12
16
17#include <daw/daw_arith_traits.h>
18#include <daw/daw_attributes.h>
19#include <daw/daw_cpp_feature_check.h>
20#include <daw/daw_cxmath.h>
21#include <daw/daw_not_null.h>
22#include <daw/daw_string_view.h>
23#include <daw/daw_traits.h>
24#include <daw/daw_uint_buffer.h>
25
26#include <cassert>
27#include <chrono>
28#include <cstdint>
29#include <type_traits>
30
31namespace daw::json {
32 inline namespace DAW_JSON_VER {
33 namespace parse_utils {
34 template<typename Result, std::size_t count>
35 constexpr Result parse_unsigned( daw::not_null<char const *> digit_str ) {
36 auto result = UInt64( );
37 for( std::size_t n = 0; n < count; ++n ) {
38 auto const dig =
39 to_uint64( json_details::parse_digit( digit_str[n] ) );
40 if( dig >= 10 ) {
41 break;
42 }
43 result *= 10U;
44 result += dig;
45 }
46 return static_cast<Result>( result );
47 }
48
49 template<typename Result>
50 constexpr Result
51 parse_unsigned2( daw::not_null<char const *> digit_str ) {
52 auto result = UInt64( );
53 unsigned dig = json_details::parse_digit( *digit_str );
54 while( dig < 10 ) {
55 result *= 10U;
56 result += dig;
57 ++digit_str;
58 dig = json_details::parse_digit( *digit_str );
59 }
60 return static_cast<Result>( result );
61 }
62
63 DAW_ATTRIB_FLATINLINE constexpr bool is_number( char c ) {
64 return json_details::parse_digit( c ) < 10U;
65 }
66 } // namespace parse_utils
67
68 namespace datetime {
69 using attoseconds = std::chrono::duration<std::int64_t, std::atto>;
70
71 namespace datetime_details {
72 template<typename Result>
73 constexpr Result parse_number( daw::string_view sv ) {
74 static_assert( daw::digits10<Result> >= 4 );
75 daw_json_ensure( not sv.empty( ), ErrorReason::InvalidNumber );
76 Result result = 0;
77 Result sign = 1;
78 if( sv.front( ) == '-' ) {
79 if constexpr( daw::is_signed_v<Result> ) {
80 sign = -1;
81 }
82 sv.remove_prefix( );
83 } else if( sv.front( ) == '+' ) {
84 sv.remove_prefix( );
85 }
86 while( not sv.empty( ) ) {
87 auto const dig = json_details::parse_digit( sv.pop_front( ) );
88 daw_json_ensure( dig < 10U, ErrorReason::InvalidNumber );
89 result *= 10;
90 result += static_cast<Result>( dig );
91 }
92 return result * sign;
93 }
94 } // namespace datetime_details
95 // See:
96 // https://stackoverflow.com/questions/16773285/how-to-convert-stdchronotime-point-to-stdtm-without-using-time-t
97 template<typename TP = std::chrono::time_point<std::chrono::system_clock,
98 std::chrono::milliseconds>>
99 constexpr TP civil_to_time_point( std::int32_t yr, std::uint32_t mo,
100 std::uint32_t dy, std::uint32_t hr,
101 std::uint32_t mn, std::uint32_t se,
102 std::uint64_t attosecond ) {
103 using Clock = typename TP::clock;
104 using Duration = typename TP::duration;
105 DAW_CPP23_STATIC_LOCAL constexpr auto calc =
106 []( std::int32_t y,
107 std::uint32_t m,
108 std::uint32_t d,
109 std::uint32_t h,
110 std::uint32_t min,
111 std::uint32_t s,
112 std::uint64_t atto ) DAW_JSON_CPP23_STATIC_CALL_OP {
113 y -= static_cast<std::int32_t>( m ) <= 2;
114 std::int32_t const era = ( y >= 0 ? y : y - 399 ) / 400;
115 auto const yoe = static_cast<std::uint32_t>(
116 static_cast<std::int32_t>( y ) - era * 400 ); // [0, 399]
117 auto const doy = static_cast<std::uint32_t>(
118 ( 153 * ( static_cast<std::int32_t>( m ) +
119 ( static_cast<std::int32_t>( m ) > 2 ? -3 : 9 ) ) +
120 2 ) /
121 5 +
122 static_cast<std::int32_t>( d ) - 1 ); // [0, 365]
123 std::uint32_t const doe =
124 yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
125 auto const days_since_epoch =
126 static_cast<int64_t>( era ) * 146097LL +
127 static_cast<std::int64_t>( doe ) - 719468LL;
128
129 auto const dur = std::chrono::floor<Duration>(
130 attoseconds( static_cast<std::int64_t>( atto ) ) );
131 auto const whole_seconds =
132 std::chrono::seconds( days_since_epoch * 86400LL +
133 static_cast<std::int64_t>( h ) * 3600LL +
134 static_cast<std::int64_t>( min ) * 60LL +
135 static_cast<std::int64_t>( s ) );
136 return std::chrono::time_point<std::chrono::system_clock,
137 Duration>{ } +
138 ( std::chrono::duration_cast<Duration>( whole_seconds ) +
139 dur );
140 };
141 // Not all clocks have the same epoch. This should account for the
142 // offset and adjust the time_point so that the days prior are in
143 // relation to unix epoch. If system_clock is used, as is the default
144 // for the return value, it will be zero and should be removed by the
145 // compiler
146 auto result = calc( yr, mo, dy, hr, mn, se, attosecond );
147
148 if constexpr( std::is_same_v<Clock, std::chrono::system_clock> ) {
149 return result;
150 } else {
151#if defined( __cpp_lib_chrono ) and __cpp_lib_chrono >= 201907
152 // We have clock_cast
153 auto const match_duration =
154 std::chrono::time_point_cast<Duration>( result );
155 auto const match_clock =
156 std::chrono::clock_cast<Clock>( match_duration );
157 return match_clock;
158#else
159 // This is a guess and will not be constexpr
160
161 // System epoch is unix epoch on(gcc/clang/msvc)
162 auto const system_epoch = std::chrono::floor<std::chrono::hours>(
163 std::chrono::system_clock::now( ).time_since_epoch( ) +
164 std::chrono::minutes( 30 ) );
165 auto const clock_epoch = std::chrono::floor<std::chrono::hours>(
166 Clock::now( ).time_since_epoch( ) + std::chrono::minutes( 30 ) );
167
168 DAW_CPP23_STATIC_LOCAL constexpr auto offset =
169 std::chrono::duration_cast<std::chrono::milliseconds>(
170 clock_epoch - system_epoch );
171 return std::chrono::duration_cast<Duration>( result + offset );
172#endif
173 }
174 }
175
176 struct date_parts {
177 std::int32_t year;
178 std::uint32_t month;
179 std::uint32_t day;
180 };
181
182 constexpr date_parts
183 parse_iso_8601_date( daw::string_view timestamp_str ) {
184 auto result = date_parts{ 0, 0, 0 };
185 result.day = parse_utils::parse_unsigned<std::uint_least32_t, 2>(
186 std::data( timestamp_str.pop_back( 2U ) ) );
187 daw_json_ensure( result.day >= 1 and result.day <= 31,
188 ErrorReason::InvalidTimestamp );
189 if( not parse_utils::is_number( timestamp_str.back( ) ) ) {
190 timestamp_str.remove_suffix( );
191 }
192 result.month = parse_utils::parse_unsigned<std::uint_least32_t, 2>(
193 std::data( timestamp_str.pop_back( 2U ) ) );
194 daw_json_ensure( result.month >= 1 and result.month <= 12,
195 ErrorReason::InvalidTimestamp );
196 if( not parse_utils::is_number( timestamp_str.back( ) ) ) {
197 timestamp_str.remove_suffix( );
198 }
199 result.year =
200 datetime_details::parse_number<std::int_least32_t>( timestamp_str );
201 auto max_day = std::uint32_t{ 31 };
202 if( result.month == 2 ) {
203 auto const is_leap_year =
204 result.year % 4 == 0 and
205 ( result.year % 100 != 0 or result.year % 400 == 0 );
206 max_day = is_leap_year ? 29U : 28U;
207 } else if( result.month == 4 or result.month == 6 or
208 result.month == 9 or result.month == 11 ) {
209 max_day = 30;
210 }
211 daw_json_ensure( result.day <= max_day,
212 ErrorReason::InvalidTimestamp );
213 return result;
214 }
215
216 struct time_parts {
217 std::uint_least32_t hour;
218 std::uint_least32_t minute;
219 std::uint_least32_t second;
220 std::uint64_t attosecond;
221 };
222
223 constexpr time_parts
224 parse_iso_8601_time( daw::string_view timestamp_str ) {
225 auto result = time_parts{ 0, 0, 0, 0 };
226 result.hour = parse_utils::parse_unsigned<std::uint_least32_t, 2>(
227 std::data( timestamp_str.pop_front( 2 ) ) );
228 daw_json_ensure( result.hour <= 24, ErrorReason::InvalidTimestamp );
229 if( not parse_utils::is_number( timestamp_str.front( ) ) ) {
230 timestamp_str.remove_prefix( );
231 }
232 result.minute = parse_utils::parse_unsigned<std::uint_least32_t, 2>(
233 std::data( timestamp_str.pop_front( 2 ) ) );
234 daw_json_ensure( result.minute <= 59, ErrorReason::InvalidTimestamp );
235 if( timestamp_str.empty( ) ) {
236 daw_json_ensure( result.hour != 24 or result.minute == 0,
237 ErrorReason::InvalidTimestamp );
238 return result;
239 }
240 if( not parse_utils::is_number( timestamp_str.front( ) ) ) {
241 timestamp_str.remove_prefix( );
242 }
243 result.second = parse_utils::parse_unsigned<std::uint_least32_t, 2>(
244 std::data( timestamp_str.pop_front( 2 ) ) );
245 daw_json_ensure( result.second <= 60, ErrorReason::InvalidTimestamp );
246 if( timestamp_str.empty( ) ) {
247 daw_json_ensure( result.hour != 24 or
248 ( result.minute == 0 and result.second == 0 ),
249 ErrorReason::InvalidTimestamp );
250 return result;
251 }
252 daw_json_ensure( timestamp_str.front( ) == '.',
253 ErrorReason::InvalidTimestamp );
254 timestamp_str.remove_prefix( );
255 daw_json_ensure( not timestamp_str.empty( ),
256 ErrorReason::InvalidTimestamp );
257 for( char const c : timestamp_str ) {
258 daw_json_ensure( parse_utils::is_number( c ),
259 ErrorReason::InvalidTimestamp );
260 }
261 auto const precision =
262 (std::min)( { timestamp_str.size( ), std::size_t{ 18 } } );
263 auto const attosecond_str = timestamp_str.substr( 0, precision );
264 result.attosecond =
265 datetime_details::parse_number<std::uint64_t>( attosecond_str );
266 result.attosecond *= daw::cxmath::pow10( 18 - precision );
268 result.hour != 24 or
269 ( result.minute == 0 and result.second == 0 and
270 result.attosecond == 0 ),
271 ErrorReason::InvalidTimestamp );
272 return result;
273 }
274
275 template<typename TP>
276 constexpr TP parse_iso8601_timestamp( daw::string_view ts ) {
277 DAW_CPP23_STATIC_LOCAL constexpr daw::string_view t_str = "T";
278 auto const date_str = ts.pop_front_until( t_str );
279 if( ts.empty( ) ) {
280 daw_json_error( true,
281 ErrorReason::InvalidTimestamp ); // Invalid timestamp,
282 // missing T separator
283 }
284
285 date_parts const ymd = parse_iso_8601_date( date_str );
286 auto time_str = ts.pop_front_until(
287 []( char c ) DAW_JSON_CPP23_STATIC_CALL_OP {
288 return not( parse_utils::is_number( c ) | ( c == ':' ) |
289 ( c == '.' ) );
290 },
291 nodiscard );
292 // TODO: verify or parse timezone
293 auto const hms = parse_iso_8601_time( time_str );
294
295 auto const local_tp = civil_to_time_point<TP>( ymd.year,
296 ymd.month,
297 ymd.day,
298 hms.hour,
299 hms.minute,
300 hms.second,
301 hms.attosecond );
302 if( ts.empty( ) or ts.front( ) == 'Z' ) {
303 return local_tp;
304 }
305 daw_json_ensure( std::size( ts ) == 5 or std::size( ts ) == 6,
306 ErrorReason::InvalidTimestamp );
307 // The format will be (+|-)hh[:]mm
308 bool sign = false;
309 switch( ts.front( ) ) {
310 case '+':
311 sign = true;
312 break;
313 case '-':
314 break;
315 default:
316 daw_json_error( true, ErrorReason::InvalidTimestamp );
317 }
318 ts.remove_prefix( );
319 auto hr_offset_str = ts.pop_front( 2 );
320 auto mn_offset_str = daw::string_view{ };
321 if( hr_offset_str.size( ) == 4 ) {
322 mn_offset_str = hr_offset_str.pop_back( 2 );
323 }
324 daw_json_ensure( hr_offset_str.size( ) == 2 and
325 parse_utils::is_number( hr_offset_str[0] ) and
326 parse_utils::is_number( hr_offset_str[1] ),
327 ErrorReason::InvalidTimestamp );
328
329 auto const hr_offset =
330 parse_utils::parse_unsigned<std::uint_least32_t, 2>(
331 std::data( hr_offset_str ) );
332 daw_json_ensure( hr_offset <= 24, ErrorReason::InvalidTimestamp );
333 if( not ts.empty( ) and ts.front( ) == ':' ) {
334 ts.remove_prefix( );
335 }
336 if( mn_offset_str.empty( ) ) {
337 mn_offset_str = ts.pop_front( 2 );
338 }
339 daw_json_ensure( mn_offset_str.size( ) == 2 and ts.empty( ) and
340 parse_utils::is_number( mn_offset_str[0] ) and
341 parse_utils::is_number( mn_offset_str[1] ),
342 ErrorReason::InvalidTimestamp );
343 auto const mn_offset =
344 parse_utils::parse_unsigned<std::uint_least32_t, 2>(
345 std::data( mn_offset_str ) );
346 daw_json_ensure( mn_offset <= 61, ErrorReason::InvalidTimestamp );
347
348 if( hr_offset == 0 and mn_offset == 0 ) {
349 return std::chrono::time_point_cast<typename TP::duration>(
350 local_tp );
351 }
352 // Apply the offset as a duration on the time_point so that
353 // borrowing across hour/day boundaries (e.g. 01:14 - 02:30)
354 // is handled by chrono instead of wrapping unsigned fields.
355 auto const offset = std::chrono::seconds{
356 static_cast<std::chrono::seconds::rep>( hr_offset ) * 3600 +
357 static_cast<std::chrono::seconds::rep>( mn_offset ) * 60 };
358 // Want to subtract offset from current time, we are converting to UTC
359 if( sign ) {
360 // Positive offset
361 return std::chrono::time_point_cast<typename TP::duration>(
362 local_tp ) -
363 std::chrono::duration_cast<typename TP::duration>( offset );
364 }
365 // Negative offset
366 return std::chrono::time_point_cast<typename TP::duration>( local_tp ) +
367 std::chrono::duration_cast<typename TP::duration>( offset );
368 }
369 struct ymdhms {
370 std::int_least32_t year;
371 std::uint_least32_t month;
372 std::uint_least32_t day;
373 std::uint_least32_t hour;
374 std::uint_least32_t minute;
375 std::uint_least32_t second;
376 std::uint64_t attosecond;
377 };
378
379 template<typename Clock, typename Duration>
380 constexpr ymdhms
381 time_point_to_civil( std::chrono::time_point<Clock, Duration> const tp ) {
382 auto dur_from_epoch = tp.time_since_epoch( );
383 using Days =
384 std::chrono::duration<std::int_least32_t, std::ratio<86400>>;
385 auto const whole_seconds =
386 std::chrono::floor<std::chrono::seconds>( dur_from_epoch );
387 auto const days_since_epoch = std::chrono::floor<Days>( whole_seconds );
388 std::int_least32_t z = days_since_epoch.count( );
389 z += 719468;
390 std::int_least32_t const era = ( z >= 0 ? z : z - 146096 ) / 146097;
391 auto const doe =
392 static_cast<std::uint_least32_t>( z - era * 146097 ); // [0, 146096]
393 std::uint_least32_t const yoe =
394 ( doe - doe / 1460 + doe / 36524 - doe / 146096 ) / 365; // [0, 399]
395 std::int_least32_t const y =
396 static_cast<std::int_least32_t>( yoe ) + era * 400;
397 std::uint_least32_t const doy =
398 doe - ( 365 * yoe + yoe / 4 - yoe / 100 ); // [0, 365]
399 std::uint_least32_t const mp = ( 5 * doy + 2 ) / 153; // [0, 11]
400 std::uint_least32_t const d = doy - ( 153 * mp + 2 ) / 5 + 1; // [1, 31]
401 auto const m = static_cast<std::uint_least32_t>(
402 static_cast<std::int_least32_t>( mp ) +
403 ( static_cast<std::int_least32_t>( mp ) < 10 ? 3 : -9 ) ); // [1, 12]
404
405 auto dur_in_day =
406 whole_seconds -
407 std::chrono::duration_cast<std::chrono::seconds>( days_since_epoch );
408 auto const hrs =
409 std::chrono::duration_cast<std::chrono::hours>( dur_in_day );
410 dur_in_day -= hrs;
411 auto const min =
412 std::chrono::duration_cast<std::chrono::minutes>( dur_in_day );
413 dur_in_day -= min;
414 auto const sec =
415 std::chrono::duration_cast<std::chrono::seconds>( dur_in_day );
416 auto const subseconds =
417 dur_from_epoch -
418 std::chrono::duration_cast<Duration>( whole_seconds );
419 auto const dur = std::chrono::duration_cast<attoseconds>( subseconds );
420 return ymdhms{ y + ( m <= 2 ),
421 m,
422 d,
423 static_cast<std::uint_least32_t>( hrs.count( ) ),
424 static_cast<std::uint_least32_t>( min.count( ) ),
425 static_cast<std::uint_least32_t>( sec.count( ) ),
426 static_cast<std::uint64_t>( dur.count( ) ) };
427 }
428
429 constexpr std::string_view month_short_name( unsigned m ) {
430 switch( m ) {
431 case 1:
432 return { "Jan" };
433 case 2:
434 return { "Feb" };
435 case 3:
436 return { "Mar" };
437 case 4:
438 return { "Apr" };
439 case 5:
440 return { "May" };
441 case 6:
442 return { "Jun" };
443 case 7:
444 return { "Jul" };
445 case 8:
446 return { "Aug" };
447 case 9:
448 return { "Sep" };
449 case 10:
450 return { "Oct" };
451 case 11:
452 return { "Nov" };
453 case 12:
454 return { "Dec" };
455 default:
456 DAW_UNLIKELY_BRANCH
457 daw_json_error( true,
458 ErrorReason::InvalidTimestamp ); // Invalid month
459 }
460 }
461
462 // Formula from
463 // http://howardhinnant.github.io/date_algorithms.html#weekday_from_days
464 template<typename Duration>
465 constexpr std::string_view short_day_of_week(
466 std::chrono::time_point<std::chrono::system_clock, Duration> tp ) {
467 using days = std::chrono::duration<long, std::ratio<86400>>;
468 auto const z =
469 std::chrono::duration_cast<days>( tp.time_since_epoch( ) ).count( );
470 auto const dow = z >= -4L ? ( z + 4L ) % 7L : ( z + 5L ) % 7L + 6L;
471 switch( dow ) {
472 case 0:
473 return { "Sun" };
474 case 1:
475 return { "Mon" };
476 case 2:
477 return { "Tue" };
478 case 3:
479 return { "Wed" };
480 case 4:
481 return { "Thu" };
482 case 5:
483 return { "Fri" };
484 case 6:
485 return { "Sat" };
486 default:
487 DAW_UNLIKELY_BRANCH
488 daw_json_error( true,
489 ErrorReason::InvalidTimestamp ); // Invalid month
490 }
491 }
492
493 namespace datetime_details {
494 constexpr std::uint_least32_t month2num( std::string_view ts ) {
495 daw_json_ensure( std::size( ts ) >= 3,
496 ErrorReason::InvalidTimestamp );
497 auto const b0 = static_cast<std::uint_least32_t>(
498 static_cast<unsigned char>( ts[0] ) );
499 auto const b1 = static_cast<std::uint_least32_t>(
500 static_cast<unsigned char>( ts[1] ) );
501 auto const b2 = static_cast<std::uint_least32_t>(
502 static_cast<unsigned char>( ts[2] ) );
503 return ( b0 << 16U ) | ( b1 << 8U ) | b2;
504 }
505 } // namespace datetime_details
506
507 constexpr unsigned parse_short_month( std::string_view ts ) {
508 // Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
509 switch( datetime_details::month2num( ts ) ) {
510 case datetime_details::month2num( "Jan" ):
511 return 1;
512 case datetime_details::month2num( "Feb" ):
513 return 2;
514 case datetime_details::month2num( "Mar" ):
515 return 3;
516 case datetime_details::month2num( "Apr" ):
517 return 4;
518 case datetime_details::month2num( "May" ):
519 return 5;
520 case datetime_details::month2num( "Jun" ):
521 return 6;
522 case datetime_details::month2num( "Jul" ):
523 return 7;
524 case datetime_details::month2num( "Aug" ):
525 return 8;
526 case datetime_details::month2num( "Sep" ):
527 return 9;
528 case datetime_details::month2num( "Oct" ):
529 return 10;
530 case datetime_details::month2num( "Nov" ):
531 return 11;
532 case datetime_details::month2num( "Dec" ):
533 return 12;
534 default:
535 DAW_UNLIKELY_BRANCH
536 daw_json_error( true,
537 ErrorReason::InvalidTimestamp ); // Invalid month
538 }
539 }
540 } // namespace datetime
541 } // namespace DAW_JSON_VER
542} // namespace daw::json
#define daw_json_ensure(Bool,...)
Ensure that Bool is true. If false pass rest of args to daw_json_error.
#define DAW_JSON_CPP23_STATIC_CALL_OP
DAW_ATTRIB_NOINLINE void daw_json_error(bool b, ErrorReason reason)
constexpr std::string_view short_day_of_week(std::chrono::time_point< std::chrono::system_clock, Duration > tp)
constexpr ymdhms time_point_to_civil(std::chrono::time_point< Clock, Duration > const tp)
constexpr TP civil_to_time_point(std::int32_t yr, std::uint32_t mo, std::uint32_t dy, std::uint32_t hr, std::uint32_t mn, std::uint32_t se, std::uint64_t attosecond)
constexpr Result parse_unsigned(daw::not_null< char const * > digit_str)
constexpr Result parse_unsigned2(daw::not_null< char const * > digit_str)
Customization point traits.
#define DAW_JSON_VER
The version string used in namespace definitions. Must be a valid namespace name.
Definition version.h:20