jj_lib/
time_util.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Provides support for parsing and matching date ranges.
16
17use chrono::DateTime;
18use chrono::FixedOffset;
19use chrono::Local;
20use chrono::TimeZone;
21use interim::parse_date_string;
22use interim::DateError;
23use interim::Dialect;
24use thiserror::Error;
25
26use crate::backend::MillisSinceEpoch;
27use crate::backend::Timestamp;
28
29/// Context needed to create a DatePattern during revset evaluation.
30#[derive(Copy, Clone, Debug)]
31pub enum DatePatternContext {
32    /// Interpret date patterns using the local machine's time zone
33    Local(DateTime<Local>),
34    /// Interpret date patterns using any FixedOffset time zone
35    Fixed(DateTime<FixedOffset>),
36}
37
38impl DatePatternContext {
39    /// Parses a DatePattern from the given string and kind.
40    pub fn parse_relative(
41        &self,
42        s: &str,
43        kind: &str,
44    ) -> Result<DatePattern, DatePatternParseError> {
45        match *self {
46            DatePatternContext::Local(dt) => DatePattern::from_str_kind(s, kind, dt),
47            DatePatternContext::Fixed(dt) => DatePattern::from_str_kind(s, kind, dt),
48        }
49    }
50}
51
52impl From<DateTime<Local>> for DatePatternContext {
53    fn from(value: DateTime<Local>) -> Self {
54        DatePatternContext::Local(value)
55    }
56}
57
58impl From<DateTime<FixedOffset>> for DatePatternContext {
59    fn from(value: DateTime<FixedOffset>) -> Self {
60        DatePatternContext::Fixed(value)
61    }
62}
63
64/// Error occurred during date pattern parsing.
65#[derive(Debug, Error)]
66pub enum DatePatternParseError {
67    /// Unknown pattern kind is specified.
68    #[error("Invalid date pattern kind `{0}:`")]
69    InvalidKind(String),
70    /// Failed to parse timestamp.
71    #[error(transparent)]
72    ParseError(#[from] DateError),
73}
74
75/// Represents an range of dates that may be matched against.
76#[derive(Copy, Clone, Debug, Eq, PartialEq)]
77pub enum DatePattern {
78    /// Represents all dates at or after the given instant.
79    AtOrAfter(MillisSinceEpoch),
80    /// Represents all dates before, but not including, the given instant.
81    Before(MillisSinceEpoch),
82}
83
84impl DatePattern {
85    /// Parses a string into a DatePattern.
86    ///
87    /// * `s` is the string to be parsed.
88    ///
89    /// * `kind` must be either "after" or "before". This determines whether the
90    ///   pattern will match dates after or before the parsed date.
91    ///
92    /// * `now` is the user's current time. This is a [`DateTime<Tz>`] because
93    ///   knowledge of offset changes is needed to correctly process relative
94    ///   times like "today". For example, California entered DST on March 10,
95    ///   2024, shifting clocks from UTC-8 to UTC-7 at 2:00 AM. If the pattern
96    ///   "today" was parsed at noon on that day, it should be interpreted as
97    ///   2024-03-10T00:00:00-08:00 even though the current offset is -07:00.
98    pub fn from_str_kind<Tz: TimeZone>(
99        s: &str,
100        kind: &str,
101        now: DateTime<Tz>,
102    ) -> Result<DatePattern, DatePatternParseError>
103    where
104        Tz::Offset: Copy,
105    {
106        let d =
107            parse_date_string(s, now, Dialect::Us).map_err(DatePatternParseError::ParseError)?;
108        let millis_since_epoch = MillisSinceEpoch(d.timestamp_millis());
109        match kind {
110            "after" => Ok(DatePattern::AtOrAfter(millis_since_epoch)),
111            "before" => Ok(DatePattern::Before(millis_since_epoch)),
112            kind => Err(DatePatternParseError::InvalidKind(kind.to_owned())),
113        }
114    }
115
116    /// Determines whether a given timestamp is matched by the pattern.
117    pub fn matches(&self, timestamp: &Timestamp) -> bool {
118        match self {
119            DatePattern::AtOrAfter(earliest) => *earliest <= timestamp.timestamp,
120            DatePattern::Before(latest) => timestamp.timestamp < *latest,
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    fn test_equal<Tz: TimeZone>(now: DateTime<Tz>, expression: &str, should_equal_time: &str)
130    where
131        Tz::Offset: Copy,
132    {
133        let expression = DatePattern::from_str_kind(expression, "after", now).unwrap();
134        assert_eq!(
135            expression,
136            DatePattern::AtOrAfter(MillisSinceEpoch(
137                DateTime::parse_from_rfc3339(should_equal_time)
138                    .unwrap()
139                    .timestamp_millis()
140            ))
141        );
142    }
143
144    #[test]
145    fn test_date_pattern_parses_dates_without_times_as_the_date_at_local_midnight() {
146        let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
147        test_equal(now, "2023-03-25", "2023-03-25T08:00:00Z");
148        test_equal(now, "3/25/2023", "2023-03-25T08:00:00Z");
149        test_equal(now, "3/25/23", "2023-03-25T08:00:00Z");
150    }
151
152    #[test]
153    fn test_date_pattern_parses_dates_with_times_without_specifying_an_offset() {
154        let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
155        test_equal(now, "2023-03-25T00:00:00", "2023-03-25T08:00:00Z");
156        test_equal(now, "2023-03-25 00:00:00", "2023-03-25T08:00:00Z");
157    }
158
159    #[test]
160    fn test_date_pattern_parses_dates_with_a_specified_offset() {
161        let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
162        test_equal(
163            now,
164            "2023-03-25T00:00:00-05:00",
165            "2023-03-25T00:00:00-05:00",
166        );
167    }
168
169    #[test]
170    fn test_date_pattern_parses_dates_with_the_z_offset() {
171        let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
172        test_equal(now, "2023-03-25T00:00:00Z", "2023-03-25T00:00:00Z");
173    }
174
175    #[test]
176    fn test_date_pattern_parses_relative_durations() {
177        let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
178        test_equal(now, "2 hours ago", "2024-01-01T06:00:00Z");
179        test_equal(now, "5 minutes", "2024-01-01T08:05:00Z");
180        test_equal(now, "1 week ago", "2023-12-25T08:00:00Z");
181        test_equal(now, "yesterday", "2023-12-31T08:00:00Z");
182        test_equal(now, "tomorrow", "2024-01-02T08:00:00Z");
183    }
184
185    #[test]
186    fn test_date_pattern_parses_relative_dates_with_times() {
187        let now = DateTime::parse_from_rfc3339("2024-01-01T08:00:00-08:00").unwrap();
188        test_equal(now, "yesterday 5pm", "2024-01-01T01:00:00Z");
189        test_equal(now, "yesterday 10am", "2023-12-31T18:00:00Z");
190        test_equal(now, "yesterday 10:30", "2023-12-31T18:30:00Z");
191    }
192}