1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
910//! Stats-related timestamp code.
1112use std::ops::Range;
1314use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
1516/// Parses a specific subset of ISO8061 timestamps.
17///
18/// This has very specific semantics so that it can enable pushdown on string
19/// timestamps in JSON. See doc/user/content/sql/functions/pushdown.md for
20/// details.
21pub fn try_parse_monotonic_iso8601_timestamp<'a>(a: &'a str) -> Option<NaiveDateTime> {
22const YYYY: Range<usize> = 0..0 + "YYYY".len();
23const LIT_DASH_0: Range<usize> = YYYY.end..YYYY.end + "-".len();
24const MM: Range<usize> = LIT_DASH_0.end..LIT_DASH_0.end + "MM".len();
25const LIT_DASH_1: Range<usize> = MM.end..MM.end + "-".len();
26const DD: Range<usize> = LIT_DASH_1.end..LIT_DASH_1.end + "DD".len();
27const LIT_T: Range<usize> = DD.end..DD.end + "T".len();
28const HH: Range<usize> = LIT_T.end..LIT_T.end + "HH".len();
29const LIT_COLON_0: Range<usize> = HH.end..HH.end + ":".len();
30const MI: Range<usize> = LIT_COLON_0.end..LIT_COLON_0.end + "MI".len();
31const LIT_COLON_1: Range<usize> = MI.end..MI.end + ":".len();
32const SS: Range<usize> = LIT_COLON_1.end..LIT_COLON_1.end + "SS".len();
33const LIT_DOT: Range<usize> = SS.end..SS.end + ".".len();
34// NB "MS" pattern is shorter than what it matches, so hardcode the 3.
35const MS: Range<usize> = LIT_DOT.end..LIT_DOT.end + 3;
36const LIT_Z: Range<usize> = MS.end..MS.end + "Z".len();
3738// The following assumes this is ASCII so do a quick check first.
39if !a.is_ascii() {
40return None;
41 }
4243if a.len() != LIT_Z.end {
44return None;
45 }
46if &a[LIT_DASH_0] != "-"
47|| &a[LIT_DASH_1] != "-"
48|| &a[LIT_T] != "T"
49|| &a[LIT_COLON_0] != ":"
50|| &a[LIT_COLON_1] != ":"
51|| &a[LIT_DOT] != "."
52|| &a[LIT_Z] != "Z"
53{
54return None;
55 }
56let yyyy = a[YYYY].parse().ok()?;
57let mm = a[MM].parse().ok()?;
58let dd = a[DD].parse().ok()?;
59let hh = a[HH].parse().ok()?;
60let mi = a[MI].parse().ok()?;
61let ss = a[SS].parse().ok()?;
62let ms = a[MS].parse().ok()?;
63let date = NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
64let time = NaiveTime::from_hms_milli_opt(hh, mi, ss, ms)?;
65Some(NaiveDateTime::new(date, time))
66}
6768#[cfg(test)]
69mod tests {
70use super::*;
7172#[mz_ore::test]
73fn monotonic_iso8601() {
74// The entire point of this method is that the lexicographic order
75 // corresponds to chronological order (ignoring None/NULL). So, verify.
76let mut inputs = vec![
77"0000-01-01T00:00:00.000Z",
78"0001-01-01T00:00:00.000Z",
79"2015-00-00T00:00:00.000Z",
80"2015-09-00T00:00:00.000Z",
81"2015-09-18T00:00:00.000Z",
82"2015-09-18T23:00:00.000Z",
83"2015-09-18T23:56:00.000Z",
84"2015-09-18T23:56:04.000Z",
85"2015-09-18T23:56:04.123Z",
86"2015-09-18T23:56:04.1234Z",
87"2015-09-18T23:56:04.124Z",
88"2015-09-18T23:56:05.000Z",
89"2015-09-18T23:57:00.000Z",
90"2015-09-18T24:00:00.000Z",
91"2015-09-19T00:00:00.000Z",
92"2015-10-00T00:00:00.000Z",
93"2016-10-00T00:00:00.000Z",
94"9999-12-31T23:59:59.999Z",
95 ];
96// Sort the inputs so we can't accidentally pass by hardcoding them in
97 // the wrong order.
98inputs.sort();
99let outputs = inputs
100 .into_iter()
101 .flat_map(try_parse_monotonic_iso8601_timestamp)
102 .collect::<Vec<_>>();
103// Sanity check that we don't trivially pass by always returning None.
104assert!(!outputs.is_empty());
105let mut outputs_sorted = outputs.clone();
106 outputs_sorted.sort();
107assert_eq!(outputs, outputs_sorted);
108 }
109}