Skip to content

Commit 0ddeec7

Browse files
authored
Merge pull request #187 from yuankunzhang/year-module
feat: add a year module
2 parents 5b52929 + 2bb8ac8 commit 0ddeec7

File tree

3 files changed

+96
-98
lines changed

3 files changed

+96
-98
lines changed

src/items/date.rs

Lines changed: 12 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@
2828
2929
use winnow::{
3030
ascii::{alpha1, multispace1},
31-
combinator::{alt, eof, opt, preceded, terminated, trace},
31+
combinator::{alt, eof, opt, preceded, terminated},
3232
error::ErrMode,
3333
stream::AsChar,
3434
token::take_while,
3535
ModalResult, Parser,
3636
};
3737

38-
use super::primitive::{ctx_err, dec_uint, s};
38+
use super::{
39+
primitive::{ctx_err, dec_uint, s},
40+
year::{year_from_str, year_str},
41+
};
3942

4043
#[derive(PartialEq, Eq, Clone, Debug, Default)]
4144
pub struct Date {
@@ -50,40 +53,11 @@ impl TryFrom<(&str, u32, u32)> for Date {
5053
/// Create a `Date` from a tuple of `(year, month, day)`.
5154
///
5255
/// Note: The `year` is represented as a `&str` to handle a specific GNU
53-
/// compatibility quirk. According to the GNU documentation: "if the year is
54-
/// 68 or smaller, then 2000 is added to it; otherwise, if year is less than
55-
/// 100, then 1900 is added to it." This adjustment only applies to
56-
/// two-digit year strings. For example, `"00"` is interpreted as `2000`,
57-
/// whereas `"0"`, `"000"`, or `"0000"` are interpreted as `0`.
56+
/// compatibility quirk. See the comment in [`year`](super::year) for more
57+
/// details.
5858
fn try_from(value: (&str, u32, u32)) -> Result<Self, Self::Error> {
5959
let (year_str, month, day) = value;
60-
61-
let mut year = year_str
62-
.parse::<u32>()
63-
.map_err(|_| "year must be a valid number")?;
64-
65-
// If year is 68 or smaller, then 2000 is added to it; otherwise, if year
66-
// is less than 100, then 1900 is added to it.
67-
//
68-
// GNU quirk: this only applies to two-digit years. For example,
69-
// "98-01-01" will be parsed as "1998-01-01", while "098-01-01" will be
70-
// parsed as "0098-01-01".
71-
if year_str.len() == 2 {
72-
if year <= 68 {
73-
year += 2000
74-
} else {
75-
year += 1900
76-
}
77-
}
78-
79-
// 2147485547 is the maximum value accepted by GNU, but chrono only
80-
// behaves like GNU for years in the range: [0, 9999], so we keep in the
81-
// range [0, 9999].
82-
//
83-
// See discussion in https://github.com/uutils/parse_datetime/issues/160.
84-
if year > 9999 {
85-
return Err("year must be no greater than 9999");
86-
}
60+
let year = year_from_str(year_str)?;
8761

8862
if !(1..=12).contains(&month) {
8963
return Err("month must be between 1 and 12");
@@ -138,15 +112,8 @@ pub fn parse(input: &mut &str) -> ModalResult<Date> {
138112
///
139113
/// This is also used by [`combined`](super::combined).
140114
pub fn iso1(input: &mut &str) -> ModalResult<Date> {
141-
let (year, _, month, _, day) = (
142-
// `year` must be a `&str`, see comment in `TryFrom` impl for `Date`.
143-
s(take_while(1.., AsChar::is_dec_digit)),
144-
s('-'),
145-
s(dec_uint),
146-
s('-'),
147-
s(dec_uint),
148-
)
149-
.parse_next(input)?;
115+
let (year, _, month, _, day) =
116+
(year_str, s('-'), s(dec_uint), s('-'), s(dec_uint)).parse_next(input)?;
150117

151118
(year, month, day)
152119
.try_into()
@@ -160,7 +127,6 @@ pub fn iso2(input: &mut &str) -> ModalResult<Date> {
160127
let date_str = take_while(5.., AsChar::is_dec_digit).parse_next(input)?;
161128
let len = date_str.len();
162129

163-
// `year` must be a `&str`, see comment in `TryFrom` impl for `Date`.
164130
let year = &date_str[..len - 4];
165131

166132
let month = date_str[len - 4..len - 2]
@@ -226,7 +192,7 @@ fn literal1(input: &mut &str) -> ModalResult<Date> {
226192
opt(s('-')),
227193
s(literal_month),
228194
opt(terminated(
229-
preceded(opt(s('-')), s(take_while(1.., AsChar::is_dec_digit))),
195+
preceded(opt(s('-')), year_str),
230196
// The year must be followed by a space or end of input.
231197
alt((multispace1, eof)),
232198
)),
@@ -254,7 +220,7 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
254220
// space between the comma and the year. This is probably to
255221
// distinguish with floats.
256222
opt(s(terminated(',', multispace1))),
257-
s(take_while(1.., AsChar::is_dec_digit)),
223+
year_str,
258224
),
259225
// The year must be followed by a space or end of input.
260226
alt((multispace1, eof)),
@@ -272,31 +238,6 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
272238
}
273239
}
274240

275-
pub fn year(input: &mut &str) -> ModalResult<u32> {
276-
// 2147485547 is the maximum value accepted
277-
// by GNU, but chrono only behaves like GNU
278-
// for years in the range: [0, 9999], so we
279-
// keep in the range [0, 9999]
280-
trace(
281-
"year",
282-
s(
283-
take_while(1..=4, AsChar::is_dec_digit).map(|number_str: &str| {
284-
let year = number_str.parse::<u32>().unwrap();
285-
if number_str.len() == 2 {
286-
if year <= 68 {
287-
year + 2000
288-
} else {
289-
year + 1900
290-
}
291-
} else {
292-
year
293-
}
294-
}),
295-
),
296-
)
297-
.parse_next(input)
298-
}
299-
300241
/// Parse the name of a month (case-insensitive)
301242
fn literal_month(input: &mut &str) -> ModalResult<u32> {
302243
s(alpha1)
@@ -639,28 +580,4 @@ mod tests {
639580
assert_eq!(parse(&mut s).unwrap(), reference);
640581
}
641582
}
642-
643-
#[test]
644-
fn test_year() {
645-
use super::year;
646-
647-
// the minimun input length is 2
648-
// assert!(year(&mut "0").is_err());
649-
// -> GNU accepts year 0
650-
// test $(date -d '1-1-1' '+%Y') -eq '0001'
651-
652-
// test $(date -d '68-1-1' '+%Y') -eq '2068'
653-
// 2-characters are converted to 19XX/20XX
654-
assert_eq!(year(&mut "10").unwrap(), 2010u32);
655-
assert_eq!(year(&mut "68").unwrap(), 2068u32);
656-
assert_eq!(year(&mut "69").unwrap(), 1969u32);
657-
assert_eq!(year(&mut "99").unwrap(), 1999u32);
658-
// 3,4-characters are converted verbatim
659-
assert_eq!(year(&mut "468").unwrap(), 468u32);
660-
assert_eq!(year(&mut "469").unwrap(), 469u32);
661-
assert_eq!(year(&mut "1568").unwrap(), 1568u32);
662-
assert_eq!(year(&mut "1569").unwrap(), 1569u32);
663-
// consumes at most 4 characters from the input
664-
//assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
665-
}
666583
}

src/items/mod.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
//! > - pure numbers.
1919
//!
2020
//! We put all of those in separate modules:
21+
//! - [`combined`]
2122
//! - [`date`]
23+
//! - [`epoch`]
24+
//! - [`relative`]
2225
//! - [`time`]
2326
//! - [`timezone`]
24-
//! - [`combined`]
2527
//! - [`weekday`]
26-
//! - [`relative`]
28+
//! - [`year`]
2729
2830
#![allow(deprecated)]
2931

@@ -35,6 +37,7 @@ mod relative;
3537
mod time;
3638
mod timezone;
3739
mod weekday;
40+
mod year;
3841

3942
// utility modules
4043
mod builder;
@@ -219,7 +222,7 @@ fn parse_item(input: &mut &str) -> ModalResult<Item> {
219222
relative::parse.map(Item::Relative),
220223
weekday::parse.map(Item::Weekday),
221224
timezone::parse.map(Item::TimeZone),
222-
date::year.map(Item::Year),
225+
year::parse.map(Item::Year),
223226
)),
224227
)
225228
.parse_next(input)

src/items/year.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// For the full copyright and license information, please view the LICENSE
2+
// file that was distributed with this source code.
3+
4+
//! Parse a year from a string.
5+
//!
6+
//! The year must be parsed to a string first, this is to handle a specific GNU
7+
//! compatibility quirk. According to the GNU documentation: "if the year is 68
8+
//! or smaller, then 2000 is added to it; otherwise, if year is less than 100,
9+
//! then 1900 is added to it." This adjustment only applies to two-digit year
10+
//! strings. For example, `"00"` is interpreted as `2000`, whereas `"0"`,
11+
//! `"000"`, or `"0000"` are interpreted as `0`.
12+
13+
use winnow::{error::ErrMode, stream::AsChar, token::take_while, ModalResult, Parser};
14+
15+
use super::primitive::{ctx_err, s};
16+
17+
pub(super) fn parse(input: &mut &str) -> ModalResult<u32> {
18+
year_from_str(year_str(input)?).map_err(|e| ErrMode::Cut(ctx_err(e)))
19+
}
20+
21+
// TODO: Leverage `TryFrom` trait.
22+
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
23+
let mut year = year_str
24+
.parse::<u32>()
25+
.map_err(|_| "year must be a valid number")?;
26+
27+
// If year is 68 or smaller, then 2000 is added to it; otherwise, if year
28+
// is less than 100, then 1900 is added to it.
29+
//
30+
// GNU quirk: this only applies to two-digit years. For example,
31+
// "98-01-01" will be parsed as "1998-01-01", whereas "098-01-01" will be
32+
// parsed as "0098-01-01".
33+
if year_str.len() == 2 {
34+
if year <= 68 {
35+
year += 2000
36+
} else {
37+
year += 1900
38+
}
39+
}
40+
41+
// 2147485547 is the maximum value accepted by GNU, but chrono only
42+
// behaves like GNU for years in the range: [0, 9999], so we keep in the
43+
// range [0, 9999].
44+
//
45+
// See discussion in https://github.com/uutils/parse_datetime/issues/160.
46+
if year > 9999 {
47+
return Err("year must be no greater than 9999");
48+
}
49+
50+
Ok(year)
51+
}
52+
53+
pub(super) fn year_str<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
54+
s(take_while(1.., AsChar::is_dec_digit)).parse_next(input)
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use super::parse;
60+
61+
#[test]
62+
fn test_year() {
63+
// 2-characters are converted to 19XX/20XX
64+
assert_eq!(parse(&mut "10").unwrap(), 2010u32);
65+
assert_eq!(parse(&mut "68").unwrap(), 2068u32);
66+
assert_eq!(parse(&mut "69").unwrap(), 1969u32);
67+
assert_eq!(parse(&mut "99").unwrap(), 1999u32);
68+
69+
// 3,4-characters are converted verbatim
70+
assert_eq!(parse(&mut "468").unwrap(), 468u32);
71+
assert_eq!(parse(&mut "469").unwrap(), 469u32);
72+
assert_eq!(parse(&mut "1568").unwrap(), 1568u32);
73+
assert_eq!(parse(&mut "1569").unwrap(), 1569u32);
74+
75+
// years greater than 9999 are not accepted
76+
assert!(parse(&mut "10000").is_err());
77+
}
78+
}

0 commit comments

Comments
 (0)