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;
#[derive(Copy, Clone, Debug)]
pub enum DatePatternContext {
Local(DateTime<Local>),
Fixed(DateTime<FixedOffset>),
}
impl DatePatternContext {
pub fn parse_relative(
&self,
s: &str,
kind: &str,
) -> Result<DatePattern, DatePatternParseError> {
match *self {
Self::Local(dt) => DatePattern::from_str_kind(s, kind, dt),
Self::Fixed(dt) => DatePattern::from_str_kind(s, kind, dt),
}
}
}
impl From<DateTime<Local>> for DatePatternContext {
fn from(value: DateTime<Local>) -> Self {
Self::Local(value)
}
}
impl From<DateTime<FixedOffset>> for DatePatternContext {
fn from(value: DateTime<FixedOffset>) -> Self {
Self::Fixed(value)
}
}
#[derive(Debug, Error)]
pub enum DatePatternParseError {
#[error("Invalid date pattern kind `{0}:`")]
InvalidKind(String),
#[error(transparent)]
ParseError(#[from] DateError),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum DatePattern {
AtOrAfter(MillisSinceEpoch),
Before(MillisSinceEpoch),
}
impl DatePattern {
pub fn from_str_kind<Tz: TimeZone>(
s: &str,
kind: &str,
now: DateTime<Tz>,
) -> Result<Self, DatePatternParseError>
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())),
}
}
pub fn matches(&self, timestamp: &Timestamp) -> bool {
match self {
Self::AtOrAfter(earliest) => *earliest <= timestamp.timestamp,
Self::Before(latest) => timestamp.timestamp < *latest,
}
}
}
pub fn parse_datetime(s: &str) -> chrono::ParseResult<Timestamp> {
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<Tz: TimeZone>(now: DateTime<Tz>, 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() {
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);
}
}