Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions src/items/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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>,
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -210,6 +210,7 @@ impl DateTimeBuilder {
hour,
minute,
second,
nanosecond,
ref offset,
}) = self.time
{
Expand All @@ -224,8 +225,8 @@ impl DateTimeBuilder {
dt.day(),
hour,
minute,
second as u32,
(second.fract() * 10f64.powi(9)).round() as u32,
second,
nanosecond,
offset,
)?;
}
Expand Down
3 changes: 2 additions & 1 deletion src/items/combined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ mod tests {
time: Time {
hour: 10,
minute: 10,
second: 55.0,
second: 55,
nanosecond: 0,
offset: None,
},
});
Expand Down
138 changes: 125 additions & 13 deletions src/items/epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

#[test]
fn sec_and_nsec_test() {
let mut input = "1234567890";
assert_eq!(sec_and_nsec(&mut input).unwrap(), (1234567890, 0));

fn float_eq(a: f64, b: f64) -> bool {
(a - b).abs() < f64::EPSILON
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,
}
);
}
}
2 changes: 1 addition & 1 deletion src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 5 additions & 2 deletions src/items/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading