Skip to content

Commit 1a6a6f6

Browse files
authored
Merge pull request #143 from yuankunzhang/main
use proper error mode in items::parse()
2 parents 1552608 + 294bd5e commit 1a6a6f6

File tree

3 files changed

+96
-35
lines changed

3 files changed

+96
-35
lines changed

src/items/mod.rs

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod ordinal;
3333
mod relative;
3434
mod time;
3535
mod weekday;
36+
3637
mod epoch {
3738
use winnow::{combinator::preceded, ModalResult, Parser};
3839

@@ -41,6 +42,7 @@ mod epoch {
4142
s(preceded("@", dec_int)).parse_next(input)
4243
}
4344
}
45+
4446
mod timezone {
4547
use super::time;
4648
use winnow::ModalResult;
@@ -53,12 +55,11 @@ mod timezone {
5355
use chrono::NaiveDate;
5456
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};
5557

56-
use winnow::error::{StrContext, StrContextValue};
5758
use winnow::{
5859
ascii::{digit1, multispace0},
5960
combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated, trace},
60-
error::{ContextError, ErrMode, ParserError},
61-
stream::AsChar,
61+
error::{AddContext, ContextError, ErrMode, ParserError, StrContext, StrContextValue},
62+
stream::{AsChar, Stream},
6263
token::{none_of, one_of, take_while},
6364
ModalResult, Parser,
6465
};
@@ -145,9 +146,9 @@ where
145146
/// following two forms:
146147
///
147148
/// - 0
148-
/// - [+-][1-9][0-9]*
149+
/// - [+-]?[1-9][0-9]*
149150
///
150-
/// Inputs like [+-]0[0-9]* (e.g., `+012`) are therefore rejected. We provide a
151+
/// Inputs like [+-]?0[0-9]* (e.g., `+012`) are therefore rejected. We provide a
151152
/// custom implementation to support such zero-prefixed integers.
152153
fn dec_int<'a, E>(input: &mut &'a str) -> winnow::Result<i32, E>
153154
where
@@ -175,6 +176,23 @@ where
175176
.parse_next(input)
176177
}
177178

179+
/// Parse a float number.
180+
///
181+
/// Rationale for not using `winnow::ascii::float`: the `float` parser provided
182+
/// by winnow accepts E-notation numbers (e.g., `1.23e4`), whereas GNU date
183+
/// rejects such numbers. To remain compatible with GNU date, we provide a
184+
/// custom implementation that only accepts inputs like [+-]?[0-9]+(\.[0-9]+)?.
185+
fn float<'a, E>(input: &mut &'a str) -> winnow::Result<f64, E>
186+
where
187+
E: ParserError<&'a str>,
188+
{
189+
(opt(one_of(['+', '-'])), digit1, opt(preceded('.', digit1)))
190+
.void()
191+
.take()
192+
.verify_map(|s: &str| s.parse().ok())
193+
.parse_next(input)
194+
}
195+
178196
// Parse an item
179197
pub fn parse_one(input: &mut &str) -> ModalResult<Item> {
180198
trace(
@@ -193,6 +211,14 @@ pub fn parse_one(input: &mut &str) -> ModalResult<Item> {
193211
.parse_next(input)
194212
}
195213

214+
fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError> {
215+
ErrMode::Cut(ContextError::new()).add_context(
216+
input,
217+
&input.checkpoint(),
218+
StrContext::Expected(StrContextValue::Description(reason)),
219+
)
220+
}
221+
196222
pub fn parse(input: &mut &str) -> ModalResult<Vec<Item>> {
197223
let mut items = Vec::new();
198224
let mut date_seen = false;
@@ -206,13 +232,10 @@ pub fn parse(input: &mut &str) -> ModalResult<Vec<Item>> {
206232
match item {
207233
Item::DateTime(ref dt) => {
208234
if date_seen || time_seen {
209-
let mut ctx_err = ContextError::new();
210-
ctx_err.push(StrContext::Expected(
211-
winnow::error::StrContextValue::Description(
212-
"date or time cannot appear more than once",
213-
),
235+
return Err(expect_error(
236+
input,
237+
"date or time cannot appear more than once",
214238
));
215-
return Err(ErrMode::Backtrack(ctx_err));
216239
}
217240

218241
date_seen = true;
@@ -223,45 +246,35 @@ pub fn parse(input: &mut &str) -> ModalResult<Vec<Item>> {
223246
}
224247
Item::Date(ref d) => {
225248
if date_seen {
226-
let mut ctx_err = ContextError::new();
227-
ctx_err.push(StrContext::Expected(StrContextValue::Description(
228-
"date cannot appear more than once",
229-
)));
230-
return Err(ErrMode::Backtrack(ctx_err));
249+
return Err(expect_error(input, "date cannot appear more than once"));
231250
}
232251

233252
date_seen = true;
234253
if d.year.is_some() {
235254
year_seen = true;
236255
}
237256
}
238-
Item::Time(_) => {
257+
Item::Time(ref t) => {
239258
if time_seen {
240-
let mut ctx_err = ContextError::new();
241-
ctx_err.push(StrContext::Expected(StrContextValue::Description(
242-
"time cannot appear more than once",
243-
)));
244-
return Err(ErrMode::Backtrack(ctx_err));
259+
return Err(expect_error(input, "time cannot appear more than once"));
245260
}
246261
time_seen = true;
262+
if t.offset.is_some() {
263+
tz_seen = true;
264+
}
247265
}
248266
Item::Year(_) => {
249267
if year_seen {
250-
let mut ctx_err = ContextError::new();
251-
ctx_err.push(StrContext::Expected(StrContextValue::Description(
252-
"year cannot appear more than once",
253-
)));
254-
return Err(ErrMode::Backtrack(ctx_err));
268+
return Err(expect_error(input, "year cannot appear more than once"));
255269
}
256270
year_seen = true;
257271
}
258272
Item::TimeZone(_) => {
259273
if tz_seen {
260-
let mut ctx_err = ContextError::new();
261-
ctx_err.push(StrContext::Expected(StrContextValue::Description(
274+
return Err(expect_error(
275+
input,
262276
"timezone cannot appear more than once",
263-
)));
264-
return Err(ErrMode::Backtrack(ctx_err));
277+
));
265278
}
266279
tz_seen = true;
267280
}
@@ -276,7 +289,7 @@ pub fn parse(input: &mut &str) -> ModalResult<Vec<Item>> {
276289

277290
space.parse_next(input)?;
278291
if !input.is_empty() {
279-
return Err(ErrMode::Backtrack(ContextError::new()));
292+
return Err(expect_error(input, "unexpected input"));
280293
}
281294

282295
Ok(items)
@@ -540,4 +553,46 @@ mod tests {
540553
test_eq_fmt("%Y-%m-%d %H:%M:%S %:z", "Jul 17 06:14:49 2024 BRT"),
541554
);
542555
}
556+
557+
#[test]
558+
fn invalid() {
559+
let result = parse(&mut "2025-05-19 2024-05-20 06:14:49");
560+
assert!(result.is_err());
561+
assert!(result
562+
.unwrap_err()
563+
.to_string()
564+
.contains("date or time cannot appear more than once"));
565+
566+
let result = parse(&mut "2025-05-19 2024-05-20");
567+
assert!(result.is_err());
568+
assert!(result
569+
.unwrap_err()
570+
.to_string()
571+
.contains("date cannot appear more than once"));
572+
573+
let result = parse(&mut "06:14:49 06:14:49");
574+
assert!(result.is_err());
575+
assert!(result
576+
.unwrap_err()
577+
.to_string()
578+
.contains("time cannot appear more than once"));
579+
580+
let result = parse(&mut "2025-05-19 2024");
581+
assert!(result.is_err());
582+
assert!(result
583+
.unwrap_err()
584+
.to_string()
585+
.contains("year cannot appear more than once"));
586+
587+
let result = parse(&mut "2025-05-19 +00:00 +01:00");
588+
assert!(result.is_err());
589+
assert!(result
590+
.unwrap_err()
591+
.to_string()
592+
.contains("timezone cannot appear more than once"));
593+
594+
let result = parse(&mut "2025-05-19 abcdef");
595+
assert!(result.is_err());
596+
assert!(result.unwrap_err().to_string().contains("unexpected input"));
597+
}
543598
}

src/items/relative.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@
3232
//! > ‘this thursday’.
3333
3434
use winnow::{
35-
ascii::{alpha1, float},
35+
ascii::alpha1,
3636
combinator::{alt, opt},
3737
ModalResult, Parser,
3838
};
3939

40-
use super::{ordinal::ordinal, s};
40+
use super::{float, ordinal::ordinal, s};
4141

4242
#[derive(Clone, Copy, Debug, PartialEq)]
4343
pub enum Relative {

src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,13 @@ mod tests {
165165

166166
#[test]
167167
fn invalid_formats() {
168-
let invalid_dts = vec!["NotADate", "202104", "202104-12T22:37:47"];
168+
let invalid_dts = vec![
169+
"NotADate",
170+
"202104",
171+
"202104-12T22:37:47",
172+
"a774e26sec", // 774e26 is not a valid seconds value (we don't accept E-notation)
173+
"12.", // Invalid floating point number
174+
];
169175
for dt in invalid_dts {
170176
assert_eq!(parse_datetime(dt), Err(ParseDateTimeError::InvalidInput));
171177
}

0 commit comments

Comments
 (0)