@@ -33,6 +33,7 @@ mod ordinal;
3333mod relative;
3434mod time;
3535mod weekday;
36+
3637mod 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+
4446mod timezone {
4547 use super :: time;
4648 use winnow:: ModalResult ;
@@ -53,12 +55,11 @@ mod timezone {
5355use chrono:: NaiveDate ;
5456use chrono:: { DateTime , Datelike , FixedOffset , TimeZone , Timelike } ;
5557
56- use winnow:: error:: { StrContext , StrContextValue } ;
5758use 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.
152153fn dec_int < ' a , E > ( input : & mut & ' a str ) -> winnow:: Result < i32 , E >
153154where
@@ -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
179197pub 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+
196222pub 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}
0 commit comments