hamelin_lib 0.19.0

Core library for Hamelin query language
Documentation
//! Shared runtime cast semantics consumed by both eval and DataFusion.
//!
//! `hamelin_lib` already owns the static description of casts (see
//! `tree::typed_ast::expression::CastKind`). This module owns the small
//! pieces of *runtime* behavior that need to match across backends — placing
//! them here means `hamelin_eval` and `hamelin_datafusion` can each depend on
//! one canonical implementation without depending on each other.

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

/// Truncate an f64 to i64 matching native `CastKind::DoubleToInt`:
/// NaN / ±Inf / out-of-range values produce `None`; finite values truncate
/// toward zero.
///
/// Marked `#[inline]` so the body is available cross-crate and gets inlined
/// into hot eval and DataFusion variant-cast paths without LTO.
#[inline]
pub fn f64_to_i64(f: f64) -> Option<i64> {
    if !f.is_finite() {
        return None;
    }
    let truncated = f.trunc();
    if truncated < i64::MIN as f64 || truncated > i64::MAX as f64 {
        return None;
    }
    Some(truncated as i64)
}

/// Parse a string as a boolean using the same acceptance set as Arrow's
/// `cast_string_to_boolean` kernel (arrow-cast/src/cast/string.rs). Case
/// insensitive with leading/trailing whitespace trimmed; accepts
/// `true`/`false`/`yes`/`no`/`on`/`off`/`1`/`0` and their prefixes.
///
/// Trino's native `TRY_CAST(JSON AS BOOLEAN)` is narrower (only
/// `true`/`false`/`1`/`0`/`t`/`f`), so values like `"yes"`/`"on"` produce
/// `true` on eval/DataFusion but `null` on Trino — that's a backend-level
/// divergence outside this function's contract.
///
/// `#[inline]` so the per-row eval cast path can inline across the crate
/// boundary without LTO.
#[inline]
pub fn parse_string_as_bool(s: &str) -> Option<bool> {
    match s.to_ascii_lowercase().trim() {
        "t" | "tr" | "tru" | "true" | "y" | "ye" | "yes" | "on" | "1" => Some(true),
        "f" | "fa" | "fal" | "fals" | "false" | "n" | "no" | "of" | "off" | "0" => Some(false),
        _ => None,
    }
}

/// Parse a timestamp string into a UTC `DateTime`, accepting the format set
/// shared between eval (`cast_string_to_timestamp`) and the DataFusion
/// `parse_timestamp_micros` UDF: RFC3339 first, then several timezone-aware
/// and naive formats, then date-only fallbacks.
///
/// Returns `None` on parse failure; both backends decide separately whether
/// that becomes a runtime error (native `string AS timestamp`) or a null
/// (variant-string → timestamp).
///
/// `#[inline]` so the per-row eval cast path can inline across the crate
/// boundary without LTO. The body is large but every call site already
/// chains chrono's monomorphic parse functions; inlining here avoids an
/// extra cross-crate call frame on the hot path.
#[inline]
pub fn parse_timestamp_to_utc(s: &str) -> Option<DateTime<Utc>> {
    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
        return Some(dt.with_timezone(&Utc));
    }

    for fmt in &["%Y-%m-%d %H:%M:%S%z", "%Y-%m-%dT%H:%M:%S%z"] {
        if let Ok(dt) = DateTime::parse_from_str(s, fmt) {
            return Some(dt.with_timezone(&Utc));
        }
    }

    for fmt in &[
        "%Y-%m-%d %H:%M:%S",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%d %H:%M:%S%.f",
        "%Y-%m-%dT%H:%M:%S%.f",
        "%Y/%m/%d %H:%M:%S",
    ] {
        if let Ok(naive) = NaiveDateTime::parse_from_str(s, fmt) {
            return Some(Utc.from_utc_datetime(&naive));
        }
    }

    for fmt in &["%Y-%m-%d", "%Y/%m/%d"] {
        if let Ok(date) = NaiveDate::parse_from_str(s, fmt) {
            if let Some(naive) = date.and_hms_opt(0, 0, 0) {
                return Some(Utc.from_utc_datetime(&naive));
            }
        }
    }

    None
}