Skip to content

Commit f75ec46

Browse files
authored
Merge pull request #230 from yuankunzhang/parser-combinators
refactor: replace imperative parsing loop with functional combinators
2 parents 583eafe + 2dc4fa3 commit f75ec46

File tree

2 files changed

+48
-62
lines changed

2 files changed

+48
-62
lines changed

src/items/builder.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use jiff::{civil, Span, Zoned};
55

6-
use super::{date, epoch, error, relative, time, timezone, weekday, year};
6+
use super::{date, epoch, error, relative, time, timezone, weekday, year, Item};
77

88
/// The builder is used to construct a DateTime object from various components.
99
/// The parser creates a `DateTimeBuilder` object with the parsed components,
@@ -248,3 +248,26 @@ impl DateTimeBuilder {
248248
Ok(dt)
249249
}
250250
}
251+
252+
impl TryFrom<Vec<Item>> for DateTimeBuilder {
253+
type Error = &'static str;
254+
255+
fn try_from(items: Vec<Item>) -> Result<Self, Self::Error> {
256+
let mut builder = DateTimeBuilder::new();
257+
258+
for item in items {
259+
builder = match item {
260+
Item::Timestamp(ts) => builder.set_timestamp(ts)?,
261+
Item::DateTime(dt) => builder.set_date(dt.date)?.set_time(dt.time)?,
262+
Item::Date(d) => builder.set_date(d)?,
263+
Item::Time(t) => builder.set_time(t)?,
264+
Item::Weekday(weekday) => builder.set_weekday(weekday)?,
265+
Item::TimeZone(tz) => builder.set_timezone(tz)?,
266+
Item::Relative(rel) => builder.push_relative(rel)?,
267+
Item::Pure(pure) => builder.set_pure(pure)?,
268+
}
269+
}
270+
271+
Ok(builder)
272+
}
273+
}

src/items/mod.rs

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub(crate) mod error;
4949
use jiff::Zoned;
5050
use primitive::space;
5151
use winnow::{
52-
combinator::{alt, eof, terminated, trace},
52+
combinator::{alt, eof, preceded, repeat_till, terminated, trace},
5353
error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue},
5454
stream::Stream,
5555
ModalResult, Parser,
@@ -70,7 +70,8 @@ enum Item {
7070
Pure(String),
7171
}
7272

73-
/// Parse a date and time string based on a specific date.
73+
/// Parse a date and time string and build a `Zoned` object. The parsed result
74+
/// is resolved against the given base date and time.
7475
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Result<Zoned, Error> {
7576
let input = input.as_ref().to_ascii_lowercase();
7677
match parse(&mut input.as_str()) {
@@ -79,11 +80,12 @@ pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Res
7980
}
8081
}
8182

82-
/// Parse a date and time string based on the current local time.
83+
/// Parse a date and time string and build a `Zoned` object. The parsed result
84+
/// is resolved against the current local date and time.
8385
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, Error> {
8486
let input = input.as_ref().to_ascii_lowercase();
8587
match parse(&mut input.as_str()) {
86-
Ok(builder) => builder.build(),
88+
Ok(builder) => builder.build(), // the builder uses current local date and time if no base is given.
8789
Err(e) => Err(e.into()),
8890
}
8991
}
@@ -196,7 +198,7 @@ fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
196198
fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
197199
trace(
198200
"parse_timestamp",
199-
terminated(epoch::parse.map(Item::Timestamp), eof),
201+
terminated(epoch::parse.map(Item::Timestamp), preceded(space, eof)),
200202
)
201203
.verify_map(|ts: Item| {
202204
if let Item::Timestamp(ts) = ts {
@@ -210,59 +212,13 @@ fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
210212

211213
/// Parse a sequence of items.
212214
fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
213-
let mut builder = DateTimeBuilder::new();
214-
215-
loop {
216-
match parse_item.parse_next(input) {
217-
Ok(item) => match item {
218-
Item::Timestamp(ts) => {
219-
builder = builder
220-
.set_timestamp(ts)
221-
.map_err(|e| expect_error(input, e))?;
222-
}
223-
Item::DateTime(dt) => {
224-
builder = builder
225-
.set_date(dt.date)
226-
.map_err(|e| expect_error(input, e))?
227-
.set_time(dt.time)
228-
.map_err(|e| expect_error(input, e))?;
229-
}
230-
Item::Date(d) => {
231-
builder = builder.set_date(d).map_err(|e| expect_error(input, e))?;
232-
}
233-
Item::Time(t) => {
234-
builder = builder.set_time(t).map_err(|e| expect_error(input, e))?;
235-
}
236-
Item::Weekday(weekday) => {
237-
builder = builder
238-
.set_weekday(weekday)
239-
.map_err(|e| expect_error(input, e))?;
240-
}
241-
Item::TimeZone(tz) => {
242-
builder = builder
243-
.set_timezone(tz)
244-
.map_err(|e| expect_error(input, e))?;
245-
}
246-
Item::Relative(rel) => {
247-
builder = builder
248-
.push_relative(rel)
249-
.map_err(|e| expect_error(input, e))?;
250-
}
251-
Item::Pure(pure) => {
252-
builder = builder.set_pure(pure).map_err(|e| expect_error(input, e))?;
253-
}
254-
},
255-
Err(ErrMode::Backtrack(_)) => break,
256-
Err(e) => return Err(e),
257-
}
258-
}
259-
260-
space.parse_next(input)?;
261-
if !input.is_empty() {
262-
return Err(expect_error(input, "unexpected input"));
263-
}
215+
let (items, _): (Vec<Item>, _) = trace(
216+
"parse_items",
217+
repeat_till(0.., parse_item, preceded(space, eof)),
218+
)
219+
.parse_next(input)?;
264220

265-
Ok(builder)
221+
items.try_into().map_err(|e| expect_error(input, e))
266222
}
267223

268224
/// Parse an item.
@@ -346,6 +302,11 @@ mod tests {
346302
test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", "@1690466034")
347303
);
348304

305+
assert_eq!(
306+
"2023-07-27T13:53:54+00:00",
307+
test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", " @1690466034 ")
308+
);
309+
349310
// https://github.com/uutils/coreutils/issues/6398
350311
// TODO: make this work
351312
// assert_eq!("1111 1111 00", test_eq_fmt("%m%d %H%M %S", "11111111"));
@@ -371,6 +332,12 @@ mod tests {
371332
);
372333
}
373334

335+
#[test]
336+
fn empty() {
337+
let result = parse(&mut "");
338+
assert!(result.is_ok());
339+
}
340+
374341
#[test]
375342
fn invalid() {
376343
let result = parse(&mut "2025-05-19 2024-05-20 06:14:49");
@@ -396,7 +363,6 @@ mod tests {
396363

397364
let result = parse(&mut "2025-05-19 +00:00 +01:00");
398365
assert!(result.is_err());
399-
assert!(result.unwrap_err().to_string().contains("unexpected input"));
400366

401367
let result = parse(&mut "m1y");
402368
assert!(result.is_err());
@@ -407,15 +373,12 @@ mod tests {
407373

408374
let result = parse(&mut "2025-05-19 abcdef");
409375
assert!(result.is_err());
410-
assert!(result.unwrap_err().to_string().contains("unexpected input"));
411376

412377
let result = parse(&mut "@1690466034 2025-05-19");
413378
assert!(result.is_err());
414-
assert!(result.unwrap_err().to_string().contains("unexpected input"));
415379

416380
let result = parse(&mut "2025-05-19 @1690466034");
417381
assert!(result.is_err());
418-
assert!(result.unwrap_err().to_string().contains("unexpected input"));
419382

420383
// Pure number as year (too large).
421384
let result = parse(&mut "jul 18 12:30 10000");

0 commit comments

Comments
 (0)