1- use crate :: { Error , ValR , ValT } ;
1+ use crate :: { Error , ValR , ValT , ValTx } ;
22use alloc:: string:: { String , ToString } ;
3- use chrono:: DateTime ;
3+ use chrono:: { DateTime , Datelike , FixedOffset , NaiveDateTime , TimeZone , Timelike , Utc } ;
4+
5+ /// Convert a UNIX epoch timestamp with optional fractions.
6+ fn epoch_to_datetime < V : ValT > ( v : & V ) -> Result < DateTime < Utc > , Error < V > > {
7+ let fail = || Error :: str ( format_args ! ( "cannot parse {v} as epoch timestamp" ) ) ;
8+ let val = if let Some ( i) = v. as_isize ( ) {
9+ ( i * 1000000 ) as i64
10+ } else {
11+ ( v. as_f64 ( ) ? * 1000000.0 ) as i64
12+ } ;
13+
14+ DateTime :: from_timestamp_micros ( val) . ok_or_else ( fail)
15+ }
16+
17+ /// Convert a date-time pair to a UNIX epoch timestamp.
18+ fn datetime_to_epoch < Tz : TimeZone , V : ValT > ( dt : DateTime < Tz > , frac : bool ) -> ValR < V > {
19+ if frac {
20+ Ok ( ( dt. timestamp_micros ( ) as f64 / 1e6 ) . into ( ) )
21+ } else {
22+ let seconds = dt. timestamp ( ) ;
23+ isize:: try_from ( seconds)
24+ . map ( V :: from)
25+ . or_else ( |_| V :: from_num ( & seconds. to_string ( ) ) )
26+ }
27+ }
28+
29+ /// Parse a "broken down time" array.
30+ fn array_to_datetime < V : ValT > ( v : & [ V ] ) -> Option < DateTime < Utc > > {
31+ let [ year, month, day, hour, min, sec] : & [ V ; 6 ] = v. get ( ..6 ) ?. try_into ( ) . ok ( ) ?;
32+ let sec = sec. as_f64 ( ) . ok ( ) ?;
33+ let u32 = |v : & V | -> Option < u32 > { v. as_isize ( ) ?. try_into ( ) . ok ( ) } ;
34+ Utc . with_ymd_and_hms (
35+ year. as_isize ( ) ?. try_into ( ) . ok ( ) ?,
36+ u32 ( month) ? + 1 ,
37+ u32 ( day) ?,
38+ u32 ( hour) ?,
39+ u32 ( min) ?,
40+ // the `as i8` cast saturates, returning a number in the range [-128, 128]
41+ ( sec. floor ( ) as i8 ) . try_into ( ) . ok ( ) ?,
42+ )
43+ . single ( ) ?
44+ . with_nanosecond ( ( sec. fract ( ) * 1e9 ) as u32 )
45+ }
46+
47+ /// Convert a DateTime<FixedOffset> to a "broken down time" array
48+ fn datetime_to_array < V : ValT > ( dt : DateTime < FixedOffset > ) -> [ V ; 8 ] {
49+ [
50+ V :: from ( dt. year ( ) as isize ) ,
51+ V :: from ( dt. month0 ( ) as isize ) ,
52+ V :: from ( dt. day ( ) as isize ) ,
53+ V :: from ( dt. hour ( ) as isize ) ,
54+ V :: from ( dt. minute ( ) as isize ) ,
55+ if dt. nanosecond ( ) > 0 {
56+ V :: from ( dt. second ( ) as f64 + dt. timestamp_subsec_micros ( ) as f64 / 1e6 )
57+ } else {
58+ V :: from ( dt. second ( ) as isize )
59+ } ,
60+ V :: from ( dt. weekday ( ) . num_days_from_sunday ( ) as isize ) ,
61+ V :: from ( dt. ordinal0 ( ) as isize ) ,
62+ ]
63+ }
464
565/// Parse an ISO 8601 timestamp string to a number holding the equivalent UNIX timestamp
666/// (seconds elapsed since 1970/01/01).
@@ -11,14 +71,7 @@ use chrono::DateTime;
1171pub fn from_iso8601 < V : ValT > ( s : & str ) -> ValR < V > {
1272 let dt = DateTime :: parse_from_rfc3339 ( s)
1373 . map_err ( |e| Error :: str ( format_args ! ( "cannot parse {s} as ISO-8601 timestamp: {e}" ) ) ) ?;
14- if s. contains ( '.' ) {
15- Ok ( ( dt. timestamp_micros ( ) as f64 / 1e6 ) . into ( ) )
16- } else {
17- let seconds = dt. timestamp ( ) ;
18- isize:: try_from ( seconds)
19- . map ( V :: from)
20- . or_else ( |_| V :: from_num ( & seconds. to_string ( ) ) )
21- }
74+ datetime_to_epoch ( dt, s. contains ( '.' ) )
2275}
2376
2477/// Format a number as an ISO 8601 timestamp string.
@@ -33,3 +86,36 @@ pub fn to_iso8601<V: ValT>(v: &V) -> Result<String, Error<V>> {
3386 Ok ( dt. format ( "%Y-%m-%dT%H:%M:%S%.6fZ" ) . to_string ( ) )
3487 }
3588}
89+
90+ /// Format a date (either number or array) in a given timezone.
91+ pub fn strftime < V : ValT > ( v : & V , fmt : & str , tz : impl TimeZone ) -> ValR < V > {
92+ let fail = || Error :: str ( format_args ! ( "cannot convert {v} to time" ) ) ;
93+ let dt = match v. clone ( ) . into_vec ( ) {
94+ Ok ( v) => array_to_datetime ( & v) . ok_or_else ( fail) ,
95+ Err ( _) => epoch_to_datetime ( v) ,
96+ } ?;
97+ let dt = dt. with_timezone ( & tz) . fixed_offset ( ) ;
98+ Ok ( dt. format ( fmt) . to_string ( ) . into ( ) )
99+ }
100+
101+ /// Convert an epoch timestamp to a "broken down time" array.
102+ pub fn gmtime < V : ValT > ( v : & V , tz : impl TimeZone ) -> ValR < V > {
103+ let dt = epoch_to_datetime ( v) ?;
104+ let dt = dt. with_timezone ( & tz) . fixed_offset ( ) ;
105+ datetime_to_array ( dt) . into_iter ( ) . map ( Ok ) . collect ( )
106+ }
107+
108+ /// Parse a string into a "broken down time" array.
109+ pub fn strptime < V : ValT > ( s : & str , fmt : & str ) -> ValR < V > {
110+ let dt = NaiveDateTime :: parse_from_str ( s, fmt)
111+ . map_err ( |e| Error :: str ( format_args ! ( "cannot parse {s} using {fmt}: {e}" ) ) ) ?;
112+ let dt = dt. and_utc ( ) . fixed_offset ( ) ;
113+ datetime_to_array ( dt) . into_iter ( ) . map ( Ok ) . collect ( )
114+ }
115+
116+ /// Parse an array into a UNIX epoch timestamp.
117+ pub fn mktime < V : ValT > ( v : & V ) -> ValR < V > {
118+ let fail = || Error :: str ( format_args ! ( "cannot convert {v} to time" ) ) ;
119+ let dt = array_to_datetime ( & v. clone ( ) . into_vec ( ) ?) . ok_or_else ( fail) ?;
120+ datetime_to_epoch ( dt, dt. timestamp_subsec_micros ( ) > 0 )
121+ }
0 commit comments