Skip to main content

tp_lib_core/
temporal.rs

1//! Temporal utilities for timezone handling
2
3use crate::errors::ProjectionError;
4use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone};
5
6/// Parse a timestamp accepting either RFC3339 (with timezone) or a naive
7/// ISO 8601 datetime without timezone. Naive datetimes are interpreted as
8/// the host's local timezone and returned with that offset attached, so
9/// downstream code always works on `DateTime<FixedOffset>` with explicit
10/// timezone information.
11pub fn parse_timestamp_flexible(s: &str) -> Result<DateTime<FixedOffset>, ProjectionError> {
12    parse_timestamp_flexible_str(s).map_err(ProjectionError::InvalidTimestamp)
13}
14
15/// Same as [`parse_timestamp_flexible`] but returns the raw error string so
16/// callers using their own error types (e.g. `DetectionError`) can wrap it.
17pub fn parse_timestamp_flexible_str(s: &str) -> Result<DateTime<FixedOffset>, String> {
18    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
19        return Ok(dt);
20    }
21    let naive = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f")
22        .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"))
23        .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f"))
24        .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S"))
25        .map_err(|e| {
26            format!(
27                "{} (expected RFC3339 with timezone, e.g. 2025-12-09T14:30:00+01:00, \
28                 or ISO 8601 without timezone interpreted as local time)",
29                e
30            )
31        })?;
32    Local
33        .from_local_datetime(&naive)
34        .single()
35        .map(|dt| dt.fixed_offset())
36        .ok_or_else(|| format!("ambiguous or non-existent local time: '{}'", s))
37}
38
39/// Parse RFC3339 timestamp with strict timezone validation.
40///
41/// Kept for callers that explicitly require timezone-bearing input. Prefer
42/// [`parse_timestamp_flexible`] for user-facing inputs (CSV/GeoJSON files,
43/// detection feeds, etc.) where a naive datetime should be accepted.
44pub fn parse_rfc3339_with_timezone(s: &str) -> Result<DateTime<FixedOffset>, ProjectionError> {
45    DateTime::parse_from_rfc3339(s)
46        .map_err(|e| ProjectionError::MissingTimezone(format!("Invalid timestamp: {}", e)))
47}
48
49/// Validate that timezone information is present
50pub fn validate_timezone_present(_dt: &DateTime<FixedOffset>) -> Result<(), ProjectionError> {
51    // DateTime<FixedOffset> always has timezone, this is a type-level guarantee
52    // This function exists for API consistency
53    Ok(())
54}
55
56#[cfg(test)]
57mod tests;