// Copyright 2024 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Provides support for parsing and matching date ranges. use chrono::DateTime; use chrono::FixedOffset; use chrono::Local; use chrono::TimeZone; use interim::DateError; use interim::Dialect; use interim::parse_date_string; use thiserror::Error; use crate::backend::MillisSinceEpoch; use crate::backend::Timestamp; /// Context needed to create a DatePattern during revset evaluation. #[derive(Copy, Clone, Debug)] pub enum DatePatternContext { /// Interpret date patterns using the local machine's time zone Local(DateTime), /// Interpret date patterns using any FixedOffset time zone Fixed(DateTime), } impl DatePatternContext { /// Parses a DatePattern from the given string and kind. pub fn parse_relative( &self, s: &str, kind: &str, ) -> Result { match *self { Self::Local(dt) => DatePattern::from_str_kind(s, kind, dt), Self::Fixed(dt) => DatePattern::from_str_kind(s, kind, dt), } } } impl From> for DatePatternContext { fn from(value: DateTime) -> Self { Self::Local(value) } } impl From> for DatePatternContext { fn from(value: DateTime) -> Self { Self::Fixed(value) } } /// Error occurred during date pattern parsing. #[derive(Debug, Error)] pub enum DatePatternParseError { /// Unknown pattern kind is specified. #[error("Invalid date pattern kind `{0}:`")] InvalidKind(String), /// Failed to parse timestamp. #[error(transparent)] ParseError(#[from] DateError), } /// Represents an range of dates that may be matched against. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum DatePattern { /// Represents all dates at or after the given instant. AtOrAfter(MillisSinceEpoch), /// Represents all dates before, but not including, the given instant. Before(MillisSinceEpoch), } impl DatePattern { /// Parses a string into a DatePattern. /// /// * `s` is the string to be parsed. /// /// * `kind` must be either "after" or "before". This determines whether the /// pattern will match dates after or before the parsed date. /// /// * `now` is the user's current time. This is a [`DateTime`] because /// knowledge of offset changes is needed to correctly process relative /// times like "today". For example, California entered DST on March 10, /// 2024, shifting clocks from UTC-8 to UTC-7 at 2:00 AM. If the pattern /// "today" was parsed at noon on that day, it should be interpreted as /// 2024-03-10T00:00:00-08:00 even though the current offset is -07:00. pub fn from_str_kind( s: &str, kind: &str, now: DateTime, ) -> Result where Tz::Offset: Copy, { let d = parse_date_string(s, now, Dialect::Us).map_err(DatePatternParseError::ParseError)?; let millis_since_epoch = MillisSinceEpoch(d.timestamp_millis()); match kind { "after" => Ok(Self::AtOrAfter(millis_since_epoch)), "before" => Ok(Self::Before(millis_since_epoch)), kind => Err(DatePatternParseError::InvalidKind(kind.to_owned())), } } /// Determines whether a given timestamp is matched by the pattern. pub fn matches(&self, timestamp: &Timestamp) -> bool { match self { Self::AtOrAfter(earliest) => *earliest <= timestamp.timestamp, Self::Before(latest) => timestamp.timestamp < *latest, } } } // @TODO ideally we would have this unified with the other parsing code. However // we use the interim crate which does not handle explicitly given time zone // information /// Parse a string with time zone information into a `Timestamp` pub fn parse_datetime(s: &str) -> chrono::ParseResult { Ok(Timestamp::from_datetime( DateTime::parse_from_rfc2822(s).or_else(|_| DateTime::parse_from_rfc3339(s))?, )) } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; fn test_equal(now: DateTime, expression: &str, should_equal_time: &str) where Tz::Offset: Copy, { let expression = DatePattern::from_str_kind(expression, "after", now).unwrap(); assert_eq!( expression, DatePattern::AtOrAfter(MillisSinceEpoch( DateTime::parse_from_rfc3339(should_equal_time) .unwrap() .timestamp_millis() )) ); } #[test] fn test_date_pattern_parses_dates_without_times_as_the_date_at_local_midnight() { let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap(); test_equal(now, "2023-03-25", "2023-03-25T08:00:00Z"); test_equal(now, "3/25/2023", "2023-03-25T08:00:00Z"); test_equal(now, "3/25/23", "2023-03-25T08:00:00Z"); } #[test] fn test_date_pattern_parses_dates_with_times_without_specifying_an_offset() { let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap(); test_equal(now, "2023-03-25T00:00:00", "2023-03-25T08:00:00Z"); test_equal(now, "2023-03-25 00:00:00", "2023-03-25T08:00:00Z"); } #[test] fn test_date_pattern_parses_dates_with_a_specified_offset() { let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap(); test_equal( now, "2023-03-25T00:00:00-05:00", "2023-03-25T00:00:00-05:00", ); } #[test] fn test_date_pattern_parses_dates_with_the_z_offset() { let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap(); test_equal(now, "2023-03-25T00:00:00Z", "2023-03-25T00:00:00Z"); } #[test] fn test_date_pattern_parses_relative_durations() { let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap(); test_equal(now, "2 hours ago", "2024-01-01T06:00:00Z"); test_equal(now, "5 minutes", "2024-01-01T08:05:00Z"); test_equal(now, "1 week ago", "2023-12-25T08:00:00Z"); test_equal(now, "yesterday", "2023-12-31T08:00:00Z"); test_equal(now, "tomorrow", "2024-01-02T08:00:00Z"); } #[test] fn test_date_pattern_parses_relative_dates_with_times() { let now = DateTime::parse_from_rfc3339("2024-01-01T08:00:00-08:00").unwrap(); test_equal(now, "yesterday 5pm", "2024-01-01T01:00:00Z"); test_equal(now, "yesterday 10am", "2023-12-31T18:00:00Z"); test_equal(now, "yesterday 10:30", "2023-12-31T18:30:00Z"); } #[test] fn test_parse_datetime_non_sense_yields_error() { use chrono::format::ParseErrorKind; assert_matches!( parse_datetime("aaaaa").unwrap_err().kind(), ParseErrorKind::Invalid | ParseErrorKind::TooShort | ParseErrorKind::TooLong ); } #[test] fn test_parse_datetime_human_readable() { // this is the example given in the help text for `jj metaedit // --author-timestamp` let timestamp = parse_datetime("2000-01-23T01:23:45-08:00").unwrap(); let human_readable = parse_datetime("Sun, 23 Jan 2000 01:23:45 PST").unwrap(); let human_readable_explicit = parse_datetime("Sun, 23 Jan 2000 01:23:45 -0800").unwrap(); assert_eq!(timestamp, human_readable); assert_eq!(timestamp, human_readable_explicit); } }