From a799ecf037cc802debba519feeb3e2a6b4550d45 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 2 Mar 2024 08:28:52 +0100 Subject: [PATCH] Support parsing negative timestamps --- src/format/parse.rs | 6 +++--- src/format/scan.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/format/parse.rs b/src/format/parse.rs index 097e02d81a..7dd301b4d9 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -357,7 +357,7 @@ where Minute => (2, false, Parsed::set_minute), Second => (2, false, Parsed::set_second), Nanosecond => (9, false, Parsed::set_nanosecond), - Timestamp => (usize::MAX, false, Parsed::set_timestamp), + Timestamp => (usize::MAX, true, Parsed::set_timestamp), // for the future expansion Internal(ref int) => match int._dummy {}, @@ -366,8 +366,7 @@ where s = s.trim_start(); let v = if signed { if s.starts_with('-') { - let v = try_consume!(scan::number(&s[1..], 1, usize::MAX)); - 0i64.checked_sub(v).ok_or(OUT_OF_RANGE)? + try_consume!(scan::negative_number(&s[1..], 1, usize::MAX)) } else if s.starts_with('+') { try_consume!(scan::number(&s[1..], 1, usize::MAX)) } else { @@ -765,6 +764,7 @@ mod tests { check(" + 42", &[Space(" "), num(Year)], Err(INVALID)); check("-", &[num(Year)], Err(TOO_SHORT)); check("+", &[num(Year)], Err(TOO_SHORT)); + check("-9223372036854775808", &[num(Timestamp)], parsed!(timestamp: i64::MIN)); // unsigned numeric check("345", &[num(Ordinal)], parsed!(ordinal: 345)); diff --git a/src/format/scan.rs b/src/format/scan.rs index 5a7c061ad0..051b2ea225 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -15,6 +15,28 @@ use crate::Weekday; /// Any number that does not fit in `i64` is an error. #[inline] pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)> { + let (s, n) = unsigned_number(s, min, max)?; + Ok((s, n.try_into().map_err(|_| OUT_OF_RANGE)?)) +} + +/// Tries to parse the negative number from `min` to `max` digits. +/// +/// The absence of digits at all is an unconditional error. +/// More than `max` digits are consumed up to the first `max` digits. +/// Any number that does not fit in `i64` is an error. +#[inline] +pub(super) fn negative_number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)> { + let (s, n) = unsigned_number(s, min, max)?; + let signed_neg = (n as i64).wrapping_neg(); + if !signed_neg.is_negative() { + return Err(OUT_OF_RANGE); + } + Ok((s, signed_neg)) +} + +/// Tries to parse a number from `min` to `max` digits as an unsigned integer. +#[inline] +pub(super) fn unsigned_number(s: &str, min: usize, max: usize) -> ParseResult<(&str, u64)> { assert!(min <= max); // We are only interested in ascii numbers, so we can work with the `str` as bytes. We stop on @@ -25,7 +47,7 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64) return Err(TOO_SHORT); } - let mut n = 0i64; + let mut n = 0u64; for (i, c) in bytes.iter().take(max).cloned().enumerate() { // cloned() = copied() if !c.is_ascii_digit() { @@ -36,7 +58,7 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64) } } - n = match n.checked_mul(10).and_then(|n| n.checked_add((c - b'0') as i64)) { + n = match n.checked_mul(10).and_then(|n| n.checked_add((c - b'0') as u64)) { Some(n) => n, None => return Err(OUT_OF_RANGE), };