29
29
use winnow:: {
30
30
ascii:: alpha1,
31
31
combinator:: { alt, opt, preceded, trace} ,
32
+ error:: ErrMode ,
32
33
seq,
33
34
stream:: AsChar ,
34
- token:: { take , take_while} ,
35
+ token:: take_while,
35
36
ModalResult , Parser ,
36
37
} ;
37
38
38
- use super :: primitive:: { dec_uint, s} ;
39
+ use super :: primitive:: { ctx_err , dec_uint, s} ;
39
40
use crate :: ParseDateTimeError ;
40
41
41
42
#[ derive( PartialEq , Eq , Clone , Debug , Default ) ]
@@ -45,39 +46,112 @@ pub struct Date {
45
46
pub year : Option < u32 > ,
46
47
}
47
48
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 if year < 100 {
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
+
48
111
pub fn parse ( input : & mut & str ) -> ModalResult < Date > {
49
112
alt ( ( iso1, iso2, us, literal1, literal2) ) . parse_next ( input)
50
113
}
51
114
52
- /// Parse `YYYY-MM-DD` or `YY-MM-DD `
115
+ /// Parse `[year]-[month]-[day] `
53
116
///
54
117
/// This is also used by [`combined`](super::combined).
55
118
pub 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) ) )
64
132
}
65
133
66
- /// Parse `YYYYMMDD `
134
+ /// Parse `[year][month][day] `
67
135
///
68
136
/// This is also used by [`combined`](super::combined).
69
137
pub 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) ) )
81
155
}
82
156
83
157
/// Parse `MM/DD/YYYY`, `MM/DD/YY` or `MM/DD`
@@ -202,6 +276,94 @@ mod tests {
202
276
// 14nov2022
203
277
// ```
204
278
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
+
205
367
#[ test]
206
368
fn with_year ( ) {
207
369
let reference = Date {
0 commit comments