Skip to content

Commit f759661

Browse files
committed
formalize grammar of timestamp strings and use nom to parse them
New feature: the parser now supports timestamp strings containing an internal decimal point (either '.' or ',').
1 parent 98826c1 commit f759661

File tree

5 files changed

+96
-81
lines changed

5 files changed

+96
-81
lines changed

src/parse/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// For the full copyright and license information, please view the LICENSE
22
// file that was distributed with this source code.
33
use relative_time::relative_times;
4+
use timestamp::timestamp;
45
use weekday::weekday;
56

67
mod primitive;
78
mod relative_time;
9+
mod timestamp;
810
mod weekday;
911

1012
// TODO: more specific errors?
@@ -22,6 +24,13 @@ pub(crate) fn parse_relative_times(input: &str) -> Result<Vec<RelativeTime>, Par
2224
.map_err(|_| ParseError)
2325
}
2426

27+
/// Parses a string of timestamp into a `f64` value (the seconds since epoch).
28+
pub(crate) fn parse_timestamp(input: &str) -> Result<f64, ParseError> {
29+
timestamp(input)
30+
.map(|(_, timestamp)| timestamp)
31+
.map_err(|_| ParseError)
32+
}
33+
2534
/// Parses a string of weekday into a `WeekdayItem` struct.
2635
pub(crate) fn parse_weekday(input: &str) -> Result<WeekdayItem, ParseError> {
2736
weekday(input)

src/parse/primitive.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub(super) fn integer(input: &str) -> IResult<&str, i64> {
6969
/// sign characters and whitespace characters. All but the last sign character
7070
/// is ignored, and the last sign character is returned as the result. This
7171
/// quirky behavior is to stay consistent with GNU date.
72-
fn sign(input: &str) -> IResult<&str, char> {
72+
pub(super) fn sign(input: &str) -> IResult<&str, char> {
7373
fold_many1(
7474
terminated(one_of("+-"), multispace0),
7575
|| '+',

src/parse/timestamp.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// For the full copyright and license information, please view the LICENSE
2+
// file that was distributed with this source code.
3+
4+
//! Module to parser timestamp strings.
5+
//!
6+
//! Grammar definition:
7+
//!
8+
//! ```ebnf
9+
//! timestamp = "@" seconds ;
10+
//!
11+
//! seconds = [ sign ] , { digit } , [ ("." | ",") , { digit } ] ;
12+
//! ```
13+
14+
use nom::{
15+
bytes::complete::tag,
16+
character::complete::{digit1, one_of},
17+
combinator::{all_consuming, map_res, opt, recognize},
18+
sequence::preceded,
19+
IResult, Parser,
20+
};
21+
22+
use super::primitive::sign;
23+
24+
pub(super) fn timestamp(input: &str) -> IResult<&str, f64> {
25+
all_consuming(preceded(tag("@"), seconds)).parse(input)
26+
}
27+
28+
fn seconds(input: &str) -> IResult<&str, f64> {
29+
let (rest, sign) = opt(sign).parse(input)?;
30+
let (rest, num) = map_res(
31+
recognize((digit1, opt((one_of(".,"), digit1)))),
32+
|s: &str| {
33+
s.replace(",", ".")
34+
.parse::<f64>()
35+
.map_err(|_| "invalid seconds")
36+
},
37+
)
38+
.parse(rest)?;
39+
40+
let num = if sign == Some('-') { -num } else { num };
41+
Ok((rest, num))
42+
}
43+
44+
#[cfg(test)]
45+
mod tests {
46+
use super::*;
47+
48+
#[test]
49+
fn test_timestamp() {
50+
assert!(timestamp("invalid").is_err());
51+
52+
assert_eq!(timestamp("@-1234.567"), Ok(("", -1234.567)));
53+
assert_eq!(timestamp("@1234,567"), Ok(("", 1234.567)));
54+
assert_eq!(timestamp("@- 1234.567"), Ok(("", -1234.567)));
55+
assert_eq!(timestamp("@-+- 1234,567"), Ok(("", -1234.567)));
56+
assert_eq!(timestamp("@1234"), Ok(("", 1234.0)));
57+
assert_eq!(timestamp("@-1234"), Ok(("", -1234.0)));
58+
}
59+
60+
#[test]
61+
fn test_seconds() {
62+
assert!(seconds("").is_err());
63+
assert!(seconds("invalid").is_err());
64+
65+
assert_eq!(seconds("-1234.567"), Ok(("", -1234.567)));
66+
assert_eq!(seconds("1234,567"), Ok(("", 1234.567)));
67+
assert_eq!(seconds("- 1234.567"), Ok(("", -1234.567)));
68+
assert_eq!(seconds("-+- 1234,567"), Ok(("", -1234.567)));
69+
assert_eq!(seconds("1234"), Ok(("", 1234.0)));
70+
assert_eq!(seconds("-1234"), Ok(("", -1234.0)));
71+
assert_eq!(seconds("1234.567abc"), Ok(("abc", 1234.567)));
72+
}
73+
}

src/parse/weekday.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
//! | "wednesday" | "wednes" | "wed"
1515
//! | "thursday" | "thurs" | "thur" | "thu"
1616
//! | "friday" | "fri"
17-
//! | "saturday" | "sat"
17+
//! | "saturday" | "sat" ;
1818
//! ```
1919
2020
use chrono::Weekday;

src/parse_timestamp.rs

Lines changed: 12 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,13 @@
11
// For the full copyright and license information, please view the LICENSE
22
// 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::{self, IResult, Parser};
14-
15-
#[derive(Debug, PartialEq)]
16-
pub enum ParseTimestampError {
17-
InvalidNumber(ParseIntError),
18-
InvalidInput,
19-
}
20-
21-
impl Display for ParseTimestampError {
22-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23-
match self {
24-
Self::InvalidInput => {
25-
write!(f, "Invalid input string: cannot be parsed as a timestamp")
26-
}
27-
Self::InvalidNumber(err) => {
28-
write!(f, "Invalid timestamp number: {err}")
29-
}
30-
}
31-
}
32-
}
33-
34-
impl Error for ParseTimestampError {}
35-
36-
// TODO is this necessary
37-
impl From<ParseIntError> for ParseTimestampError {
38-
fn from(err: ParseIntError) -> Self {
39-
Self::InvalidNumber(err)
40-
}
41-
}
42-
43-
type NomError<'a> = nom::Err<nom::error::Error<&'a str>>;
44-
45-
impl<'a> From<NomError<'a>> for ParseTimestampError {
46-
fn from(_err: NomError<'a>) -> Self {
47-
Self::InvalidInput
48-
}
49-
}
50-
51-
pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseTimestampError> {
52-
let s = s.trim().to_lowercase();
53-
let s = s.as_str();
54-
55-
let res: IResult<&str, (char, &str)> = all_consuming(preceded(
56-
char('@'),
57-
(
58-
// Note: to stay compatible with gnu date this code allows
59-
// multiple + and - and only considers the last one
60-
fold_many0(
61-
// parse either + or -
62-
alt((char('+'), char('-'))),
63-
// start with a +
64-
|| '+',
65-
// whatever we get (+ or -), update the accumulator to that value
66-
|_, c| c,
67-
),
68-
digit1,
69-
),
70-
))
71-
.parse(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)
3+
use crate::{parse, ParseDateTimeError};
4+
5+
pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseDateTimeError> {
6+
// If the timestamp contains excess precision, it is truncated toward minus
7+
// infinity.
8+
parse::parse_timestamp(s)
9+
.map(|f| f.floor() as i64)
10+
.map_err(|_| ParseDateTimeError::InvalidInput)
8211
}
8312

8413
#[cfg(test)]
@@ -100,6 +29,10 @@ mod tests {
10029
assert_eq!(parse_timestamp("@+++-12"), Ok(-12));
10130
assert_eq!(parse_timestamp("@+----+12"), Ok(12));
10231
assert_eq!(parse_timestamp("@++++-123"), Ok(-123));
32+
33+
// with excess precision
34+
assert_eq!(parse_timestamp("@1234.567"), Ok(1234));
35+
assert_eq!(parse_timestamp("@-1234,567"), Ok(-1235));
10336
}
10437

10538
#[test]

0 commit comments

Comments
 (0)