g1t/crates/contracts/src/time.rs
| 1 | //! Timestamps. Everywhere in g1t, in storage and on the wire, a time is an |
| 2 | //! RFC 3339 string in UTC with millisecond precision, such as |
| 3 | //! `2026-10-02T05:16:19.000Z`. Strings in this one fixed format sort and |
| 4 | //! compare as text, so they need no parsing to be ordered. |
| 5 | |
| 6 | /// SQLite's expression for the current time in g1t's format. |
| 7 | pub const SQL_NOW: &str = "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')"; |
| 8 | |
| 9 | /// SQLite's expression for a time `seconds` from now, in g1t's format. |
| 10 | pub fn sql_after(seconds: u64) -> String { |
| 11 | format!("strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '+{seconds} seconds')") |
| 12 | } |
| 13 | |
| 14 | /// Milliseconds since the Unix epoch as an RFC 3339 UTC timestamp. |
| 15 | pub fn rfc3339(ms: u64) -> String { |
| 16 | let (seconds, millis) = (ms / 1000, ms % 1000); |
| 17 | let (days, rest) = (seconds / 86_400, seconds % 86_400); |
| 18 | let (hour, minute, second) = (rest / 3600, rest % 3600 / 60, rest % 60); |
| 19 | |
| 20 | // Days since the epoch to a calendar date, after Howard Hinnant's |
| 21 | // civil_from_days. The era arithmetic starts years in March. |
| 22 | let z = days as i64 + 719_468; |
| 23 | let era = z.div_euclid(146_097); |
| 24 | let day_of_era = z.rem_euclid(146_097); |
| 25 | let year_of_era = |
| 26 | (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; |
| 27 | let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); |
| 28 | let shifted_month = (5 * day_of_year + 2) / 153; |
| 29 | let day = day_of_year - (153 * shifted_month + 2) / 5 + 1; |
| 30 | let month = if shifted_month < 10 { |
| 31 | shifted_month + 3 |
| 32 | } else { |
| 33 | shifted_month - 9 |
| 34 | }; |
| 35 | let year = year_of_era + era * 400 + i64::from(month <= 2); |
| 36 | |
| 37 | format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z") |
| 38 | } |
| 39 | |
| 40 | /// An RFC 3339 UTC timestamp in g1t's format (or without milliseconds) |
| 41 | /// back to milliseconds since the Unix epoch. `None` for anything else. |
| 42 | pub fn parse_rfc3339(text: &str) -> Option<u64> { |
| 43 | let text = text.strip_suffix('Z')?; |
| 44 | let (date, time) = text.split_once('T')?; |
| 45 | let mut date = date.splitn(3, '-').map(|part| part.parse::<i64>().ok()); |
| 46 | let (year, month, day) = (date.next()??, date.next()??, date.next()??); |
| 47 | let (clock, millis) = match time.split_once('.') { |
| 48 | Some((clock, fraction)) => { |
| 49 | let digits: String = fraction.chars().chain("000".chars()).take(3).collect(); |
| 50 | (clock, digits.parse::<u64>().ok()?) |
| 51 | } |
| 52 | None => (time, 0), |
| 53 | }; |
| 54 | let mut clock = clock.splitn(3, ':').map(|part| part.parse::<u64>().ok()); |
| 55 | let (hour, minute, second) = (clock.next()??, clock.next()??, clock.next()??); |
| 56 | if !(1..=12).contains(&month) || !(1..=31).contains(&day) || hour > 23 || minute > 59 || second > 60 { |
| 57 | return None; |
| 58 | } |
| 59 | // Howard Hinnant's days_from_civil, the inverse of the above. |
| 60 | let y = if month <= 2 { year - 1 } else { year }; |
| 61 | let era = y.div_euclid(400); |
| 62 | let year_of_era = y - era * 400; |
| 63 | let day_of_year = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1; |
| 64 | let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; |
| 65 | let days = u64::try_from(era * 146_097 + day_of_era - 719_468).ok()?; |
| 66 | Some(((days * 86_400 + hour * 3600 + minute * 60 + second) * 1000) + millis) |
| 67 | } |
| 68 | |
| 69 | #[cfg(test)] |
| 70 | mod tests { |
| 71 | use super::*; |
| 72 | |
| 73 | #[test] |
| 74 | fn parses_what_it_formats() { |
| 75 | for ms in [0, 951_782_400_000, 1_790_918_179_123, 4_102_444_799_999] { |
| 76 | assert_eq!(parse_rfc3339(&rfc3339(ms)), Some(ms)); |
| 77 | } |
| 78 | assert_eq!(parse_rfc3339("2026-10-02T05:16:19Z"), Some(1_790_918_179_000)); |
| 79 | assert_eq!(parse_rfc3339("yesterday"), None); |
| 80 | assert_eq!(parse_rfc3339("2026-13-02T05:16:19Z"), None); |
| 81 | } |
| 82 | |
| 83 | #[test] |
| 84 | fn formats_known_instants() { |
| 85 | assert_eq!(rfc3339(0), "1970-01-01T00:00:00.000Z"); |
| 86 | assert_eq!(rfc3339(951_782_400_000), "2000-02-29T00:00:00.000Z"); |
| 87 | assert_eq!(rfc3339(1_790_918_179_123), "2026-10-02T05:16:19.123Z"); |
| 88 | assert_eq!(rfc3339(4_102_444_799_999), "2099-12-31T23:59:59.999Z"); |
| 89 | } |
| 90 | |
| 91 | #[test] |
| 92 | fn later_times_sort_later_as_text() { |
| 93 | let times: Vec<String> = [0, 999, 1000, 86_399_999, 86_400_000, 1_790_918_179_123] |
| 94 | .into_iter() |
| 95 | .map(rfc3339) |
| 96 | .collect(); |
| 97 | assert!(times.windows(2).all(|pair| pair[0] < pair[1])); |
| 98 | } |
| 99 | } |