You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Merge pull request #205 from yuankunzhang/refactor-timestamp-parsing
refactor: replace floating-point seconds with precise integer representation
This commit is contained in:
+13
-12
@@ -3,7 +3,7 @@
|
||||
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
|
||||
|
||||
use super::{date, relative, time, timezone, weekday, year};
|
||||
use super::{date, epoch, relative, time, timezone, weekday, year};
|
||||
|
||||
/// The builder is used to construct a DateTime object from various components.
|
||||
/// The parser creates a `DateTimeBuilder` object with the parsed components,
|
||||
@@ -13,7 +13,7 @@ use super::{date, relative, time, timezone, weekday, year};
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct DateTimeBuilder {
|
||||
base: Option<DateTime<FixedOffset>>,
|
||||
timestamp: Option<f64>,
|
||||
timestamp: Option<epoch::Timestamp>,
|
||||
date: Option<date::Date>,
|
||||
time: Option<time::Time>,
|
||||
weekday: Option<weekday::Weekday>,
|
||||
@@ -35,7 +35,7 @@ impl DateTimeBuilder {
|
||||
|
||||
/// Sets a timestamp value. Timestamp values are exclusive to other date/time
|
||||
/// items (date, time, weekday, timezone, relative adjustments).
|
||||
pub(super) fn set_timestamp(mut self, ts: f64) -> Result<Self, &'static str> {
|
||||
pub(super) fn set_timestamp(mut self, ts: epoch::Timestamp) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot appear more than once");
|
||||
} else if self.date.is_some()
|
||||
@@ -148,15 +148,15 @@ impl DateTimeBuilder {
|
||||
self.set_time(time)
|
||||
}
|
||||
|
||||
fn build_from_timestamp(ts: f64, tz: &FixedOffset) -> Option<DateTime<FixedOffset>> {
|
||||
// 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) {
|
||||
fn build_from_timestamp(
|
||||
ts: epoch::Timestamp,
|
||||
tz: &FixedOffset,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
match chrono::Utc.timestamp_opt(ts.second, ts.nanosecond) {
|
||||
chrono::MappedLocalTime::Single(t) => Some(t.with_timezone(tz)),
|
||||
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.
|
||||
// When there is a fold in the local time, we use the earliest
|
||||
// one.
|
||||
Some(earliest.with_timezone(tz))
|
||||
}
|
||||
chrono::MappedLocalTime::None => None, // Invalid timestamp
|
||||
@@ -210,6 +210,7 @@ impl DateTimeBuilder {
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
nanosecond,
|
||||
ref offset,
|
||||
}) = self.time
|
||||
{
|
||||
@@ -224,8 +225,8 @@ impl DateTimeBuilder {
|
||||
dt.day(),
|
||||
hour,
|
||||
minute,
|
||||
second as u32,
|
||||
(second.fract() * 10f64.powi(9)).round() as u32,
|
||||
second,
|
||||
nanosecond,
|
||||
offset,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ mod tests {
|
||||
time: Time {
|
||||
hour: 10,
|
||||
minute: 10,
|
||||
second: 55.0,
|
||||
second: 55,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
+125
-13
@@ -15,35 +15,147 @@
|
||||
//! > ‘@1483228800’ represents 2017-01-01 00:00:00 UTC, and there is no way to
|
||||
//! > represent the intervening leap second 2016-12-31 23:59:60 UTC.
|
||||
|
||||
use winnow::{combinator::preceded, ModalResult, Parser};
|
||||
use winnow::{
|
||||
ascii::digit1,
|
||||
combinator::{opt, preceded},
|
||||
token::one_of,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::primitive::{float, s};
|
||||
use super::primitive::{dec_uint, s};
|
||||
|
||||
/// Parse a timestamp in the form of `@1234567890`.
|
||||
pub fn parse(input: &mut &str) -> ModalResult<f64> {
|
||||
s(preceded("@", float)).parse_next(input)
|
||||
/// Represents a timestamp with nanosecond accuracy.
|
||||
///
|
||||
/// # Invariants
|
||||
///
|
||||
/// - `nanosecond` is always in the range of `0..1_000_000_000`.
|
||||
/// - Negative timestamps are represented by a negative `second` value and a
|
||||
/// positive `nanosecond` value.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Timestamp {
|
||||
pub(crate) second: i64,
|
||||
pub(crate) nanosecond: u32,
|
||||
}
|
||||
|
||||
/// Parse a timestamp in the form of `1234567890` or `-1234567890.12345` or
|
||||
/// `1234567890,12345`.
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<Timestamp> {
|
||||
(s("@"), opt(s(one_of(['-', '+']))), sec_and_nsec)
|
||||
.verify_map(|(_, sign, (sec, nsec))| {
|
||||
let sec = i64::try_from(sec).ok()?;
|
||||
let (second, nanosecond) = match (sign, nsec) {
|
||||
(Some('-'), 0) => (-sec, 0),
|
||||
// Truncate towards minus infinity.
|
||||
(Some('-'), _) => ((-sec).checked_sub(1)?, 1_000_000_000 - nsec),
|
||||
_ => (sec, nsec),
|
||||
};
|
||||
Some(Timestamp { second, nanosecond })
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a second value in the form of `1234567890` or `1234567890.12345` or
|
||||
/// `1234567890,12345`.
|
||||
///
|
||||
/// The first part represents whole seconds. The optional second part represents
|
||||
/// fractional seconds, parsed as a nanosecond value from up to 9 digits
|
||||
/// (padded with zeros on the right if fewer digits are present). If the second
|
||||
/// part is omitted, it defaults to 0 nanoseconds.
|
||||
pub(super) fn sec_and_nsec(input: &mut &str) -> ModalResult<(u64, u32)> {
|
||||
(s(dec_uint), opt(preceded(one_of(['.', ',']), digit1)))
|
||||
.verify_map(|(sec, opt_nsec_str)| match opt_nsec_str {
|
||||
Some(nsec_str) if nsec_str.len() >= 9 => Some((sec, nsec_str[..9].parse().ok()?)),
|
||||
Some(nsec_str) => {
|
||||
let multiplier = 10_u32.pow(9 - nsec_str.len() as u32);
|
||||
Some((sec, nsec_str.parse::<u32>().ok()?.checked_mul(multiplier)?))
|
||||
}
|
||||
None => Some((sec, 0)),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
use super::*;
|
||||
|
||||
fn float_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < f64::EPSILON
|
||||
#[test]
|
||||
fn sec_and_nsec_test() {
|
||||
let mut input = "1234567890";
|
||||
assert_eq!(sec_and_nsec(&mut input).unwrap(), (1234567890, 0));
|
||||
|
||||
let mut input = "1234567890.12345";
|
||||
assert_eq!(sec_and_nsec(&mut input).unwrap(), (1234567890, 123450000));
|
||||
|
||||
let mut input = "1234567890,12345";
|
||||
assert_eq!(sec_and_nsec(&mut input).unwrap(), (1234567890, 123450000));
|
||||
|
||||
let mut input = "1234567890.1234567890123";
|
||||
assert_eq!(sec_and_nsec(&mut input).unwrap(), (1234567890, 123456789));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float() {
|
||||
fn timestamp() {
|
||||
let mut input = "@1234567890";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.0));
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: 1234567890,
|
||||
nanosecond: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let mut input = "@ 1234567890";
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: 1234567890,
|
||||
nanosecond: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let mut input = "@ -1234567890";
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: -1234567890,
|
||||
nanosecond: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let mut input = "@ - 1234567890";
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: -1234567890,
|
||||
nanosecond: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let mut input = "@1234567890.12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: 1234567890,
|
||||
nanosecond: 123450000,
|
||||
}
|
||||
);
|
||||
|
||||
let mut input = "@1234567890,12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: 1234567890,
|
||||
nanosecond: 123450000,
|
||||
}
|
||||
);
|
||||
|
||||
let mut input = "@-1234567890.12345";
|
||||
assert_eq!(parse(&mut input).unwrap(), -1234567890.12345);
|
||||
assert_eq!(
|
||||
parse(&mut input).unwrap(),
|
||||
Timestamp {
|
||||
second: -1234567891,
|
||||
nanosecond: 876550000,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ use crate::ParseDateTimeError;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum Item {
|
||||
Timestamp(f64),
|
||||
Timestamp(epoch::Timestamp),
|
||||
DateTime(combined::DateTime),
|
||||
Date(date::Date),
|
||||
Time(time::Time),
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
//! Primitive combinators.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use winnow::{
|
||||
ascii::{digit1, multispace0},
|
||||
ascii::{digit1, multispace0, Uint},
|
||||
combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated},
|
||||
error::{ContextError, ParserError, StrContext, StrContextValue},
|
||||
stream::AsChar,
|
||||
@@ -100,8 +102,9 @@ where
|
||||
///
|
||||
/// See the rationale for `dec_int` for why we don't use
|
||||
/// `winnow::ascii::dec_uint`.
|
||||
pub(super) fn dec_uint<'a, E>(input: &mut &'a str) -> winnow::Result<u32, E>
|
||||
pub(super) fn dec_uint<'a, O, E>(input: &mut &'a str) -> winnow::Result<O, E>
|
||||
where
|
||||
O: Uint + FromStr,
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
digit1
|
||||
|
||||
+80
-29
@@ -40,11 +40,12 @@
|
||||
use winnow::{
|
||||
combinator::{alt, opt, preceded},
|
||||
error::ErrMode,
|
||||
seq, ModalResult, Parser,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{
|
||||
primitive::{colon, ctx_err, dec_uint, float, s},
|
||||
epoch::sec_and_nsec,
|
||||
primitive::{colon, ctx_err, dec_uint, s},
|
||||
timezone::{timezone_num, Offset},
|
||||
};
|
||||
|
||||
@@ -52,7 +53,8 @@ use super::{
|
||||
pub(crate) struct Time {
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
pub second: f64,
|
||||
pub second: u32,
|
||||
pub nanosecond: u32,
|
||||
pub offset: Option<Offset>,
|
||||
}
|
||||
|
||||
@@ -74,16 +76,24 @@ pub(super) fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
(hour24, timezone_num).map(|(hour, offset)| Time {
|
||||
hour,
|
||||
minute: 0,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: Some(offset),
|
||||
}),
|
||||
seq!(Time {
|
||||
hour: hour24,
|
||||
_: colon,
|
||||
minute: minute,
|
||||
second: opt(preceded(colon, second)).map(|s| s.unwrap_or(0.0)),
|
||||
offset: opt(timezone_num)
|
||||
}),
|
||||
(
|
||||
hour24,
|
||||
colon,
|
||||
minute,
|
||||
opt(preceded(colon, second)),
|
||||
opt(timezone_num),
|
||||
)
|
||||
.map(|(hour, _, minute, sec_nsec, offset)| Time {
|
||||
hour,
|
||||
minute,
|
||||
second: sec_nsec.map_or(0, |(s, _)| s),
|
||||
nanosecond: sec_nsec.map_or(0, |(_, ns)| ns),
|
||||
offset,
|
||||
}),
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
@@ -92,7 +102,7 @@ pub(super) fn iso(input: &mut &str) -> ModalResult<Time> {
|
||||
///
|
||||
/// The hours are restricted to 12 or lower in this format
|
||||
fn am_pm_time(input: &mut &str) -> ModalResult<Time> {
|
||||
let (h, m, s, meridiem) = (
|
||||
let (h, m, sec_nsec, meridiem) = (
|
||||
hour12,
|
||||
opt(preceded(colon, minute)),
|
||||
opt(preceded(colon, second)),
|
||||
@@ -118,7 +128,8 @@ fn am_pm_time(input: &mut &str) -> ModalResult<Time> {
|
||||
Ok(Time {
|
||||
hour: h,
|
||||
minute: m.unwrap_or(0),
|
||||
second: s.unwrap_or(0.0),
|
||||
second: sec_nsec.map_or(0, |(s, _)| s),
|
||||
nanosecond: sec_nsec.map_or(0, |(_, ns)| ns),
|
||||
offset: None,
|
||||
})
|
||||
}
|
||||
@@ -139,14 +150,9 @@ pub(super) fn minute(input: &mut &str) -> ModalResult<u32> {
|
||||
}
|
||||
|
||||
/// Parse a number of seconds (preceded by whitespace)
|
||||
fn second(input: &mut &str) -> ModalResult<f64> {
|
||||
s(float)
|
||||
.verify(|x| *x < 60.0)
|
||||
.map(|x| {
|
||||
// Truncates the fractional part of seconds to 9 digits.
|
||||
let factor = 10f64.powi(9);
|
||||
(x * factor).trunc() / factor
|
||||
})
|
||||
fn second(input: &mut &str) -> ModalResult<(u32, u32)> {
|
||||
sec_and_nsec
|
||||
.verify_map(|(s, ns)| if s < 60 { Some((s as u32, ns)) } else { None })
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
@@ -159,7 +165,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 20,
|
||||
minute: 2,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
@@ -201,7 +208,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 11,
|
||||
minute: 0,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
@@ -223,12 +231,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nanoseconds() {
|
||||
let reference = Time {
|
||||
hour: 11,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
nanosecond: 123450000,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
for mut s in ["11:00:00.12345", "11:00:00.12345am"] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(
|
||||
parse(&mut s).ok(),
|
||||
Some(reference.clone()),
|
||||
"Format string: {old_s}"
|
||||
);
|
||||
}
|
||||
|
||||
let reference = Time {
|
||||
hour: 11,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
nanosecond: 123456789,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
for mut s in ["11:00:00.123456789", "11:00:00.1234567890123"] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(
|
||||
parse(&mut s).ok(),
|
||||
Some(reference.clone()),
|
||||
"Format string: {old_s}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noon() {
|
||||
let reference = Time {
|
||||
hour: 12,
|
||||
minute: 0,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
@@ -254,7 +300,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
@@ -273,7 +320,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 1,
|
||||
minute: 23,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: Some(Offset {
|
||||
negative: false,
|
||||
hours: 5,
|
||||
@@ -303,7 +351,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 3,
|
||||
minute: 45,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: Some(Offset {
|
||||
negative: false,
|
||||
hours: 5,
|
||||
@@ -335,7 +384,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 3,
|
||||
minute: 45,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: Some(Offset {
|
||||
negative: false,
|
||||
hours: 0,
|
||||
@@ -366,7 +416,8 @@ mod tests {
|
||||
let reference = Time {
|
||||
hour: 3,
|
||||
minute: 45,
|
||||
second: 0.0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: Some(Offset {
|
||||
negative: true,
|
||||
hours: 5,
|
||||
|
||||
Reference in New Issue
Block a user