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