Skip to main content

tardis_cli/parser/
mod.rs

1//! Custom natural-language date parser for **TARDIS**.
2//!
3//! Pipeline: input string -> lexer (tokens) -> grammar (AST) -> resolver (Zoned).
4//! Public submodules: [`ast`], [`token`], [`error`] (for library consumers).
5//! Internal submodules: `grammar`, `lexer`, `resolver`, `suggest`.
6
7pub mod ast;
8pub mod error;
9pub(crate) mod grammar;
10pub(crate) mod lexer;
11pub(crate) mod resolver;
12pub(crate) mod suggest;
13pub mod token;
14
15pub use error::ParseError;
16
17/// Maximum input length in bytes. Inputs longer than this are rejected
18/// before tokenization to prevent abuse.
19const MAX_INPUT_LEN: usize = 1024;
20
21/// Parse a natural-language date expression into a [`jiff::Zoned`] datetime.
22///
23/// * `input` -- the raw expression (e.g. `"next friday"`, `"@1735689600"`, `"in 3 days"`)
24/// * `now` -- reference "now" for relative resolution
25///
26/// Returns the resolved datetime or a [`ParseError`] with span-based diagnostics.
27#[must_use = "parse returns a Result that should not be discarded"]
28pub fn parse(input: &str, now: &jiff::Zoned) -> std::result::Result<jiff::Zoned, ParseError> {
29    if input.len() > MAX_INPUT_LEN {
30        return Err(ParseError::input_too_long(input.len(), MAX_INPUT_LEN));
31    }
32
33    let trimmed = input.trim();
34    if trimmed.is_empty() {
35        return resolver::resolve(&ast::DateExpr::Now, now);
36    }
37
38    if let Ok(ts) = trimmed.parse::<jiff::Timestamp>() {
39        return Ok(ts.to_zoned(now.time_zone().clone()));
40    }
41
42    let tokens = lexer::tokenize(trimmed);
43    let mut parser = grammar::Parser::new(&tokens, trimmed);
44    let expr = parser.parse_expression()?;
45    resolver::resolve(&expr, now)
46}
47
48/// Parse any expression and resolve it as a range with implicit granularity.
49///
50/// This is the API used by the `td range` subcommand. It accepts any expression
51/// type (not just Range variants) and applies granularity expansion based on
52/// the smallest unspecified time unit:
53///
54/// - `"tomorrow"` -> day granularity (00:00:00..23:59:59)
55/// - `"tomorrow at 18h"` -> hour granularity (18:00:00..18:59:59)
56/// - `"tomorrow at 18:30"` -> minute granularity (18:30:00..18:30:59)
57/// - `"now"` -> instant (now..now)
58/// - `"this week"` -> week range (Monday..Sunday)
59#[must_use = "parse_range_with_granularity returns a Result that should not be discarded"]
60pub fn parse_range_with_granularity(
61    input: &str,
62    now: &jiff::Zoned,
63) -> std::result::Result<(jiff::Zoned, jiff::Zoned), ParseError> {
64    if input.len() > MAX_INPUT_LEN {
65        return Err(ParseError::input_too_long(input.len(), MAX_INPUT_LEN));
66    }
67
68    let trimmed = input.trim();
69    if trimmed.is_empty() {
70        let z = now.clone();
71        return Ok((z.clone(), z));
72    }
73
74    if let Ok(ts) = trimmed.parse::<jiff::Timestamp>() {
75        let z = ts.to_zoned(now.time_zone().clone());
76        return Ok((z.clone(), z));
77    }
78
79    let tokens = lexer::tokenize(trimmed);
80    let mut parser = grammar::Parser::new(&tokens, trimmed);
81    let expr = parser.parse_expression()?;
82    resolver::resolve_range_with_granularity(&expr, now)
83}