From c47ce6870a45da5de0c60ba123ed6a149d6f5e0a Mon Sep 17 00:00:00 2001 From: yuankunzhang Date: Wed, 23 Jul 2025 14:49:41 +0000 Subject: [PATCH] fix: support float timestamp values --- src/items/builder.rs | 26 ++++++++++++++++++++------ src/items/epoch.rs | 30 +++++++++++++++++++++++++++--- src/items/mod.rs | 2 +- src/items/primitive.rs | 1 + 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/items/builder.rs b/src/items/builder.rs index 02c8974..2d2db7f 100644 --- a/src/items/builder.rs +++ b/src/items/builder.rs @@ -13,7 +13,7 @@ use super::{date, relative, time, weekday}; #[derive(Debug, Default)] pub struct DateTimeBuilder { base: Option>, - timestamp: Option, + timestamp: Option, date: Option, time: Option, weekday: Option, @@ -35,7 +35,7 @@ impl DateTimeBuilder { /// Timestamp value is exclusive to other date/time components. Caller of /// the builder must ensure that it is not combined with other items. - pub(super) fn set_timestamp(mut self, ts: i32) -> Result { + pub(super) fn set_timestamp(mut self, ts: f64) -> Result { self.timestamp = Some(ts); Ok(self) } @@ -117,10 +117,24 @@ impl DateTimeBuilder { )?; if let Some(ts) = self.timestamp { - dt = chrono::Utc - .timestamp_opt(ts.into(), 0) - .unwrap() - .with_timezone(&dt.timezone()); + // TODO: How to make the fract -> nanosecond conversion more precise? + // Maybe considering using the + // [rust_decimal](https://crates.io/crates/rust_decimal) crate? + match chrono::Utc.timestamp_opt(ts as i64, (ts.fract() * 10f64.powi(9)).round() as u32) + { + chrono::MappedLocalTime::Single(t) => { + // If the timestamp is valid, we can use it directly. + dt = t.with_timezone(&dt.timezone()); + } + chrono::MappedLocalTime::Ambiguous(earliest, _latest) => { + // TODO: When there is a fold in the local time, which value + // do we choose? For now, we use the earliest one. + dt = earliest.with_timezone(&dt.timezone()); + } + chrono::MappedLocalTime::None => { + return None; // Invalid timestamp + } + } } if let Some(date::Date { year, month, day }) = self.date { diff --git a/src/items/epoch.rs b/src/items/epoch.rs index 67dd1a7..5cde089 100644 --- a/src/items/epoch.rs +++ b/src/items/epoch.rs @@ -3,9 +3,33 @@ use winnow::{combinator::preceded, ModalResult, Parser}; -use super::primitive::{dec_int, s}; +use super::primitive::{float, s}; /// Parse a timestamp in the form of `@1234567890`. -pub fn parse(input: &mut &str) -> ModalResult { - s(preceded("@", dec_int)).parse_next(input) +pub fn parse(input: &mut &str) -> ModalResult { + s(preceded("@", float)).parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::parse; + + fn float_eq(a: f64, b: f64) -> bool { + (a - b).abs() < f64::EPSILON + } + + #[test] + fn float() { + let mut input = "@1234567890"; + assert!(float_eq(parse(&mut input).unwrap(), 1234567890.0)); + + let mut input = "@1234567890.12345"; + assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345)); + + let mut input = "@1234567890,12345"; + assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345)); + + let mut input = "@-1234567890.12345"; + assert_eq!(parse(&mut input).unwrap(), -1234567890.12345); + } } diff --git a/src/items/mod.rs b/src/items/mod.rs index cd1da36..4259664 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -58,7 +58,7 @@ use crate::ParseDateTimeError; #[derive(PartialEq, Debug)] pub(crate) enum Item { - Timestamp(i32), + Timestamp(f64), Year(u32), DateTime(combined::DateTime), Date(date::Date), diff --git a/src/items/primitive.rs b/src/items/primitive.rs index 0f54359..cd8bd4d 100644 --- a/src/items/primitive.rs +++ b/src/items/primitive.rs @@ -84,6 +84,7 @@ where /// /// Inputs like [+-]?0[0-9]* (e.g., `+012`) are therefore rejected. We provide a /// custom implementation to support such zero-prefixed integers. +#[allow(unused)] pub(super) fn dec_int<'a, E>(input: &mut &'a str) -> winnow::Result where E: ParserError<&'a str>,