2929use winnow:: {
3030 ascii:: alpha1,
3131 combinator:: { alt, opt, preceded, trace} ,
32+ error:: ErrMode ,
3233 seq,
3334 stream:: AsChar ,
34- token:: { take , take_while} ,
35+ token:: take_while,
3536 ModalResult , Parser ,
3637} ;
3738
38- use super :: primitive:: { dec_uint, s} ;
39+ use super :: primitive:: { ctx_err , dec_uint, s} ;
3940use crate :: ParseDateTimeError ;
4041
4142#[ derive( PartialEq , Eq , Clone , Debug , Default ) ]
@@ -45,39 +46,112 @@ pub struct Date {
4546 pub year : Option < u32 > ,
4647}
4748
49+ impl TryFrom < ( & str , u32 , u32 ) > for Date {
50+ type Error = & ' static str ;
51+
52+ /// Create a `Date` from a tuple of `(year, month, day)`.
53+ ///
54+ /// Note: The `year` is represented as a `&str` to handle a specific GNU
55+ /// compatibility quirk. According to the GNU documentation: "if the year is
56+ /// 68 or smaller, then 2000 is added to it; otherwise, if year is less than
57+ /// 100, then 1900 is added to it." This adjustment only applies to
58+ /// two-digit year strings. For example, `"00"` is interpreted as `2000`,
59+ /// whereas `"0"`, `"000"`, or `"0000"` are interpreted as `0`.
60+ fn try_from ( value : ( & str , u32 , u32 ) ) -> Result < Self , Self :: Error > {
61+ let ( year_str, month, day) = value;
62+
63+ let mut year = year_str
64+ . parse :: < u32 > ( )
65+ . map_err ( |_| "year must be a valid number" ) ?;
66+
67+ // If year is 68 or smaller, then 2000 is added to it; otherwise, if year
68+ // is less than 100, then 1900 is added to it.
69+ //
70+ // GNU quirk: this only applies to two-digit years. For example,
71+ // "98-01-01" will be parsed as "1998-01-01", while "098-01-01" will be
72+ // parsed as "0098-01-01".
73+ if year_str. len ( ) == 2 {
74+ if year <= 68 {
75+ year += 2000
76+ } else {
77+ year += 1900
78+ }
79+ }
80+
81+ // 2147485547 is the maximum value accepted by GNU, but chrono only
82+ // behaves like GNU for years in the range: [0, 9999], so we keep in the
83+ // range [0, 9999].
84+ //
85+ // See discussion in https://github.com/uutils/parse_datetime/issues/160.
86+ if year > 9999 {
87+ return Err ( "year must be no greater than 9999" ) ;
88+ }
89+
90+ if !( 1 ..=12 ) . contains ( & month) {
91+ return Err ( "month must be between 1 and 12" ) ;
92+ }
93+
94+ let is_leap_year = ( year % 4 == 0 && year % 100 != 0 ) || ( year % 400 == 0 ) ;
95+
96+ if !( 1 ..=31 ) . contains ( & day)
97+ || ( month == 2 && day > ( if is_leap_year { 29 } else { 28 } ) )
98+ || ( ( month == 4 || month == 6 || month == 9 || month == 11 ) && day > 30 )
99+ {
100+ return Err ( "day is not valid for the given month" ) ;
101+ }
102+
103+ Ok ( Date {
104+ day,
105+ month,
106+ year : Some ( year) ,
107+ } )
108+ }
109+ }
110+
48111pub fn parse ( input : & mut & str ) -> ModalResult < Date > {
49112 alt ( ( iso1, iso2, us, literal1, literal2) ) . parse_next ( input)
50113}
51114
52- /// Parse `YYYY-MM-DD` or `YY-MM-DD `
115+ /// Parse `[year]-[month]-[day] `
53116///
54117/// This is also used by [`combined`](super::combined).
55118pub fn iso1 ( input : & mut & str ) -> ModalResult < Date > {
56- seq ! ( Date {
57- year: year. map( Some ) ,
58- _: s( '-' ) ,
59- month: month,
60- _: s( '-' ) ,
61- day: day,
62- } )
63- . parse_next ( input)
119+ let ( year, _, month, _, day) = (
120+ // `year` must be a `&str`, see comment in `TryFrom` impl for `Date`.
121+ s ( take_while ( 1 .., AsChar :: is_dec_digit) ) ,
122+ s ( '-' ) ,
123+ s ( dec_uint) ,
124+ s ( '-' ) ,
125+ s ( dec_uint) ,
126+ )
127+ . parse_next ( input) ?;
128+
129+ ( year, month, day)
130+ . try_into ( )
131+ . map_err ( |e| ErrMode :: Cut ( ctx_err ( e) ) )
64132}
65133
66- /// Parse `YYYYMMDD `
134+ /// Parse `[year][month][day] `
67135///
68136/// This is also used by [`combined`](super::combined).
69137pub fn iso2 ( input : & mut & str ) -> ModalResult < Date > {
70- s ( (
71- take ( 4usize ) . try_map ( |s : & str | s. parse :: < u32 > ( ) ) ,
72- take ( 2usize ) . try_map ( |s : & str | s. parse :: < u32 > ( ) ) ,
73- take ( 2usize ) . try_map ( |s : & str | s. parse :: < u32 > ( ) ) ,
74- ) )
75- . map ( |( year, month, day) : ( u32 , u32 , u32 ) | Date {
76- day,
77- month,
78- year : Some ( year) ,
79- } )
80- . parse_next ( input)
138+ let date_str = take_while ( 5 .., AsChar :: is_dec_digit) . parse_next ( input) ?;
139+ let len = date_str. len ( ) ;
140+
141+ // `year` must be a `&str`, see comment in `TryFrom` impl for `Date`.
142+ let year = & date_str[ ..len - 4 ] ;
143+
144+ let month = date_str[ len - 4 ..len - 2 ]
145+ . parse :: < u32 > ( )
146+ . map_err ( |_| ErrMode :: Cut ( ctx_err ( "month must be a valid number" ) ) ) ?;
147+
148+ let day = date_str[ len - 2 ..]
149+ . parse :: < u32 > ( )
150+ . map_err ( |_| ErrMode :: Cut ( ctx_err ( "day must be a valid number" ) ) ) ?;
151+
152+ ( year, month, day)
153+ . try_into ( )
154+ . map_err ( |e| ErrMode :: Cut ( ctx_err ( e) ) )
81155}
82156
83157/// Parse `MM/DD/YYYY`, `MM/DD/YY` or `MM/DD`
@@ -202,6 +276,94 @@ mod tests {
202276 // 14nov2022
203277 // ```
204278
279+ #[ test]
280+ fn iso1 ( ) {
281+ let reference = Date {
282+ year : Some ( 1 ) ,
283+ month : 1 ,
284+ day : 1 ,
285+ } ;
286+
287+ for mut s in [ "1-1-1" , "1 - 1 - 1" , "1-01-01" , "1-001-001" , "001-01-01" ] {
288+ let old_s = s. to_owned ( ) ;
289+ assert_eq ! ( parse( & mut s) . unwrap( ) , reference, "Format string: {old_s}" ) ;
290+ }
291+
292+ // GNU quirk: when year string is 2 characters long and year is 68 or
293+ // smaller, 2000 is added to it.
294+ let reference = Date {
295+ year : Some ( 2001 ) ,
296+ month : 1 ,
297+ day : 1 ,
298+ } ;
299+
300+ for mut s in [ "01-1-1" , "01-01-01" ] {
301+ let old_s = s. to_owned ( ) ;
302+ assert_eq ! ( parse( & mut s) . unwrap( ) , reference, "Format string: {old_s}" ) ;
303+ }
304+
305+ // GNU quirk: when year string is 2 characters long and year is less
306+ // than 100, 1900 is added to it.
307+ let reference = Date {
308+ year : Some ( 1970 ) ,
309+ month : 1 ,
310+ day : 1 ,
311+ } ;
312+
313+ for mut s in [ "70-1-1" , "70-01-01" ] {
314+ let old_s = s. to_owned ( ) ;
315+ assert_eq ! ( parse( & mut s) . unwrap( ) , reference, "Format string: {old_s}" ) ;
316+ }
317+
318+ for mut s in [ "01-00-01" , "01-13-01" , "01-01-32" , "01-02-29" , "01-04-31" ] {
319+ let old_s = s. to_owned ( ) ;
320+ assert ! ( parse( & mut s) . is_err( ) , "Format string: {old_s}" ) ;
321+ }
322+ }
323+
324+ #[ test]
325+ fn iso2 ( ) {
326+ let reference = Date {
327+ year : Some ( 1 ) ,
328+ month : 1 ,
329+ day : 1 ,
330+ } ;
331+
332+ for mut s in [ "10101" , "0010101" , "00010101" , "000010101" ] {
333+ let old_s = s. to_owned ( ) ;
334+ assert_eq ! ( parse( & mut s) . unwrap( ) , reference, "Format string: {old_s}" ) ;
335+ }
336+
337+ // GNU quirk: when year string is 2 characters long and year is 68 or
338+ // smaller, 2000 is added to it.
339+ let reference = Date {
340+ year : Some ( 2001 ) ,
341+ month : 1 ,
342+ day : 1 ,
343+ } ;
344+
345+ let mut s = "010101" ;
346+ let old_s = s. to_owned ( ) ;
347+ assert_eq ! ( parse( & mut s) . unwrap( ) , reference, "Format string: {old_s}" ) ;
348+
349+ // GNU quirk: when year string is 2 characters long and year is less
350+ // than 100, 1900 is added to it.
351+ let reference = Date {
352+ year : Some ( 1970 ) ,
353+ month : 1 ,
354+ day : 1 ,
355+ } ;
356+
357+ let mut s = "700101" ;
358+ let old_s = s. to_owned ( ) ;
359+ assert_eq ! ( parse( & mut s) . unwrap( ) , reference, "Format string: {old_s}" ) ;
360+
361+ for mut s in [ "010001" , "011301" , "010132" , "010229" , "010431" ] {
362+ let old_s = s. to_owned ( ) ;
363+ assert ! ( parse( & mut s) . is_err( ) , "Format string: {old_s}" ) ;
364+ }
365+ }
366+
205367 #[ test]
206368 fn with_year ( ) {
207369 let reference = Date {
0 commit comments