tardis-cli 0.2.0

TARDIS - Translates natural language dates into machine-readable formats
Documentation
//! Abstract syntax tree for parsed date expressions.
//!
//! The AST separates syntax (what the user typed) from semantics (what datetime
//! it resolves to). The resolver in `resolver.rs` maps these nodes to `jiff::Zoned`.

use crate::parser::token::{BoundaryKind, EpochPrecision, TemporalUnit};

/// Top-level AST node representing a parsed date expression.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum DateExpr {
    /// "now" or empty input
    Now,
    /// "today", "tomorrow", "yesterday", "overmorrow" with optional time
    Relative(RelativeDate, Option<TimeExpr>),
    /// "next/last/this friday" with optional time
    DayRef(Direction, jiff::civil::Weekday, Option<TimeExpr>),
    /// "2025-01-01", "24 March 2025" with optional time
    Absolute(AbsoluteDate, Option<TimeExpr>),
    /// "15:30" (time only, resolved against today)
    TimeOnly(TimeExpr),
    /// "@1735689600", "@1735689600ms"
    Epoch(EpochValue),
    /// "in 3 days", "3 hours ago"
    Offset(Direction, Vec<DurationComponent>),
    /// "3 hours ago from next friday"
    OffsetFrom(Direction, Vec<DurationComponent>, Box<DateExpr>),

    /// "tomorrow + 3 hours" -- compound arithmetic
    Arithmetic(Box<DateExpr>, ArithOp, Vec<DurationComponent>),
    /// "last week", "this month", "next year", "Q3 2025" -- period expressions
    Range(RangeExpr),

    /// Boundary keyword: `eod`, `sow`, etc.
    Boundary(BoundaryKind),
}

/// Named relative date variants.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelativeDate {
    Today,
    Tomorrow,
    Yesterday,
    Overmorrow,
    Ereyesterday,
}

/// Direction for day references and duration offsets.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
    Next,
    Last,
    This,
    Future,
    Past,
}

/// A single duration component (e.g., "3 hours" -> count=3, unit=Hour).
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct DurationComponent {
    pub count: i64,
    pub unit: TemporalUnit,
}

/// Time expression (hours:minutes or hours:minutes:seconds).
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeExpr {
    HourMinute(i8, i8),
    HourMinuteSecond(i8, i8, i8),
    /// Hour-only time specification for range granularity (e.g., "today 18h")
    HourOnly(i8),
    /// "at same time" -- preserve current time from `now` reference.
    SameTime,
}

/// Absolute date components.
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbsoluteDate {
    pub year: i16,
    pub month: i8,
    pub day: i8,
}

/// Epoch value with precision.
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct EpochValue {
    pub raw: i64,
    pub precision: EpochPrecision,
}

/// Arithmetic operation for compound date expressions.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithOp {
    Add,
    Sub,
}

/// Range expression types for date range queries.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum RangeExpr {
    LastWeek,
    ThisWeek,
    NextWeek,
    LastMonth,
    ThisMonth,
    NextMonth,
    LastYear,
    ThisYear,
    NextYear,
    Quarter(i16, i8),
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::expect_used)]
    use super::*;

    #[test]
    fn date_expr_relative_with_time() {
        let expr = DateExpr::Relative(RelativeDate::Tomorrow, Some(TimeExpr::HourMinute(15, 30)));
        assert!(matches!(
            expr,
            DateExpr::Relative(RelativeDate::Tomorrow, Some(_))
        ));
    }

    #[test]
    fn duration_component_construction() {
        let dc = DurationComponent {
            count: 3,
            unit: TemporalUnit::Hour,
        };
        assert_eq!(dc.count, 3);
        assert_eq!(dc.unit, TemporalUnit::Hour);
    }

    #[test]
    fn epoch_value_construction() {
        let ev = EpochValue {
            raw: 1735689600,
            precision: EpochPrecision::Seconds,
        };
        assert_eq!(ev.raw, 1735689600);
        assert_eq!(ev.precision, EpochPrecision::Seconds);
    }

    #[test]
    fn boundary_expr() {
        let expr = DateExpr::Boundary(BoundaryKind::Sod);
        assert!(matches!(expr, DateExpr::Boundary(BoundaryKind::Sod)));
    }

    #[test]
    fn hour_only_time_expr() {
        let t = TimeExpr::HourOnly(18);
        assert!(matches!(t, TimeExpr::HourOnly(18)));
        assert_ne!(TimeExpr::HourOnly(18), TimeExpr::HourMinute(18, 0));
    }

    #[test]
    fn extension_types_exist() {
        let _ = DateExpr::Arithmetic(
            Box::new(DateExpr::Now),
            ArithOp::Add,
            vec![DurationComponent {
                count: 1,
                unit: TemporalUnit::Day,
            }],
        );
        let _ = DateExpr::Range(RangeExpr::LastWeek);
    }
}