aimx/literals/
date.rs

1//! Date literal parser for AIMX expressions.
2//!
3//! Provides internal helpers to parse ISO 8601-style date strings
4//! into [`jiff::civil::DateTime`].
5//! Supported forms:
6//! - `YYYY-MM-DD`
7//! - `YYYY-MM-DD[ T]HH:MM:SS[.sss]`
8//! - `YYYY-MM-DD[ T]HH:MM:SS[.sss]Z` (treated as UTC).
9use jiff::civil::DateTime;
10use nom::{
11    IResult, Parser,
12    branch::alt,
13    bytes::complete::take_while_m_n,
14    character::complete::char,
15    combinator::{map, map_res},
16};
17
18/// Parse an AIMX date literal.
19///
20/// Tries, in order:
21/// - `YYYY-MM-DD[ T]HH:MM:SS[.sss]Z`
22/// - `YYYY-MM-DD[ T]HH:MM:SS[.sss]`
23/// - `YYYY-MM-DD`.
24///
25/// Returns remaining input and the parsed [`DateTime`].
26pub fn parse_date(input: &str) -> IResult<&str, DateTime> {
27    alt((
28        parse_full_datetime_with_timezone,
29        parse_full_datetime,
30        parse_date_only,
31    ))
32    .parse(input)
33}
34
35/// Parse `YYYY-MM-DD[ T]HH:MM:SS[.sss]Z` as UTC.
36fn parse_full_datetime_with_timezone(input: &str) -> IResult<&str, DateTime> {
37    map_res(
38        (
39            parse_date_components,
40            alt((char('T'), char(' '))),
41            parse_time_components,
42            parse_optional_nanosecond,
43            char('Z'),
44        ),
45        |((year, month, day), _, (hour, minute, second), nanos, _)| {
46            // Use jiff's builder pattern - it will validate the date
47            DateTime::new(year, month, day, hour, minute, second, nanos)
48        },
49    )
50    .parse(input)
51}
52
53/// Parse `YYYY-MM-DD[ T]HH:MM:SS[.sss]` without timezone.
54fn parse_full_datetime(input: &str) -> IResult<&str, DateTime> {
55    map_res(
56        (
57            parse_date_components,
58            alt((char('T'), char(' '))),
59            parse_time_components,
60            parse_optional_nanosecond,
61        ),
62        |((year, month, day), _, (hour, minute, second), nanosecond)| {
63            DateTime::new(year, month, day, hour, minute, second, nanosecond)
64        },
65    )
66    .parse(input)
67}
68
69/// Parse `YYYY-MM-DD` as midnight.
70fn parse_date_only(input: &str) -> IResult<&str, DateTime> {
71    map_res(parse_date_components, |(year, month, day)| {
72        // Use jiff's builder pattern for date only - it will validate the date
73        DateTime::new(year, month, day, 0, 0, 0, 0)
74    })
75    .parse(input)
76}
77
78/// Parse `YYYY-MM-DD` components.
79fn parse_date_components(input: &str) -> IResult<&str, (i16, i8, i8)> {
80    map(
81        (
82            parse_four_digit_number,
83            char('-'),
84            parse_two_digit_number,
85            char('-'),
86            parse_two_digit_number,
87        ),
88        |(year, _, month, _, day)| (year, month, day),
89    )
90    .parse(input)
91}
92
93/// Parse `HH:MM:SS` components.
94fn parse_time_components(input: &str) -> IResult<&str, (i8, i8, i8)> {
95    map(
96        (
97            parse_two_digit_number,
98            char(':'),
99            parse_two_digit_number,
100            char(':'),
101            parse_two_digit_number,
102        ),
103        |(hour, _, minute, _, second)| (hour, minute, second),
104    )
105    .parse(input)
106}
107
108/// Parse optional `.sss` fractional seconds into nanoseconds.
109fn parse_optional_nanosecond(input: &str) -> IResult<&str, i32> {
110    alt((
111        map(
112            (
113                char('.'),
114                take_while_m_n(1, 3, |c: char| c.is_ascii_digit()),
115            ),
116            |(_, digits): (char, &str)| {
117                // Convert milliseconds to nanoseconds
118                let nanosecond: i32 = digits.parse().unwrap_or(0) * 1000000;
119                // Scale to nanoseconds if fewer than 3 digits
120                match digits.len() {
121                    1 => nanosecond * 100,
122                    2 => nanosecond * 10,
123                    _ => nanosecond,
124                }
125            },
126        ),
127        nom::combinator::success(0i32),
128    ))
129    .parse(input)
130}
131
132/// Parse four ASCII digits (year).
133fn parse_four_digit_number(input: &str) -> IResult<&str, i16> {
134    map_res(
135        take_while_m_n(4, 4, |c: char| c.is_ascii_digit()),
136        |s: &str| s.parse::<i16>(),
137    )
138    .parse(input)
139}
140
141/// Parse two ASCII digits.
142fn parse_two_digit_number(input: &str) -> IResult<&str, i8> {
143    map_res(
144        take_while_m_n(2, 2, |c: char| c.is_ascii_digit()),
145        |s: &str| s.parse::<i8>(),
146    )
147    .parse(input)
148}