Skip to content

Commit

Permalink
Support parsing negative timestamps
Browse files Browse the repository at this point in the history
  • Loading branch information
pitdicker committed Mar 2, 2024
1 parent 752da7b commit 64a4bfd
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 21 deletions.
34 changes: 17 additions & 17 deletions src/format/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,14 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st
}

s = s.trim_start();
parsed.set_day(try_consume!(scan::number(s, 1, 2)))?;
parsed.set_day(try_consume!(scan::number(s, 1, 2, true)))?;
s = scan::space(s)?; // mandatory
parsed.set_month(1 + i64::from(try_consume!(scan::short_month0(s))))?;
s = scan::space(s)?; // mandatory

// distinguish two- and three-digit years from four-digit years
let prevlen = s.len();
let mut year = try_consume!(scan::number(s, 2, usize::MAX));
let mut year = try_consume!(scan::number(s, 2, usize::MAX, true));
let yearlen = prevlen - s.len();
match (yearlen, year) {
(2, 0..=49) => {
Expand All @@ -133,12 +133,12 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st
parsed.set_year(year)?;

s = scan::space(s)?; // mandatory
parsed.set_hour(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_hour(try_consume!(scan::number(s, 2, 2, true)))?;
s = scan::char(s.trim_start(), b':')?.trim_start(); // *S ":" *S
parsed.set_minute(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_minute(try_consume!(scan::number(s, 2, 2, true)))?;
if let Ok(s_) = scan::char(s.trim_start(), b':') {
// [ ":" *S 2DIGIT ]
parsed.set_second(try_consume!(scan::number(s_, 2, 2)))?;
parsed.set_second(try_consume!(scan::number(s_, 2, 2, true)))?;
}

s = scan::space(s)?; // mandatory
Expand Down Expand Up @@ -190,23 +190,23 @@ pub(crate) fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseRes
//
// - For readability a full-date and a full-time may be separated by a space character.

parsed.set_year(try_consume!(scan::number(s, 4, 4)))?;
parsed.set_year(try_consume!(scan::number(s, 4, 4, true)))?;
s = scan::char(s, b'-')?;
parsed.set_month(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_month(try_consume!(scan::number(s, 2, 2, true)))?;
s = scan::char(s, b'-')?;
parsed.set_day(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_day(try_consume!(scan::number(s, 2, 2, true)))?;

s = match s.as_bytes().first() {
Some(&b't' | &b'T' | &b' ') => &s[1..],
Some(_) => return Err(INVALID),
None => return Err(TOO_SHORT),
};

parsed.set_hour(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_hour(try_consume!(scan::number(s, 2, 2, true)))?;
s = scan::char(s, b':')?;
parsed.set_minute(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_minute(try_consume!(scan::number(s, 2, 2, true)))?;
s = scan::char(s, b':')?;
parsed.set_second(try_consume!(scan::number(s, 2, 2)))?;
parsed.set_second(try_consume!(scan::number(s, 2, 2, true)))?;
if s.starts_with('.') {
let nanosecond = try_consume!(scan::nanosecond(&s[1..]));
parsed.set_nanosecond(nanosecond)?;
Expand Down Expand Up @@ -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 {},
Expand All @@ -366,16 +366,15 @@ 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::number(&s[1..], 1, usize::MAX, false))
} else if s.starts_with('+') {
try_consume!(scan::number(&s[1..], 1, usize::MAX))
try_consume!(scan::number(&s[1..], 1, usize::MAX, true))
} else {
// if there is no explicit sign, we respect the original `width`
try_consume!(scan::number(s, 1, width))
try_consume!(scan::number(s, 1, width, true))
}
} else {
try_consume!(scan::number(s, 1, width))
try_consume!(scan::number(s, 1, width, true))
};
set(parsed, v)?;
}
Expand Down Expand Up @@ -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));
Expand Down
16 changes: 12 additions & 4 deletions src/format/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::Weekday;
/// 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 number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)> {
pub(super) fn number(s: &str, min: usize, max: usize, positive: bool) -> ParseResult<(&str, i64)> {
assert!(min <= max);

// We are only interested in ascii numbers, so we can work with the `str` as bytes. We stop on
Expand All @@ -25,23 +25,31 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)
return Err(TOO_SHORT);
}

// We construct the value as a negative integer first, and flip the sign if `positive`.
// This allows us to parse `i64::MIN`.
let mut n = 0i64;
for (i, c) in bytes.iter().take(max).cloned().enumerate() {
// cloned() = copied()
if !c.is_ascii_digit() {
if i < min {
return Err(INVALID);
} else {
if positive {
n = n.checked_neg().ok_or(OUT_OF_RANGE)?;
}
return Ok((&s[i..], n));
}
}

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_sub((c - b'0') as i64)) {
Some(n) => n,
None => return Err(OUT_OF_RANGE),
};
}

if positive {
n = n.checked_neg().ok_or(OUT_OF_RANGE)?;
}
Ok((&s[core::cmp::min(max, bytes.len())..], n))
}

Expand All @@ -50,7 +58,7 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)
pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, i64)> {
// record the number of digits consumed for later scaling.
let origlen = s.len();
let (s, v) = number(s, 1, 9)?;
let (s, v) = number(s, 1, 9, true)?;
let consumed = origlen - s.len();

// scale the number accordingly.
Expand All @@ -68,7 +76,7 @@ pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, i64)> {
/// Returns the number of whole nanoseconds (0--999,999,999).
pub(super) fn nanosecond_fixed(s: &str, digits: usize) -> ParseResult<(&str, i64)> {
// record the number of digits consumed for later scaling.
let (s, v) = number(s, digits, digits)?;
let (s, v) = number(s, digits, digits, true)?;

// scale the number accordingly.
static SCALE: [i64; 10] =
Expand Down

0 comments on commit 64a4bfd

Please sign in to comment.