nom_date_parsers/
numeric.rs

1use chrono::{Datelike, Local, NaiveDate};
2use nom::{
3    branch::alt,
4    bytes::complete::{tag, take},
5    character::complete::space1,
6    combinator::map_res,
7    sequence::{separated_pair, tuple},
8};
9
10use crate::{error::Error, types::IResult};
11
12/// Recognizes a separator of numeric date parts in the following templates
13/// (asterisk symbol denotes some separator):
14/// - dd\*mm\*yyyy
15/// - mm\*dd\*yyyy
16/// - yyyy\*mm\*dd
17///
18/// Currently the following separators are recognized: `/`, `-`, `.` and any
19/// number of spaces and tabs.
20pub fn numeric_date_parts_separator(input: &str) -> IResult<&str, ()> {
21    let (input, _) = alt((tag("/"), tag("-"), tag("."), space1))(input)?;
22
23    Ok((input, ()))
24}
25
26/// Recognizes either one or two digits of a `day` part.
27///
28/// Accepts numbers in the range `01..=31`, otherwise returns
29/// [`Error::DayOutOfRange`].
30///
31/// It can be used to recognize the `dd` part in the `dd`/mm/yyyy pattern, for
32/// instance.
33pub fn dd(input: &str) -> IResult<&str, u32> {
34    let (input, dd) = alt((
35        map_res(take(2_u8), |s: &str| s.parse()),
36        map_res(take(1_u8), |s: &str| s.parse()),
37    ))(input)?;
38
39    if dd == 0 || dd > 31 {
40        return Err(nom::Err::Error(Error::DayOutOfRange));
41    }
42    Ok((input, dd))
43}
44
45/// Recognizes either one or two digits of a `day` part and returns the
46/// [`NaiveDate`] with the selected day and current month and year if the date
47/// exists, otherwise returns [`Error::NonExistentDate`].
48///
49/// # Examples
50///
51/// ```
52/// use chrono::prelude::*;
53/// use nom_date_parsers::prelude::*;
54///
55/// assert_eq!(
56///     dd_only("13")?.1,
57///     Local::now().date_naive().with_day(13).unwrap()
58/// );
59/// assert_eq!(dd_only("42"), Err(nom::Err::Error(Error::DayOutOfRange)));
60///
61/// # Ok::<(), Box::<dyn std::error::Error>>(())
62/// ```
63pub fn dd_only(input: &str) -> IResult<&str, NaiveDate> {
64    let (input, day) = dd(input)?;
65    let now = Local::now();
66    let (month, year) = (now.month(), now.year());
67
68    Ok((
69        input,
70        NaiveDate::from_ymd_opt(year, month, day).ok_or(nom::Err::Error(Error::NonExistentDate))?,
71    ))
72}
73
74/// Recognizes either one or two digits of a `month` part.
75///
76/// Accepts numbers in the range `01..=12`, otherwise returns.
77/// [`Error::MonthOutOfRange`]
78pub fn mm(input: &str) -> IResult<&str, u32> {
79    let (input, mm) = alt((
80        map_res(take(2_u8), |s: &str| s.parse()),
81        map_res(take(1_u8), |s: &str| s.parse()),
82    ))(input)?;
83    if mm == 0 || mm > 12 {
84        return Err(nom::Err::Error(Error::MonthOutOfRange));
85    }
86
87    Ok((input, mm))
88}
89
90/// Recognizes the `day` and `month` parts separated by the
91/// [`numeric_date_parts_separator`] using the [`dd`] and [`mm`] parsers.
92pub fn dd_mm(input: &str) -> IResult<&str, (u32, u32)> {
93    separated_pair(dd, numeric_date_parts_separator, mm)(input)
94}
95
96/// Recognizes the `day` and `month` parts separated by the
97/// [`numeric_date_parts_separator`] and returns the [`NaiveDate`] with the
98/// selected day, month and current year if the date exists, otherwise returns
99/// [`Error::NonExistentDate`].
100///
101/// # Examples
102///
103/// ```
104/// use chrono::prelude::*;
105/// use nom_date_parsers::prelude::*;
106///
107/// assert_eq!(
108///     dd_mm_only("31-02"),
109///     NaiveDate::from_ymd_opt(Local::now().year(), 2, 31)
110///         .ok_or(nom::Err::Error(Error::NonExistentDate))
111///         .map(|d| ("", d))
112/// );
113///
114/// # Ok::<(), Box::<dyn std::error::Error>>(())
115/// ```
116pub fn dd_mm_only(input: &str) -> IResult<&str, NaiveDate> {
117    let (input, (day, month)) = dd_mm(input)?;
118    let year = Local::now().year();
119
120    Ok((
121        input,
122        NaiveDate::from_ymd_opt(year, month, day).ok_or(nom::Err::Error(Error::NonExistentDate))?,
123    ))
124}
125
126/// Recognizes the `month` and `day` parts separated by the
127/// [`numeric_date_parts_separator`] using the [`mm`] and [`dd`] parsers.
128pub fn mm_dd(input: &str) -> IResult<&str, (u32, u32)> {
129    separated_pair(mm, numeric_date_parts_separator, dd)(input)
130}
131
132/// Recognizes the `month` and `day` parts separated by the
133/// [`numeric_date_parts_separator`] and returns the [`NaiveDate`] with the
134/// selected day, month and current year if the date exists, otherwise returns
135/// [`Error::NonExistentDate`].
136///
137/// # Examples
138///
139/// ```
140/// use chrono::prelude::*;
141/// use nom_date_parsers::prelude::*;
142///
143/// assert_eq!(
144///     mm_dd_only("10/18")?.1,
145///     NaiveDate::from_ymd_opt(Local::now().year(), 10, 18).unwrap()
146/// );
147///
148/// # Ok::<(), Box::<dyn std::error::Error>>(())
149/// ```
150pub fn mm_dd_only(input: &str) -> IResult<&str, NaiveDate> {
151    let (input, (month, day)) = mm_dd(input)?;
152
153    Ok((
154        input,
155        NaiveDate::from_ymd_opt(Local::now().year(), month, day)
156            .ok_or(nom::Err::Error(Error::NonExistentDate))?,
157    ))
158}
159
160/// Recognizes four digits of the `year` part.
161///
162/// Accepts numbers in the range `0000..=9999`, technically.
163pub fn y4(input: &str) -> IResult<&str, u32> {
164    map_res(take(4_u8), |s: &str| s.parse::<u32>())(input)
165}
166
167/// Recognizes the `year`, `month` and `day` parts separated by the
168/// [`numeric_date_parts_separator`] and returns [`NaiveDate`] with the selected
169/// parts if the date exists, otherwise returns [`Error::NonExistentDate`].
170///
171/// # Examples
172///
173/// ```
174/// use chrono::prelude::*;
175/// use nom_date_parsers::prelude::*;
176///
177/// assert_eq!(
178///     y4_mm_dd("2024-07-13")?.1,
179///     NaiveDate::from_ymd_opt(2024, 7, 13).unwrap()
180/// );
181///
182/// # Ok::<(), Box::<dyn std::error::Error>>(())
183/// ```
184pub fn y4_mm_dd(input: &str) -> IResult<&str, NaiveDate> {
185    let (input, (y4, (), mm, (), dd)) = tuple((
186        y4,
187        numeric_date_parts_separator,
188        mm,
189        numeric_date_parts_separator,
190        dd,
191    ))(input)?;
192
193    Ok((
194        input,
195        NaiveDate::from_ymd_opt(y4 as i32, mm, dd)
196            .ok_or(nom::Err::Error(Error::NonExistentDate))?,
197    ))
198}
199
200/// Recognizes the `day`, `month` and `year` parts separated by the
201/// [`numeric_date_parts_separator`] and returns [`NaiveDate`] with the selected
202/// parts if the date exists, otherwise returns [`Error::NonExistentDate`].
203///
204/// # Examples
205///
206/// ```
207/// use chrono::prelude::*;
208/// use nom_date_parsers::prelude::*;
209///
210/// assert_eq!(
211///     dd_mm_y4("13/07/2024")?.1,
212///     NaiveDate::from_ymd_opt(2024, 7, 13).unwrap()
213/// );
214///
215/// # Ok::<(), Box::<dyn std::error::Error>>(())
216/// ```
217pub fn dd_mm_y4(input: &str) -> IResult<&str, NaiveDate> {
218    let (input, (dd, (), mm, (), y4)) = tuple((
219        dd,
220        numeric_date_parts_separator,
221        mm,
222        numeric_date_parts_separator,
223        y4,
224    ))(input)?;
225
226    Ok((
227        input,
228        NaiveDate::from_ymd_opt(y4 as i32, mm, dd)
229            .ok_or(nom::Err::Error(Error::NonExistentDate))?,
230    ))
231}
232
233/// Recognizes the `month`, `day` and `year` parts separated by the
234/// [`numeric_date_parts_separator`] and returns [`NaiveDate`] with the selected
235/// parts if the date exists, otherwise returns [`Error::NonExistentDate`].
236///
237/// # Examples
238///
239/// ```
240/// use chrono::prelude::*;
241/// use nom_date_parsers::prelude::*;
242///
243/// assert_eq!(
244///     mm_dd_y4("07-13-2024")?.1,
245///     NaiveDate::from_ymd_opt(2024, 7, 13).unwrap()
246/// );
247///
248/// # Ok::<(), Box::<dyn std::error::Error>>(())
249/// ```
250pub fn mm_dd_y4(input: &str) -> IResult<&str, NaiveDate> {
251    let (input, (mm, (), dd, (), y4)) = tuple((
252        mm,
253        numeric_date_parts_separator,
254        dd,
255        numeric_date_parts_separator,
256        y4,
257    ))(input)?;
258
259    Ok((
260        input,
261        NaiveDate::from_ymd_opt(y4 as i32, mm, dd)
262            .ok_or(nom::Err::Error(Error::NonExistentDate))?,
263    ))
264}
265
266#[cfg(test)]
267mod tests {
268    use chrono::Local;
269    use nom::error::ErrorKind;
270    use pretty_assertions::assert_eq;
271    use rstest::rstest;
272
273    use super::*;
274
275    #[rstest]
276    #[case("9", Ok(("", 9)))]
277    #[case("09", Ok(("", 9)))]
278    #[case("31", Ok(("", 31)))]
279    #[case("00", Err(nom::Err::Error(Error::DayOutOfRange)))]
280    #[case("42", Err(nom::Err::Error(Error::DayOutOfRange)))]
281    fn test_dd(#[case] input: &str, #[case] expected: IResult<&str, u32>) {
282        assert_eq!(dd(input), expected);
283    }
284
285    #[rstest]
286    #[case("9", Ok(("", Local::now().date_naive().with_day(9).unwrap())))]
287    #[case("09", Ok(("", Local::now().date_naive().with_day(9).unwrap())))]
288    #[case("31", Local::now().date_naive().with_day(31).ok_or(nom::Err::Error(Error::NonExistentDate)).map(|d| ("", d)))]
289    #[case("00", Err(nom::Err::Error(Error::DayOutOfRange)))]
290    #[case("42", Err(nom::Err::Error(Error::DayOutOfRange)))]
291    fn test_dd_only(#[case] input: &str, #[case] expected: IResult<&str, NaiveDate>) {
292        assert_eq!(dd_only(input), expected)
293    }
294
295    #[rstest]
296    #[case("9", Ok(("", 9)))]
297    #[case("09", Ok(("", 9)))]
298    #[case("12", Ok(("", 12)))]
299    #[case("00", Err(nom::Err::Error(Error::MonthOutOfRange)))]
300    #[case("13", Err(nom::Err::Error(Error::MonthOutOfRange)))]
301    fn test_mm(#[case] input: &str, #[case] expected: IResult<&str, u32>) {
302        assert_eq!(mm(input), expected);
303    }
304
305    #[rstest]
306    #[case("3/9", Ok(("", Local::now().date_naive().with_day(3).unwrap().with_month(9).unwrap())))]
307    #[case("03-09", Ok(("", Local::now().date_naive().with_day(3).unwrap().with_month(9).unwrap())))]
308    #[case("03/12", Ok(("", Local::now().date_naive().with_day(3).unwrap().with_month(12).unwrap())))]
309    #[case("00", Err(nom::Err::Error(Error::DayOutOfRange)))]
310    #[case("42", Err(nom::Err::Error(Error::DayOutOfRange)))]
311    #[case("13.00", Err(nom::Err::Error(Error::MonthOutOfRange)))]
312    #[case("13\t13", Err(nom::Err::Error(Error::MonthOutOfRange)))]
313    fn test_dd_mm_only(#[case] input: &str, #[case] expected: IResult<&str, NaiveDate>) {
314        assert_eq!(dd_mm_only(input), expected);
315    }
316
317    #[rstest]
318    #[case("0042", Ok(("", 42)))]
319    #[case("2024", Ok(("", 2024)))]
320    #[case("42", Err(nom::Err::Error(Error::Nom("42", ErrorKind::Eof))))]
321    #[case("10001", Ok(("1", 1000)))]
322    fn test_y4(#[case] input: &str, #[case] expected: IResult<&str, u32>) {
323        assert_eq!(y4(input), expected);
324    }
325
326    #[rstest]
327    #[case("2024-06-13", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
328    #[case("2024/06-13", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
329    #[case("2024.06.13", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
330    #[case("2024    06\t13", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
331    #[case("2024/00/06", Err(nom::Err::Error(Error::MonthOutOfRange)))]
332    #[case("2024/13/06", Err(nom::Err::Error(Error::MonthOutOfRange)))]
333    #[case("2024/10/00", Err(nom::Err::Error(Error::DayOutOfRange)))]
334    #[case("2024/10/42", Err(nom::Err::Error(Error::DayOutOfRange)))]
335    fn test_y4_mm_dd(#[case] input: &str, #[case] expected: IResult<&str, NaiveDate>) {
336        assert_eq!(y4_mm_dd(input), expected);
337    }
338
339    #[rstest]
340    #[case("13-06-2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
341    #[case("13/06-2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
342    #[case("13.06.2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
343    #[case("13    06\t2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
344    #[case("00/10/2024", Err(nom::Err::Error(Error::DayOutOfRange)))]
345    #[case("42/10/2024", Err(nom::Err::Error(Error::DayOutOfRange)))]
346    #[case("06/00/2024", Err(nom::Err::Error(Error::MonthOutOfRange)))]
347    #[case("06/13/2024", Err(nom::Err::Error(Error::MonthOutOfRange)))]
348    #[case("31/02/2024", Err(nom::Err::Error(Error::NonExistentDate)))]
349    fn test_dd_mm_y4(#[case] input: &str, #[case] expected: IResult<&str, NaiveDate>) {
350        assert_eq!(dd_mm_y4(input), expected);
351    }
352
353    #[rstest]
354    #[case("06-13-2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
355    #[case("06/13-2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
356    #[case("06.13.2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
357    #[case("06    13\t2024", Ok(("", NaiveDate::from_ymd_opt(2024, 6, 13).unwrap())))]
358    #[case("00/06/2024", Err(nom::Err::Error(Error::MonthOutOfRange)))]
359    #[case("13/06/2024", Err(nom::Err::Error(Error::MonthOutOfRange)))]
360    #[case("10/00/2024", Err(nom::Err::Error(Error::DayOutOfRange)))]
361    #[case("10/32/2024", Err(nom::Err::Error(Error::DayOutOfRange)))]
362    #[case("02/31/2024", Err(nom::Err::Error(Error::NonExistentDate)))]
363    fn test_mm_dd_y4(#[case] input: &str, #[case] expected: IResult<&str, NaiveDate>) {
364        assert_eq!(mm_dd_y4(input), expected)
365    }
366}