Skip to content

Commit b150932

Browse files
committed
Fixes Negative numbers for @ not being recognized
This commit resolves #40. Adds new file and functions parse_timestamp. Adds tests for handling negative numbers.
1 parent f9ce504 commit b150932

File tree

5 files changed

+143
-4
lines changed

5 files changed

+143
-4
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ readme = "README.md"
1010
[dependencies]
1111
regex = "1.9"
1212
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
13+
nom = "7.1.3"

src/lib.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ use std::fmt::{self, Display};
1414

1515
// Expose parse_datetime
1616
mod parse_relative_time;
17+
mod parse_timestamp;
1718

1819
use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone};
1920

2021
use parse_relative_time::parse_relative_time;
22+
use parse_timestamp::parse_timestamp;
2123

2224
#[derive(Debug, PartialEq)]
2325
pub enum ParseDateTimeError {
@@ -169,9 +171,9 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
169171
}
170172

171173
// Parse epoch seconds
172-
if s.as_ref().bytes().next() == Some(b'@') {
173-
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") {
174-
if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
174+
if let Ok(timestamp) = parse_timestamp(s.as_ref()) {
175+
if let Some(timestamp_date) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
176+
if let Ok(dt) = naive_dt_to_fixed_offset(date, timestamp_date) {
175177
return Ok(dt);
176178
}
177179
}
@@ -359,15 +361,21 @@ mod tests {
359361
use chrono::{TimeZone, Utc};
360362

361363
#[test]
362-
fn test_positive_offsets() {
364+
fn test_positive_and_negative_offsets() {
363365
let offsets: Vec<i64> = vec![
364366
0, 1, 2, 10, 100, 150, 2000, 1234400000, 1334400000, 1692582913, 2092582910,
365367
];
366368

367369
for offset in offsets {
370+
// positive offset
368371
let time = Utc.timestamp_opt(offset, 0).unwrap();
369372
let dt = parse_datetime(format!("@{}", offset));
370373
assert_eq!(dt.unwrap(), time);
374+
375+
// negative offset
376+
let time = Utc.timestamp_opt(-offset, 0).unwrap();
377+
let dt = parse_datetime(format!("@-{}", offset));
378+
assert_eq!(dt.unwrap(), time);
371379
}
372380
}
373381
}

src/parse_relative_time.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// For the full copyright and license information, please view the LICENSE
2+
// file that was distributed with this source code.
13
use crate::ParseDateTimeError;
24
use chrono::{Duration, Local, NaiveDate, Utc};
35
use regex::Regex;

src/parse_timestamp.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// For the full copyright and license information, please view the LICENSE
2+
// file that was distributed with this source code.
3+
use core::fmt;
4+
use std::error::Error;
5+
use std::fmt::Display;
6+
use std::num::ParseIntError;
7+
8+
use nom::branch::alt;
9+
use nom::character::complete::{char, digit1};
10+
use nom::combinator::all_consuming;
11+
use nom::multi::fold_many0;
12+
use nom::sequence::preceded;
13+
use nom::sequence::tuple;
14+
use nom::{self, IResult};
15+
16+
#[derive(Debug, PartialEq)]
17+
pub enum ParseTimestampError {
18+
InvalidNumber(ParseIntError),
19+
InvalidInput,
20+
}
21+
22+
impl Display for ParseTimestampError {
23+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24+
match self {
25+
Self::InvalidInput => {
26+
write!(f, "Invalid input string: cannot be parsed as a timestamp")
27+
}
28+
Self::InvalidNumber(err) => {
29+
write!(f, "Invalid timestamp number: {err}")
30+
}
31+
}
32+
}
33+
}
34+
35+
impl Error for ParseTimestampError {}
36+
37+
// TODO is this necessary
38+
impl From<ParseIntError> for ParseTimestampError {
39+
fn from(err: ParseIntError) -> Self {
40+
Self::InvalidNumber(err)
41+
}
42+
}
43+
44+
type NomError<'a> = nom::Err<nom::error::Error<&'a str>>;
45+
46+
impl<'a> From<NomError<'a>> for ParseTimestampError {
47+
fn from(_err: NomError<'a>) -> Self {
48+
Self::InvalidInput
49+
}
50+
}
51+
52+
pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseTimestampError> {
53+
let s = s.trim().to_lowercase();
54+
let s = s.as_str();
55+
56+
let res: IResult<&str, (char, &str)> = all_consuming(preceded(
57+
char('@'),
58+
tuple((
59+
// Note: to stay compatible with gnu date this code allows
60+
// multiple + and - and only considers the last one
61+
fold_many0(
62+
// parse either + or -
63+
alt((char('+'), char('-'))),
64+
// start with a +
65+
|| '+',
66+
// whatever we get (+ or -), update the accumulator to that value
67+
|_, c| c,
68+
),
69+
digit1,
70+
)),
71+
))(s);
72+
73+
let (_, (sign, number_str)) = res?;
74+
75+
let mut number = number_str.parse::<i64>()?;
76+
77+
if sign == '-' {
78+
number *= -1;
79+
}
80+
81+
Ok(number)
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
87+
use crate::parse_timestamp::parse_timestamp;
88+
89+
#[test]
90+
fn test_valid_timestamp() {
91+
assert_eq!(parse_timestamp("@1234"), Ok(1234));
92+
assert_eq!(parse_timestamp("@99999"), Ok(99999));
93+
assert_eq!(parse_timestamp("@-4"), Ok(-4));
94+
assert_eq!(parse_timestamp("@-99999"), Ok(-99999));
95+
assert_eq!(parse_timestamp("@+4"), Ok(4));
96+
assert_eq!(parse_timestamp("@0"), Ok(0));
97+
98+
// gnu date acceppts numbers signs and uses the last sign
99+
assert_eq!(parse_timestamp("@---+12"), Ok(12));
100+
assert_eq!(parse_timestamp("@+++-12"), Ok(-12));
101+
assert_eq!(parse_timestamp("@+----+12"), Ok(12));
102+
assert_eq!(parse_timestamp("@++++-123"), Ok(-123));
103+
}
104+
105+
#[test]
106+
fn test_invalid_timestamp() {
107+
assert!(parse_timestamp("@").is_err());
108+
assert!(parse_timestamp("@+--+").is_err());
109+
assert!(parse_timestamp("@+1ab2").is_err());
110+
}
111+
}

0 commit comments

Comments
 (0)