tardis-cli 0.2.0

TARDIS - Translates natural language dates into machine-readable formats
Documentation
//! Custom natural-language date parser for **TARDIS**.
//!
//! Pipeline: input string -> lexer (tokens) -> grammar (AST) -> resolver (Zoned).
//! Public submodules: [`ast`], [`token`], [`error`] (for library consumers).
//! Internal submodules: `grammar`, `lexer`, `resolver`, `suggest`.

pub mod ast;
pub mod error;
pub(crate) mod grammar;
pub(crate) mod lexer;
pub(crate) mod resolver;
pub(crate) mod suggest;
pub mod token;

pub use error::ParseError;

/// Maximum input length in bytes. Inputs longer than this are rejected
/// before tokenization to prevent abuse.
const MAX_INPUT_LEN: usize = 1024;

/// Parse a natural-language date expression into a [`jiff::Zoned`] datetime.
///
/// * `input` -- the raw expression (e.g. `"next friday"`, `"@1735689600"`, `"in 3 days"`)
/// * `now` -- reference "now" for relative resolution
///
/// Returns the resolved datetime or a [`ParseError`] with span-based diagnostics.
#[must_use = "parse returns a Result that should not be discarded"]
pub fn parse(input: &str, now: &jiff::Zoned) -> std::result::Result<jiff::Zoned, ParseError> {
    if input.len() > MAX_INPUT_LEN {
        return Err(ParseError::input_too_long(input.len(), MAX_INPUT_LEN));
    }

    let trimmed = input.trim();
    if trimmed.is_empty() {
        return resolver::resolve(&ast::DateExpr::Now, now);
    }

    if let Ok(ts) = trimmed.parse::<jiff::Timestamp>() {
        return Ok(ts.to_zoned(now.time_zone().clone()));
    }

    let tokens = lexer::tokenize(trimmed);
    let mut parser = grammar::Parser::new(&tokens, trimmed);
    let expr = parser.parse_expression()?;
    resolver::resolve(&expr, now)
}

/// Parse any expression and resolve it as a range with implicit granularity.
///
/// This is the API used by the `td range` subcommand. It accepts any expression
/// type (not just Range variants) and applies granularity expansion based on
/// the smallest unspecified time unit:
///
/// - `"tomorrow"` -> day granularity (00:00:00..23:59:59)
/// - `"tomorrow at 18h"` -> hour granularity (18:00:00..18:59:59)
/// - `"tomorrow at 18:30"` -> minute granularity (18:30:00..18:30:59)
/// - `"now"` -> instant (now..now)
/// - `"this week"` -> week range (Monday..Sunday)
#[must_use = "parse_range_with_granularity returns a Result that should not be discarded"]
pub fn parse_range_with_granularity(
    input: &str,
    now: &jiff::Zoned,
) -> std::result::Result<(jiff::Zoned, jiff::Zoned), ParseError> {
    if input.len() > MAX_INPUT_LEN {
        return Err(ParseError::input_too_long(input.len(), MAX_INPUT_LEN));
    }

    let trimmed = input.trim();
    if trimmed.is_empty() {
        let z = now.clone();
        return Ok((z.clone(), z));
    }

    if let Ok(ts) = trimmed.parse::<jiff::Timestamp>() {
        let z = ts.to_zoned(now.time_zone().clone());
        return Ok((z.clone(), z));
    }

    let tokens = lexer::tokenize(trimmed);
    let mut parser = grammar::Parser::new(&tokens, trimmed);
    let expr = parser.parse_expression()?;
    resolver::resolve_range_with_granularity(&expr, now)
}