Merge pull request #189 from yuankunzhang/support-float-timestamp

fix: support float timestamp values
This commit is contained in:
Daniel Hofstetter
2025-07-24 10:03:56 +02:00
committed by GitHub
4 changed files with 49 additions and 10 deletions
+20 -6
View File
@@ -13,7 +13,7 @@ use super::{date, relative, time, weekday};
#[derive(Debug, Default)]
pub struct DateTimeBuilder {
base: Option<DateTime<FixedOffset>>,
timestamp: Option<i32>,
timestamp: Option<f64>,
date: Option<date::Date>,
time: Option<time::Time>,
weekday: Option<weekday::Weekday>,
@@ -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<Self, &'static str> {
pub(super) fn set_timestamp(mut self, ts: f64) -> Result<Self, &'static str> {
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 {
+27 -3
View File
@@ -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<i32> {
s(preceded("@", dec_int)).parse_next(input)
pub fn parse(input: &mut &str) -> ModalResult<f64> {
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);
}
}
+1 -1
View File
@@ -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),
+1
View File
@@ -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<i32, E>
where
E: ParserError<&'a str>,