Skip to content

Commit ac9bdab

Browse files
committed
formalize grammar of weekday strings and use nom to parse them
1 parent 89f4810 commit ac9bdab

File tree

6 files changed

+625
-229
lines changed

6 files changed

+625
-229
lines changed

src/lib.rs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ mod parse_time_only_str;
2323
mod parse_weekday;
2424

2525
use chrono::{
26-
DateTime, Datelike, Duration, FixedOffset, Local, LocalResult, MappedLocalTime, NaiveDate,
27-
NaiveDateTime, TimeZone, Timelike,
26+
DateTime, FixedOffset, Local, LocalResult, MappedLocalTime, NaiveDate, NaiveDateTime, TimeZone,
2827
};
2928

3029
use parse_relative_time::parse_relative_time_at_date;
3130
use parse_timestamp::parse_timestamp;
31+
use parse_weekday::parse_weekday_at_date;
3232

3333
#[derive(Debug, PartialEq)]
3434
pub enum ParseDateTimeError {
@@ -302,23 +302,8 @@ where
302302
}
303303

304304
// parse weekday
305-
if let Some(weekday) = parse_weekday::parse_weekday(s.as_ref()) {
306-
let mut beginning_of_day = date
307-
.with_hour(0)
308-
.unwrap()
309-
.with_minute(0)
310-
.unwrap()
311-
.with_second(0)
312-
.unwrap()
313-
.with_nanosecond(0)
314-
.unwrap();
315-
316-
while beginning_of_day.weekday() != weekday {
317-
beginning_of_day += Duration::days(1);
318-
}
319-
320-
let dt = DateTime::<FixedOffset>::from(beginning_of_day);
321-
305+
if let Ok(dt) = parse_weekday_at_date(date, s.as_ref()) {
306+
let dt = DateTime::<FixedOffset>::from(dt);
322307
return Some((dt, s.as_ref().len()));
323308
}
324309

src/parse/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,41 @@
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 weekday::weekday;
45

6+
mod primitive;
57
mod relative_time;
8+
mod weekday;
69

710
// TODO: more specific errors?
811
#[derive(Debug)]
912
pub(crate) struct ParseError;
1013

1114
pub(crate) use relative_time::RelativeTime;
1215
pub(crate) use relative_time::TimeUnit;
16+
pub(crate) use weekday::WeekdayItem;
1317

1418
/// Parses a string of relative times into a vector of `RelativeTime` structs.
1519
pub(crate) fn parse_relative_times(input: &str) -> Result<Vec<RelativeTime>, ParseError> {
1620
relative_times(input)
1721
.map(|(_, times)| times)
1822
.map_err(|_| ParseError)
1923
}
24+
25+
/// Parses a string of weekday into a `WeekdayItem` struct.
26+
pub(crate) fn parse_weekday(input: &str) -> Result<WeekdayItem, ParseError> {
27+
weekday(input)
28+
.map(|(_, weekday_item)| weekday_item)
29+
.map_err(|_| ParseError)
30+
}
31+
32+
/// Finds a value in a list of pairs by its key.
33+
fn find_in_pairs<T: Clone>(pairs: &[(&str, T)], key: &str) -> Option<T> {
34+
pairs.iter().find_map(|(k, v)| {
35+
if k.eq_ignore_ascii_case(key) {
36+
Some(v.clone())
37+
} else {
38+
None
39+
}
40+
})
41+
}

src/parse/primitive.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 relative time strings.
5+
//!
6+
//! Grammar definition:
7+
//!
8+
//! ```ebnf
9+
//! ordinal = "last" | "this" | "next"
10+
//! | "first" | "third" | "fourth" | "fifth"
11+
//! | "sixth" | "seventh" | "eighth" | "ninth"
12+
//! | "tenth" | "eleventh" | "twelfth" ;
13+
//!
14+
//! integer = [ sign ] , digit , { digit } ;
15+
//!
16+
//! sign = { ("+" | "-") , { whitespace } } ;
17+
//!
18+
//! digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
19+
//! ```
20+
21+
use nom::{
22+
bytes::complete::take_while1,
23+
character::complete::{digit1, multispace0, one_of},
24+
combinator::{map_res, opt},
25+
multi::fold_many1,
26+
sequence::terminated,
27+
IResult, Parser,
28+
};
29+
30+
use super::find_in_pairs;
31+
32+
const ORDINALS: &[(&str, i64)] = &[
33+
("last", -1),
34+
("this", 0),
35+
("next", 1),
36+
("first", 1),
37+
// Unfortunately we can't use "second" as ordinal, the keyword is overloaded
38+
("third", 3),
39+
("fourth", 4),
40+
("fifth", 5),
41+
("sixth", 6),
42+
("seventh", 7),
43+
("eighth", 8),
44+
("ninth", 9),
45+
("tenth", 10),
46+
("eleventh", 11),
47+
("twelfth", 12),
48+
];
49+
50+
pub(super) fn ordinal(input: &str) -> IResult<&str, i64> {
51+
map_res(take_while1(|c: char| c.is_alphabetic()), |s: &str| {
52+
find_in_pairs(ORDINALS, s).ok_or("unknown ordinal")
53+
})
54+
.parse(input)
55+
}
56+
57+
pub(super) fn integer(input: &str) -> IResult<&str, i64> {
58+
let (rest, sign) = opt(sign).parse(input)?;
59+
let (rest, num) = map_res(digit1, str::parse::<i64>).parse(rest)?;
60+
if sign == Some('-') {
61+
Ok((rest, -num))
62+
} else {
63+
Ok((rest, num))
64+
}
65+
}
66+
67+
/// Parses a sign (either + or -) from the input string. The input string must
68+
/// start with a sign character followed by arbitrary number of interleaving
69+
/// sign characters and whitespace characters. All but the last sign character
70+
/// is ignored, and the last sign character is returned as the result. This
71+
/// quirky behavior is to stay consistent with GNU date.
72+
fn sign(input: &str) -> IResult<&str, char> {
73+
fold_many1(
74+
terminated(one_of("+-"), multispace0),
75+
|| '+',
76+
|acc, c| if "+-".contains(c) { c } else { acc },
77+
)
78+
.parse(input)
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
85+
#[test]
86+
fn test_ordinal() {
87+
assert!(ordinal("").is_err());
88+
assert!(ordinal("invalid").is_err());
89+
assert!(ordinal(" last").is_err());
90+
91+
assert_eq!(ordinal("last"), Ok(("", -1)));
92+
assert_eq!(ordinal("this"), Ok(("", 0)));
93+
assert_eq!(ordinal("next"), Ok(("", 1)));
94+
assert_eq!(ordinal("first"), Ok(("", 1)));
95+
assert_eq!(ordinal("third"), Ok(("", 3)));
96+
assert_eq!(ordinal("fourth"), Ok(("", 4)));
97+
assert_eq!(ordinal("fifth"), Ok(("", 5)));
98+
assert_eq!(ordinal("sixth"), Ok(("", 6)));
99+
assert_eq!(ordinal("seventh"), Ok(("", 7)));
100+
assert_eq!(ordinal("eighth"), Ok(("", 8)));
101+
assert_eq!(ordinal("ninth"), Ok(("", 9)));
102+
assert_eq!(ordinal("tenth"), Ok(("", 10)));
103+
assert_eq!(ordinal("eleventh"), Ok(("", 11)));
104+
assert_eq!(ordinal("twelfth"), Ok(("", 12)));
105+
106+
// Boundary
107+
assert_eq!(ordinal("last123"), Ok(("123", -1)));
108+
assert_eq!(ordinal("last abc"), Ok((" abc", -1)));
109+
assert!(ordinal("lastabc").is_err());
110+
111+
// Case insensitive
112+
assert_eq!(ordinal("THIS"), Ok(("", 0)));
113+
assert_eq!(ordinal("This"), Ok(("", 0)));
114+
}
115+
116+
#[test]
117+
fn test_integer() {
118+
assert!(integer("").is_err());
119+
assert!(integer("invalid").is_err());
120+
assert!(integer(" 123").is_err());
121+
122+
assert_eq!(integer("123"), Ok(("", 123)));
123+
assert_eq!(integer("+123"), Ok(("", 123)));
124+
assert_eq!(integer("- 123"), Ok(("", -123)));
125+
126+
// Boundary
127+
assert_eq!(integer("- 123abc"), Ok(("abc", -123)));
128+
assert_eq!(integer("- +- 123abc"), Ok(("abc", -123)));
129+
}
130+
131+
#[test]
132+
fn test_sign() {
133+
assert!(sign("").is_err());
134+
assert!(sign("invalid").is_err());
135+
assert!(sign(" +").is_err());
136+
137+
assert_eq!(sign("+"), Ok(("", '+')));
138+
assert_eq!(sign("-"), Ok(("", '-')));
139+
assert_eq!(sign("- + - "), Ok(("", '-')));
140+
141+
// Boundary
142+
assert_eq!(sign("- + - abc"), Ok(("abc", '-')));
143+
}
144+
}

0 commit comments

Comments
 (0)