bql 0.1.0

BQL — Bridge Query Language parser and formatter (Rust, generated).
Documentation
//! Generated chrono integration (Rust). Do not edit by hand.
//!
//! Feature-gated (`chrono`) typed date/datetime constructors and accessors
//! on the value type. Rust-only, with no cross-language analogue.

use std::borrow::Cow;

use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};

use crate::ast::Value;
use crate::error::{BqlError, BqlResult};

impl Value<'static> {
    /// Build a `Value::Date` from a `NaiveDate`, rendered as
    /// the canonical `YYYY-MM-DD` BQL date lexeme.
    pub fn date(d: NaiveDate) -> Self {
        Value::Date(Cow::Owned(d.format("%Y-%m-%d").to_string()))
    }
    /// Build a `Value::DateTime` from a UTC `DateTime`, rendered as
    /// an RFC 3339 lexeme with a `Z` offset, using `AutoSi` sub-second
    /// precision: 0, 3, 6, or 9 fractional digits as needed (e.g.
    /// `2024-01-31T10:30:00Z` or `2024-01-31T10:30:00.123Z`).
    pub fn date_time(dt: DateTime<Utc>) -> Self {
        Value::DateTime(Cow::Owned(
            dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true),
        ))
    }
}

impl Value<'_> {
    /// Parse this value's text as a calendar date (`YYYY-MM-DD`). Errors
    /// when the value is not a date variant or is not a valid calendar date.
    pub fn as_date(&self) -> BqlResult<NaiveDate> {
        match self {
            Value::Date(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")
                .map_err(|e| BqlError::new(0, format!("invalid date `{s}`: {e}"))),
            _ => Err(BqlError::new(0, "value is not a date")),
        }
    }
    /// Parse this value's text as a UTC `DateTime`. Accepts a `T`/`t`/space
    /// date-time separator; a zone-less timestamp is interpreted as UTC.
    /// Errors when the value is not a datetime variant or does not parse.
    pub fn as_date_time(&self) -> BqlResult<DateTime<Utc>> {
        match self {
            Value::DateTime(s) => parse_date_time(s),
            _ => Err(BqlError::new(0, "value is not a datetime")),
        }
    }
}

/// Parse a BQL datetime lexeme into a UTC `DateTime`. Normalises the
/// date/time separator to `T`, tries a zoned RFC 3339 timestamp first,
/// then falls back to a zone-less timestamp interpreted as UTC.
fn parse_date_time(s: &str) -> BqlResult<DateTime<Utc>> {
    let normalized = normalize_separator(s);
    if let Ok(dt) = DateTime::parse_from_rfc3339(&normalized) {
        return Ok(dt.with_timezone(&Utc));
    }
    for fmt in [
        "%Y-%m-%dT%H:%M:%S%.f",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%dT%H:%M",
    ] {
        if let Ok(naive) = NaiveDateTime::parse_from_str(&normalized, fmt) {
            return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
        }
    }
    Err(BqlError::new(0, format!("invalid datetime `{s}`")))
}

/// The datetime lexeme carries its date/time separator at byte 10
/// (`YYYY-MM-DD` is 10 bytes); normalise a `t`/space separator to `T`.
fn normalize_separator(s: &str) -> String {
    let mut out = s.to_string();
    if out.len() > 10
        && out.is_char_boundary(10)
        && out.is_char_boundary(11)
        && matches!(&out[10..11], "t" | " ")
    {
        out.replace_range(10..11, "T");
    }
    out
}