aimx/literals/date.rs
1//! Date parsing utilities for ISO 8601 format strings.
2//!
3//! This module provides parsing functions for ISO 8601 date and datetime formats
4//! used in AIMX expressions. It supports various date formats including
5//! date-only, date with time, and date with time and timezone specifications.
6//!
7//! The parser is designed to integrate with the larger AIMX literal parsing system
8//! and produces [`jiff::civil::DateTime`] objects that can be used throughout
9//! the expression evaluation system.
10//!
11//! # Supported Formats
12//!
13//! - **Date only**: `YYYY-MM-DD`
14//! - **Date with time**: `YYYY-MM-DDTHH:MM:SS` or `YYYY-MM-DD HH:MM:SS`
15//! - **Date with time and timezone**: `YYYY-MM-DDTHH:MM:SSZ` or `YYYY-MM-DD HH:MM:SSZ`
16//! - **Date with optional milliseconds**: `YYYY-MM-DDTHH:MM:SS.sssZ`
17//!
18//! # Examples
19//!
20//! ```text
21//! 2023-01-15 // Date only
22//! 2023-01-15T10:30:00 // Date with time (T separator)
23//! 2023-01-15 10:30:00 // Date with time (space separator)
24//! 2023-01-15T10:30:00.123Z // Date with time and milliseconds
25//! 2023-01-15T10:30:00Z // Date with time and UTC timezone
26//! ```
27//!
28//! # Integration with AIMX
29//!
30//! This parser is used internally by the [`crate::literal::parse_literal`] function in the
31//! [`crate::literal`] module to parse date literals. It's not typically used
32//! directly but rather through the main expression parsing system.
33//!
34//! Date literals in expressions are automatically converted to [`crate::Literal::Date`]
35//! variants which can then be used with date functions or in date arithmetic.
36//!
37//! ```rust
38//! use aimx::{aimx_parse, ExpressionLike, Context, Literal};
39//!
40//! let mut context = Context::new();
41//!
42//! // Parse a date literal directly
43//! let expression = aimx_parse("2023-01-15");
44//! let result = expression.evaluate(&mut context).unwrap();
45//! assert!(matches!(result, aimx::Value::Literal(Literal::Date(_))));
46//!
47//! // Use in date functions
48//! let expression = aimx_parse("year(2023-12-25)");
49//! let result = expression.evaluate(&mut context).unwrap();
50//! assert_eq!(result.to_string(), "2023");
51//! ```
52//!
53//! # Notes
54//!
55//! - All dates are parsed into [`jiff::civil::DateTime`] objects
56//! - The parser validates date components (e.g., rejects invalid months or days)
57//! - Timezone information is currently ignored but the 'Z' suffix is supported
58//! - Nanosecond precision is supported with up to 3 decimal places for milliseconds
59//! - Whitespace is automatically trimmed before parsing
60
61use jiff::civil::DateTime;
62use nom::{
63 IResult, Parser,
64 branch::alt,
65 bytes::complete::take_while_m_n,
66 character::complete::char,
67 combinator::{map, map_res},
68};
69
70/// Parse a date literal in ISO 8601 format.
71///
72/// This is the main entry point for parsing date literals in AIMX.
73/// It supports multiple ISO 8601 date formats in order of preference:
74/// 1. Date with time and timezone: `YYYY-MM-DDTHH:MM:SS[.sss]Z`
75/// 2. Date with time: `YYYY-MM-DD[ T]HH:MM:SS[.sss]`
76/// 3. Date only: `YYYY-MM-DD`
77///
78/// This function is used internally by the literal parsing system and is not
79/// typically called directly by user code. For parsing date strings in your
80/// own code, consider using the date functions like `as_date()` instead.
81///
82/// # Arguments
83///
84/// * `input` - The input string slice to parse
85///
86/// # Returns
87///
88/// A `IResult` containing the remaining unparsed input and parsed `DateTime`.
89///
90/// # Examples
91///
92/// ```rust
93/// use aimx::literals::date::parse_date;
94/// use jiff::civil::DateTime;
95///
96/// // Date only
97/// let result = parse_date("2023-01-15");
98/// assert!(result.is_ok());
99/// let (remaining, datetime) = result.unwrap();
100/// assert_eq!(remaining, "");
101/// assert_eq!(datetime.year(), 2023);
102/// assert_eq!(datetime.month(), 1);
103/// assert_eq!(datetime.day(), 15);
104///
105/// // Date with time (space separator)
106/// let result = parse_date("2023-01-15 10:30:00");
107/// assert!(result.is_ok());
108///
109/// // Date with time and timezone
110/// let result = parse_date("2023-01-15T10:30:00Z");
111/// assert!(result.is_ok());
112///
113/// // Date with milliseconds
114/// let result = parse_date("2023-01-15T10:30:00.123Z");
115/// assert!(result.is_ok());
116/// ```
117///
118/// # See Also
119///
120/// - [`crate::literal::parse_literal`] - Main parser that uses this function
121/// - [`crate::functions`] - Module containing date manipulation functions that work with parsed dates
122pub fn parse_date(input: &str) -> IResult<&str, DateTime> {
123 alt((
124 parse_full_datetime_with_timezone,
125 parse_full_datetime,
126 parse_date_only,
127 ))
128 .parse(input)
129}
130
131/// Parse a date with time and timezone (Z suffix).
132///
133/// This function handles dates in the format: `YYYY-MM-DD[ T]HH:MM:SS[.sss]Z`
134/// where the 'Z' indicates UTC timezone.
135///
136/// # Arguments
137///
138/// * `input` - The input string slice to parse
139///
140/// # Returns
141///
142/// A `IResult` containing the remaining unparsed input and parsed `DateTime`.
143fn parse_full_datetime_with_timezone(input: &str) -> IResult<&str, DateTime> {
144 map_res(
145 (
146 parse_date_components,
147 alt((char('T'), char(' '))),
148 parse_time_components,
149 parse_optional_nanosecond,
150 char('Z'),
151 ),
152 |((year, month, day), _, (hour, minute, second), nanos, _)| {
153 // Use jiff's builder pattern - it will validate the date
154 DateTime::new(year, month, day, hour, minute, second, nanos)
155 },
156 )
157 .parse(input)
158}
159
160/// Parse a date with time (no timezone).
161///
162/// This function handles dates in the format: `YYYY-MM-DD[ T]HH:MM:SS[.sss]`
163/// without timezone information.
164///
165/// # Arguments
166///
167/// * `input` - The input string slice to parse
168///
169/// # Returns
170///
171/// A `IResult` containing the remaining unparsed input and parsed `DateTime`.
172fn parse_full_datetime(input: &str) -> IResult<&str, DateTime> {
173 map_res(
174 (
175 parse_date_components,
176 alt((char('T'), char(' '))),
177 parse_time_components,
178 parse_optional_nanosecond,
179 ),
180 |((year, month, day), _, (hour, minute, second), nanosecond)| {
181 DateTime::new(year, month, day, hour, minute, second, nanosecond)
182 },
183 )
184 .parse(input)
185}
186
187/// Parse date only format.
188///
189/// This function handles dates in the format: `YYYY-MM-DD`
190/// with time components set to 00:00:00.000.
191///
192/// # Arguments
193///
194/// * `input` - The input string slice to parse
195///
196/// # Returns
197///
198/// A `IResult` containing the remaining unparsed input and parsed `DateTime`.
199fn parse_date_only(input: &str) -> IResult<&str, DateTime> {
200 map_res(parse_date_components, |(year, month, day)| {
201 // Use jiff's builder pattern for date only - it will validate the date
202 DateTime::new(year, month, day, 0, 0, 0, 0)
203 })
204 .parse(input)
205}
206
207/// Parse date components (YYYY-MM-DD).
208///
209/// This function parses the date portion: `YYYY-MM-DD`
210///
211/// # Arguments
212///
213/// * `input` - The input string slice to parse
214///
215/// # Returns
216///
217/// A `IResult` containing the remaining unparsed input and parsed `(year, month, day)` tuple.
218fn parse_date_components(input: &str) -> IResult<&str, (i16, i8, i8)> {
219 map(
220 (
221 parse_four_digit_number,
222 char('-'),
223 parse_two_digit_number,
224 char('-'),
225 parse_two_digit_number,
226 ),
227 |(year, _, month, _, day)| (year, month, day),
228 )
229 .parse(input)
230}
231
232/// Parse time components (HH:MM:SS).
233///
234/// This function parses the time portion: `HH:MM:SS`
235///
236/// # Arguments
237///
238/// * `input` - The input string slice to parse
239///
240/// # Returns
241///
242/// A `IResult` containing the remaining unparsed input and parsed `(hour, minute, second)` tuple.
243fn parse_time_components(input: &str) -> IResult<&str, (i8, i8, i8)> {
244 map(
245 (
246 parse_two_digit_number,
247 char(':'),
248 parse_two_digit_number,
249 char(':'),
250 parse_two_digit_number,
251 ),
252 |(hour, _, minute, _, second)| (hour, minute, second),
253 )
254 .parse(input)
255}
256
257/// Parse optional nanosecond/millisecond component.
258///
259/// This function parses the optional fractional seconds: `.sss`
260/// supporting up to 3 digits (milliseconds).
261///
262/// # Arguments
263///
264/// * `input` - The input string slice to parse
265///
266/// # Returns
267///
268/// A `IResult` containing the remaining unparsed input and parsed nanoseconds as i32.
269fn parse_optional_nanosecond(input: &str) -> IResult<&str, i32> {
270 alt((
271 map(
272 (
273 char('.'),
274 take_while_m_n(1, 3, |c: char| c.is_ascii_digit()),
275 ),
276 |(_, digits): (char, &str)| {
277 // Convert milliseconds to nanoseconds
278 let nanosecond: i32 = digits.parse().unwrap_or(0) * 1000000;
279 // Scale to nanoseconds if fewer than 3 digits
280 match digits.len() {
281 1 => nanosecond * 100,
282 2 => nanosecond * 10,
283 _ => nanosecond,
284 }
285 },
286 ),
287 nom::combinator::success(0i32),
288 ))
289 .parse(input)
290}
291
292/// Parse a four-digit number (for year).
293///
294/// # Arguments
295///
296/// * `input` - The input string slice to parse
297///
298/// # Returns
299///
300/// A `IResult` containing the remaining unparsed input and parsed i16 number.
301fn parse_four_digit_number(input: &str) -> IResult<&str, i16> {
302 map_res(
303 take_while_m_n(4, 4, |c: char| c.is_ascii_digit()),
304 |s: &str| s.parse::<i16>(),
305 )
306 .parse(input)
307}
308
309/// Parse a two-digit number (for month, day, hour, minute, second).
310///
311/// # Arguments
312///
313/// * `input` - The input string slice to parse
314///
315/// # Returns
316///
317/// A `IResult` containing the remaining unparsed input and parsed i8 number.
318fn parse_two_digit_number(input: &str) -> IResult<&str, i8> {
319 map_res(
320 take_while_m_n(2, 2, |c: char| c.is_ascii_digit()),
321 |s: &str| s.parse::<i8>(),
322 )
323 .parse(input)
324}