Skip to main content

tardis_cli/parser/
ast.rs

1//! Abstract syntax tree for parsed date expressions.
2//!
3//! The AST separates syntax (what the user typed) from semantics (what datetime
4//! it resolves to). The resolver in `resolver.rs` maps these nodes to `jiff::Zoned`.
5
6use crate::parser::token::{BoundaryKind, EpochPrecision, TemporalUnit};
7
8/// Top-level AST node representing a parsed date expression.
9#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq)]
11pub enum DateExpr {
12    /// "now" or empty input
13    Now,
14    /// "today", "tomorrow", "yesterday", "overmorrow" with optional time
15    Relative(RelativeDate, Option<TimeExpr>),
16    /// "next/last/this friday" with optional time
17    DayRef(Direction, jiff::civil::Weekday, Option<TimeExpr>),
18    /// "2025-01-01", "24 March 2025" with optional time
19    Absolute(AbsoluteDate, Option<TimeExpr>),
20    /// "15:30" (time only, resolved against today)
21    TimeOnly(TimeExpr),
22    /// "@1735689600", "@1735689600ms"
23    Epoch(EpochValue),
24    /// "in 3 days", "3 hours ago"
25    Offset(Direction, Vec<DurationComponent>),
26    /// "3 hours ago from next friday"
27    OffsetFrom(Direction, Vec<DurationComponent>, Box<DateExpr>),
28
29    /// "tomorrow + 3 hours" -- compound arithmetic
30    Arithmetic(Box<DateExpr>, ArithOp, Vec<DurationComponent>),
31    /// "last week", "this month", "next year", "Q3 2025" -- period expressions
32    Range(RangeExpr),
33
34    /// Boundary keyword: `eod`, `sow`, etc.
35    Boundary(BoundaryKind),
36}
37
38/// Named relative date variants.
39#[non_exhaustive]
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum RelativeDate {
42    Today,
43    Tomorrow,
44    Yesterday,
45    Overmorrow,
46    Ereyesterday,
47}
48
49/// Direction for day references and duration offsets.
50#[non_exhaustive]
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Direction {
53    Next,
54    Last,
55    This,
56    Future,
57    Past,
58}
59
60/// A single duration component (e.g., "3 hours" -> count=3, unit=Hour).
61#[must_use]
62#[derive(Debug, Clone, PartialEq)]
63pub struct DurationComponent {
64    pub count: i64,
65    pub unit: TemporalUnit,
66}
67
68/// Time expression (hours:minutes or hours:minutes:seconds).
69#[non_exhaustive]
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum TimeExpr {
72    HourMinute(i8, i8),
73    HourMinuteSecond(i8, i8, i8),
74    /// Hour-only time specification for range granularity (e.g., "today 18h")
75    HourOnly(i8),
76    /// "at same time" -- preserve current time from `now` reference.
77    SameTime,
78}
79
80/// Absolute date components.
81#[must_use]
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct AbsoluteDate {
84    pub year: i16,
85    pub month: i8,
86    pub day: i8,
87}
88
89/// Epoch value with precision.
90#[must_use]
91#[derive(Debug, Clone, PartialEq)]
92pub struct EpochValue {
93    pub raw: i64,
94    pub precision: EpochPrecision,
95}
96
97/// Arithmetic operation for compound date expressions.
98#[non_exhaustive]
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum ArithOp {
101    Add,
102    Sub,
103}
104
105/// Range expression types for date range queries.
106#[non_exhaustive]
107#[derive(Debug, Clone, PartialEq)]
108pub enum RangeExpr {
109    LastWeek,
110    ThisWeek,
111    NextWeek,
112    LastMonth,
113    ThisMonth,
114    NextMonth,
115    LastYear,
116    ThisYear,
117    NextYear,
118    Quarter(i16, i8),
119}
120
121#[cfg(test)]
122mod tests {
123    #![allow(clippy::unwrap_used, clippy::expect_used)]
124    use super::*;
125
126    #[test]
127    fn date_expr_relative_with_time() {
128        let expr = DateExpr::Relative(RelativeDate::Tomorrow, Some(TimeExpr::HourMinute(15, 30)));
129        assert!(matches!(
130            expr,
131            DateExpr::Relative(RelativeDate::Tomorrow, Some(_))
132        ));
133    }
134
135    #[test]
136    fn duration_component_construction() {
137        let dc = DurationComponent {
138            count: 3,
139            unit: TemporalUnit::Hour,
140        };
141        assert_eq!(dc.count, 3);
142        assert_eq!(dc.unit, TemporalUnit::Hour);
143    }
144
145    #[test]
146    fn epoch_value_construction() {
147        let ev = EpochValue {
148            raw: 1735689600,
149            precision: EpochPrecision::Seconds,
150        };
151        assert_eq!(ev.raw, 1735689600);
152        assert_eq!(ev.precision, EpochPrecision::Seconds);
153    }
154
155    #[test]
156    fn boundary_expr() {
157        let expr = DateExpr::Boundary(BoundaryKind::Sod);
158        assert!(matches!(expr, DateExpr::Boundary(BoundaryKind::Sod)));
159    }
160
161    #[test]
162    fn hour_only_time_expr() {
163        let t = TimeExpr::HourOnly(18);
164        assert!(matches!(t, TimeExpr::HourOnly(18)));
165        assert_ne!(TimeExpr::HourOnly(18), TimeExpr::HourMinute(18, 0));
166    }
167
168    #[test]
169    fn extension_types_exist() {
170        let _ = DateExpr::Arithmetic(
171            Box::new(DateExpr::Now),
172            ArithOp::Add,
173            vec![DurationComponent {
174                count: 1,
175                unit: TemporalUnit::Day,
176            }],
177        );
178        let _ = DateExpr::Range(RangeExpr::LastWeek);
179    }
180}