Skip to main content

temps_core/
lib.rs

1//! # temps-core
2//!
3//! Core functionality for parsing human-readable time expressions.
4//!
5//! This crate provides the fundamental types and traits for parsing natural language
6//! time expressions like "in 5 minutes", "yesterday at 3pm", or "next Monday".
7//! It is designed to be backend-agnostic, allowing different datetime libraries
8//! (chrono, jiff, etc.) to implement the parsing logic.
9//!
10//! ## Overview
11//!
12//! The crate consists of several key components:
13//!
14//! - **Types**: Core data structures representing different time expressions
15//! - **Traits**: Interfaces for implementing time parsing with different backends
16//! - **Parsers**: Language-specific parsers (English and German)
17//! - **Utilities**: Helper functions for time calculations and conversions
18//!
19//! ## Example
20//!
21//! ```
22//! use temps_core::{parse, Language, TimeExpression};
23//!
24//! // Parse a relative time expression
25//! let expr = parse("in 5 minutes", Language::English).unwrap();
26//! match expr {
27//!     TimeExpression::Relative(rel) => {
28//!         println!("Amount: {}, Unit: {:?}", rel.amount, rel.unit);
29//!     }
30//!     _ => {}
31//! }
32//!
33//! // Parse with German language
34//! let expr = parse("in 5 Minuten", Language::German).unwrap();
35//! ```
36//!
37//! ## Supported Languages
38//!
39//! - English
40//! - German
41//!
42//! ## Error Handling
43//!
44//! All parsing operations return a `Result<T, TempsError>` where `TempsError`
45//! provides detailed information about what went wrong during parsing or
46//! date calculations.
47
48use winnow::{
49    ascii::digit1,
50    combinator::{alt, opt},
51    prelude::*,
52    token::{one_of, take_while},
53};
54
55// ===== Error Module =====
56pub mod error;
57pub use error::{Result, TempsError};
58
59// ===== Core Types =====
60
61/// Represents a parsed time expression.
62///
63/// This is the main output type of the parsing functions. It can represent
64/// various forms of time expressions from natural language input.
65///
66/// # Examples
67///
68/// ```
69/// use temps_core::{parse, Language, TimeExpression};
70///
71/// // "now" -> TimeExpression::Now
72/// // "in 5 minutes" -> TimeExpression::Relative(...)
73/// // "2024-01-15T14:30:00Z" -> TimeExpression::Absolute(...)
74/// // "tomorrow" -> TimeExpression::Day(...)
75/// // "3:30 pm" -> TimeExpression::Time(...)
76/// // "tomorrow at 3:30 pm" -> TimeExpression::DayTime(...)
77/// ```
78#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
79pub enum TimeExpression {
80    /// The current moment in time (e.g., "now", "jetzt")
81    Now,
82    /// A time relative to now (e.g., "in 5 minutes", "3 days ago")
83    Relative(RelativeTime),
84    /// An absolute date/time (e.g., "2024-01-15T14:30:00Z")
85    Absolute(AbsoluteTime),
86    /// A day reference (e.g., "tomorrow", "next Monday")
87    Day(DayReference),
88    /// A time of day (e.g., "3:30 pm", "14:30")
89    Time(Time),
90    /// A calendar date (e.g., "15/03/2024", "31-12-2025")
91    Date(StandardDate),
92    /// A day with a specific time (e.g., "tomorrow at 3:30 pm")
93    DayTime(DayTime),
94}
95
96/// Represents a time relative to the current moment.
97///
98/// # Examples
99///
100/// ```
101/// use temps_core::{RelativeTime, TimeUnit, Direction};
102///
103/// // "in 5 minutes"
104/// let future = RelativeTime {
105///     amount: 5,
106///     unit: TimeUnit::Minute,
107///     direction: Direction::Future,
108/// };
109///
110/// // "3 days ago"
111/// let past = RelativeTime {
112///     amount: 3,
113///     unit: TimeUnit::Day,
114///     direction: Direction::Past,
115/// };
116/// ```
117#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
118pub struct RelativeTime {
119    /// The numeric amount (e.g., 5 in "5 minutes")
120    pub amount: i64,
121    /// The time unit (second, minute, hour, etc.)
122    pub unit: TimeUnit,
123    /// Whether this is in the past or future
124    pub direction: Direction,
125}
126
127/// Represents an absolute date and time.
128///
129/// This type can represent various levels of precision, from just a date
130/// to a full timestamp with timezone and nanosecond precision.
131///
132/// # Examples
133///
134/// ```
135/// use temps_core::{AbsoluteTime, Timezone};
136///
137/// // Date only: "2024-01-15"
138/// let date_only = AbsoluteTime {
139///     year: 2024,
140///     month: 1,
141///     day: 15,
142///     hour: None,
143///     minute: None,
144///     second: None,
145///     nanosecond: None,
146///     timezone: None,
147/// };
148///
149/// // Full timestamp: "2024-01-15T14:30:00Z"
150/// let full_timestamp = AbsoluteTime {
151///     year: 2024,
152///     month: 1,
153///     day: 15,
154///     hour: Some(14),
155///     minute: Some(30),
156///     second: Some(0),
157///     nanosecond: None,
158///     timezone: Some(Timezone::Utc),
159/// };
160/// ```
161#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
162pub struct AbsoluteTime {
163    /// The year (e.g., 2024)
164    pub year: u16,
165    /// The month (1-12)
166    pub month: u8,
167    /// The day of month (1-31)
168    pub day: u8,
169    /// The hour (0-23), if specified
170    pub hour: Option<u8>,
171    /// The minute (0-59), if specified
172    pub minute: Option<u8>,
173    /// The second (0-59), if specified
174    pub second: Option<u8>,
175    /// The nanosecond (0-999999999), if specified
176    pub nanosecond: Option<u32>,
177    /// The timezone, if specified
178    pub timezone: Option<Timezone>,
179}
180
181/// Represents a timezone specification.
182///
183/// # Examples
184///
185/// ```
186/// use temps_core::Timezone;
187///
188/// // UTC timezone ("Z")
189/// let utc = Timezone::Utc;
190///
191/// // Offset timezone ("+02:00")
192/// let offset = Timezone::Offset { hours: 2, minutes: 0 };
193///
194/// // Negative offset ("-05:30")
195/// let negative = Timezone::Offset { hours: -5, minutes: 30 };
196/// ```
197#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
198pub enum Timezone {
199    /// UTC timezone (represented as "Z" in ISO format)
200    Utc,
201    /// Timezone offset from UTC
202    Offset {
203        /// Hours offset (-12 to +14)
204        hours: i8,
205        /// Minutes offset (0-59)
206        minutes: u8,
207    },
208}
209
210/// Represents a reference to a specific day.
211///
212/// # Examples
213///
214/// ```
215/// use temps_core::{DayReference, Weekday, WeekdayModifier};
216///
217/// // "today"
218/// let today = DayReference::Today;
219///
220/// // "next Monday"
221/// let next_monday = DayReference::Weekday {
222///     day: Weekday::Monday,
223///     modifier: Some(WeekdayModifier::Next),
224/// };
225///
226/// // "Friday" (upcoming Friday)
227/// let friday = DayReference::Weekday {
228///     day: Weekday::Friday,
229///     modifier: None,
230/// };
231/// ```
232#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
233pub enum DayReference {
234    /// Today's date
235    Today,
236    /// Yesterday's date
237    Yesterday,
238    /// Tomorrow's date
239    Tomorrow,
240    /// A specific weekday
241    Weekday {
242        /// The day of the week
243        day: Weekday,
244        /// Optional modifier (next/last)
245        modifier: Option<WeekdayModifier>,
246    },
247}
248
249/// Represents a time of day.
250///
251/// Can represent both 12-hour (with AM/PM) and 24-hour formats.
252///
253/// # Examples
254///
255/// ```
256/// use temps_core::{Time, Meridiem};
257///
258/// // "3:30 PM"
259/// let afternoon = Time {
260///     hour: 3,
261///     minute: 30,
262///     second: 0,
263///     meridiem: Some(Meridiem::PM),
264/// };
265///
266/// // "14:30" (24-hour format)
267/// let military = Time {
268///     hour: 14,
269///     minute: 30,
270///     second: 0,
271///     meridiem: None,
272/// };
273/// ```
274#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
275pub struct Time {
276    /// Hour (0-23 for 24-hour format, 1-12 for 12-hour format)
277    pub hour: u8,
278    /// Minute (0-59)
279    pub minute: u8,
280    /// Second (0-59)
281    pub second: u8,
282    /// AM/PM indicator for 12-hour format
283    pub meridiem: Option<Meridiem>,
284}
285
286/// Represents a calendar date.
287///
288/// Used for parsing date formats like "15/03/2024" or "31-12-2025".
289///
290/// # Examples
291///
292/// ```
293/// use temps_core::StandardDate;
294///
295/// // "15/03/2024"
296/// let date = StandardDate {
297///     day: 15,
298///     month: 3,
299///     year: 2024,
300/// };
301/// ```
302#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
303pub struct StandardDate {
304    /// Day of month (1-31)
305    pub day: u8,
306    /// Month (1-12)
307    pub month: u8,
308    /// Year (e.g., 2024)
309    pub year: u16,
310}
311
312/// Represents a combination of a day reference and a specific time.
313///
314/// Used for expressions like "tomorrow at 3:30 pm" or "next Monday at 9:00 am".
315///
316/// # Examples
317///
318/// ```
319/// use temps_core::{DayTime, DayReference, Time, Meridiem};
320///
321/// // "tomorrow at 3:30 pm"
322/// let tomorrow_afternoon = DayTime {
323///     day: DayReference::Tomorrow,
324///     time: Time {
325///         hour: 3,
326///         minute: 30,
327///         second: 0,
328///         meridiem: Some(Meridiem::PM),
329///     },
330/// };
331/// ```
332#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
333pub struct DayTime {
334    /// The day reference
335    pub day: DayReference,
336    /// The specific time on that day
337    pub time: Time,
338}
339
340/// Units of time used in relative expressions.
341///
342/// # Examples
343///
344/// ```
345/// use temps_core::TimeUnit;
346///
347/// // Used in expressions like:
348/// // "5 seconds", "10 minutes", "2 hours", "3 days",
349/// // "1 week", "6 months", "2 years"
350/// ```
351#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
352pub enum TimeUnit {
353    Second,
354    Minute,
355    Hour,
356    Day,
357    Week,
358    Month,
359    Year,
360}
361
362/// Direction of time relative to now.
363///
364/// # Examples
365///
366/// ```
367/// use temps_core::Direction;
368///
369/// // "5 minutes ago" -> Direction::Past
370/// // "in 5 minutes" -> Direction::Future
371/// ```
372#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
373pub enum Direction {
374    Past,
375    Future,
376}
377
378/// Days of the week.
379///
380/// Used in expressions like "next Monday" or "last Friday".
381#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
382pub enum Weekday {
383    Monday,
384    Tuesday,
385    Wednesday,
386    Thursday,
387    Friday,
388    Saturday,
389    Sunday,
390}
391
392/// Modifiers for weekday references.
393///
394/// # Examples
395///
396/// ```
397/// use temps_core::WeekdayModifier;
398///
399/// // "last Monday" -> WeekdayModifier::Last
400/// // "next Friday" -> WeekdayModifier::Next
401/// // "Monday" (no modifier) -> finds the next occurrence
402/// ```
403#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
404pub enum WeekdayModifier {
405    Last,
406    Next,
407}
408
409/// AM/PM indicator for 12-hour time format.
410#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
411pub enum Meridiem {
412    AM,
413    PM,
414}
415
416/// Supported languages for parsing time expressions.
417///
418/// # Examples
419///
420/// ```
421/// use temps_core::{parse, Language};
422///
423/// // Parse English
424/// let expr = parse("in 5 minutes", Language::English);
425///
426/// // Parse German
427/// let expr = parse("in 5 Minuten", Language::German);
428/// ```
429#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
430pub enum Language {
431    English,
432    German,
433}
434
435// ===== Traits =====
436
437/// Trait for implementing time parsing with a specific datetime backend.
438///
439/// This trait should be implemented by datetime libraries (chrono, jiff, etc.)
440/// to provide the actual time calculation logic.
441///
442/// # Examples
443///
444/// ```
445/// use temps_core::{TimeParser, TimeExpression, Result};
446///
447/// struct MyTimeParser;
448///
449/// impl TimeParser for MyTimeParser {
450///     type DateTime = String; // Your datetime type
451///
452///     fn now(&self) -> Self::DateTime {
453///         "2024-01-15T14:30:00Z".to_string()
454///     }
455///
456///     fn parse_expression(&self, expr: TimeExpression) -> Result<Self::DateTime> {
457///         // Implementation here
458///         Ok(self.now())
459///     }
460/// }
461/// ```
462pub trait TimeParser {
463    /// The datetime type used by this implementation
464    type DateTime;
465
466    /// Get the current date and time
467    fn now(&self) -> Self::DateTime;
468
469    /// Parse a time expression into a concrete datetime
470    ///
471    /// # Errors
472    ///
473    /// Returns `TempsError` if:
474    /// - Date calculation results in an invalid date
475    /// - Arithmetic overflow occurs
476    /// - The backend library returns an error
477    fn parse_expression(&self, expr: TimeExpression) -> Result<Self::DateTime>;
478}
479
480/// Trait for implementing language-specific parsers.
481///
482/// This trait is implemented by language modules to provide
483/// natural language parsing for different languages.
484///
485/// # Examples
486///
487/// ```
488/// use temps_core::{LanguageParser, TimeExpression, Result};
489///
490/// struct MyLanguageParser;
491///
492/// impl LanguageParser for MyLanguageParser {
493///     fn parse(&self, input: &str) -> Result<TimeExpression> {
494///         // Parse language-specific input
495///         Ok(TimeExpression::Now)
496///     }
497/// }
498/// ```
499pub trait LanguageParser {
500    /// Parse a natural language time expression
501    ///
502    /// # Errors
503    ///
504    /// Returns `TempsError::ParseError` if the input cannot be parsed
505    fn parse(&self, input: &str) -> Result<TimeExpression>;
506}
507
508// ===== Constants Module =====
509
510pub mod constants {
511    //! Common constants used across the temps library
512
513    /// Number of seconds in one hour
514    pub const SECONDS_PER_HOUR: i32 = 3600;
515
516    /// Number of seconds in one minute  
517    pub const SECONDS_PER_MINUTE: i32 = 60;
518
519    /// Number of minutes in one hour
520    pub const MINUTES_PER_HOUR: i32 = 60;
521
522    /// Number of hours in one day
523    pub const HOURS_PER_DAY: i32 = 24;
524
525    /// Number of days in one week
526    pub const DAYS_PER_WEEK: i32 = 7;
527
528    /// Number of months in one year
529    pub const MONTHS_PER_YEAR: i32 = 12;
530}
531
532// ===== Errors Module =====
533
534pub mod errors {
535    //! Common error messages and error handling utilities
536
537    /// Error message for when month amount must be positive
538    pub const ERR_MONTH_POSITIVE: &str = "Month amount must be a positive number";
539
540    /// Error message for when year amount must be positive
541    pub const ERR_YEAR_POSITIVE: &str = "Year amount must be a positive number";
542
543    /// Error message for invalid date calculation
544    pub const ERR_DATE_CALC_INVALID: &str = "Date calculation resulted in invalid date";
545
546    /// Error message for year calculation overflow
547    pub const ERR_YEAR_OVERFLOW: &str = "Year calculation overflow";
548
549    /// Error message for invalid date
550    pub const ERR_INVALID_DATE: &str = "Invalid date";
551
552    /// Error message for invalid time
553    pub const ERR_INVALID_TIME: &str = "Invalid time";
554
555    /// Error message for ambiguous local time
556    pub const ERR_AMBIGUOUS_TIME: &str = "Ambiguous or invalid local time";
557
558    /// Error message for failed midnight time creation
559    pub const ERR_MIDNIGHT_FAILED: &str = "Failed to create midnight time";
560
561    /// Error message for date calculation errors
562    pub const ERR_DATE_CALC_ERROR: &str = "Date calculation error";
563
564    /// Error message for timezone conversion errors
565    pub const ERR_TIMEZONE_CONVERSION: &str = "Timezone conversion error";
566
567    /// Error message for negative relative amounts
568    pub const ERR_RELATIVE_AMOUNT_NON_NEGATIVE: &str = "Relative amount must be non-negative";
569
570    /// Format error message for invalid date with components
571    #[must_use]
572    pub fn format_invalid_date(year: u16, month: u8, day: u8) -> String {
573        format!("Invalid date: {year}-{month}-{day}")
574    }
575
576    /// Format error message for invalid time with components
577    #[must_use]
578    pub fn format_invalid_time(hour: u8, minute: u8, second: u8) -> String {
579        format!("Invalid time: {hour}:{minute}:{second}")
580    }
581
582    /// Format error message for invalid timezone offset
583    #[must_use]
584    pub fn format_invalid_timezone_offset(hours: i8, minutes: u8) -> String {
585        format!("Invalid timezone offset: {hours}:{minutes}")
586    }
587}
588
589// ===== Time Utils Module =====
590
591pub mod time_utils {
592    //! Time conversion and calculation utilities
593
594    use crate::{
595        Meridiem, Timezone, WeekdayModifier,
596        constants::{SECONDS_PER_HOUR, SECONDS_PER_MINUTE},
597    };
598
599    /// Convert 12-hour time format to 24-hour format
600    ///
601    /// # Examples
602    /// ```
603    /// use temps_core::{Meridiem, time_utils::convert_12_to_24_hour};
604    ///
605    /// assert_eq!(convert_12_to_24_hour(12, Some(&Meridiem::AM)), 0);  // 12 AM -> 0
606    /// assert_eq!(convert_12_to_24_hour(12, Some(&Meridiem::PM)), 12); // 12 PM -> 12
607    /// assert_eq!(convert_12_to_24_hour(3, Some(&Meridiem::PM)), 15);  // 3 PM -> 15
608    /// assert_eq!(convert_12_to_24_hour(14, None), 14);                // 24-hour format
609    /// ```
610    #[must_use]
611    pub fn convert_12_to_24_hour(hour: u8, meridiem: Option<&Meridiem>) -> u8 {
612        match meridiem {
613            Some(Meridiem::AM) => {
614                if hour == 12 {
615                    0
616                } else {
617                    hour
618                }
619            }
620            Some(Meridiem::PM) => {
621                if hour == 12 {
622                    hour
623                } else {
624                    hour + 12
625                }
626            }
627            None => hour,
628        }
629    }
630
631    /// Calculate total seconds for a timezone offset
632    ///
633    /// Uses saturating arithmetic to prevent overflow
634    #[must_use]
635    pub fn calculate_timezone_offset_seconds(hours: i8, minutes: u8) -> i32 {
636        let hour_seconds = i32::from(hours).saturating_mul(SECONDS_PER_HOUR);
637        let minute_seconds = i32::from(minutes).saturating_mul(SECONDS_PER_MINUTE);
638        let minute_seconds = if hours < 0 {
639            -minute_seconds
640        } else {
641            minute_seconds
642        };
643
644        hour_seconds.saturating_add(minute_seconds)
645    }
646
647    /// Check whether the date components form a real calendar date.
648    #[must_use]
649    pub fn is_valid_calendar_date(year: u16, month: u8, day: u8) -> bool {
650        let days_in_month = match month {
651            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
652            4 | 6 | 9 | 11 => 30,
653            2 if is_leap_year(year) => 29,
654            2 => 28,
655            _ => return false,
656        };
657
658        (1..=days_in_month).contains(&day)
659    }
660
661    /// Check whether the time components form a valid 24-hour clock time.
662    #[must_use]
663    pub fn is_valid_24_hour_time(hour: u8, minute: u8, second: u8) -> bool {
664        hour <= 23 && minute <= 59 && second <= 59
665    }
666
667    /// Check whether time components are valid for either 24-hour or AM/PM notation.
668    #[must_use]
669    pub fn is_valid_time(hour: u8, minute: u8, second: u8, meridiem: Option<Meridiem>) -> bool {
670        match meridiem {
671            Some(_) => (1..=12).contains(&hour) && minute <= 59 && second <= 59,
672            None => is_valid_24_hour_time(hour, minute, second),
673        }
674    }
675
676    /// Check whether a timezone offset is in the supported UTC-12:00..=UTC+14:00 range.
677    #[must_use]
678    pub fn is_valid_timezone_offset(offset: Timezone) -> bool {
679        match offset {
680            Timezone::Utc => true,
681            Timezone::Offset { hours, minutes } => {
682                minutes <= 59
683                    && match hours {
684                        -12 => minutes == 0,
685                        -11..=13 => true,
686                        14 => minutes == 0,
687                        _ => false,
688                    }
689            }
690        }
691    }
692
693    /// Calculate the day offset for weekday calculations
694    ///
695    /// Returns the number of days to add/subtract to reach the target weekday
696    ///
697    /// # Arguments
698    /// * `current_day_offset` - Current weekday as offset from Monday (0-6)
699    /// * `target_day_offset` - Target weekday as offset from Monday (0-6)
700    /// * `modifier` - Whether to get next, last, or closest occurrence
701    #[must_use]
702    pub fn calculate_weekday_offset(
703        current_day_offset: i64,
704        target_day_offset: i64,
705        modifier: Option<WeekdayModifier>,
706    ) -> i64 {
707        let days_diff = target_day_offset - current_day_offset;
708
709        match modifier {
710            None => {
711                // Get the next occurrence (including today if it matches)
712                if days_diff >= 0 {
713                    days_diff
714                } else {
715                    7 + days_diff
716                }
717            }
718            Some(WeekdayModifier::Next) => {
719                // Next occurrence (not including today)
720                if days_diff > 0 {
721                    days_diff
722                } else {
723                    7 + days_diff
724                }
725            }
726            Some(WeekdayModifier::Last) => {
727                // Previous occurrence (not including today)
728                if days_diff < 0 {
729                    days_diff
730                } else {
731                    days_diff - 7
732                }
733            }
734        }
735    }
736
737    #[must_use]
738    fn is_leap_year(year: u16) -> bool {
739        year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
740    }
741}
742
743// ===== Common Parsing Module =====
744
745/// Common parsing utilities shared across language implementations.
746///
747/// This module contains parser functions that are shared between
748/// different language implementations, such as ISO datetime parsing
749/// and number parsing.
750pub mod common {
751
752    use super::*;
753
754    /// Parse a sequence of digits as an i64.
755    ///
756    /// Used for parsing numeric amounts in time expressions.
757    ///
758    /// # Examples
759    ///
760    /// This parses "123" -> 123, "5" -> 5, etc.
761    pub fn parse_digit_number(input: &mut &str) -> winnow::Result<i64> {
762        digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
763    }
764
765    /// Parse ISO 8601 datetime format.
766    ///
767    /// Supports various ISO datetime formats:
768    /// - Date only: `2024-01-15`
769    /// - Date and time: `2024-01-15T14:30:00`
770    /// - With timezone: `2024-01-15T14:30:00Z`
771    /// - With offset: `2024-01-15T14:30:00+02:00`
772    /// - With fractional seconds: `2024-01-15T14:30:00.123Z`
773    ///
774    /// # Examples
775    ///
776    /// ```ignore
777    /// // Parses: "2024-01-15T14:30:00Z"
778    /// // Into: TimeExpression::Absolute(AbsoluteTime { ... })
779    /// ```
780    pub fn parse_iso_datetime(input: &mut &str) -> winnow::Result<TimeExpression> {
781        // Parse date components
782        let (year, month, day) = (
783            parse_four_digit_number,
784            '-',
785            parse_two_digit_number,
786            '-',
787            parse_two_digit_number,
788        )
789            .verify_map(|(year, _, month, _, day)| {
790                time_utils::is_valid_calendar_date(year, month, day).then_some((year, month, day))
791            })
792            .parse_next(input)?;
793
794        // Parse optional time components
795        let time_part = opt((
796            one_of(['T', ' ']),
797            parse_two_digit_number, // hour
798            ':',
799            parse_two_digit_number, // minute
800            opt((
801                ':',
802                parse_two_digit_number, // second
803                opt((
804                    '.',
805                    digit1.try_map(|s: &str| {
806                        // Convert fractional seconds to nanoseconds
807                        let fraction = if s.len() > 9 { &s[..9] } else { s };
808
809                        // Parse the fraction and multiply by appropriate power of 10
810                        let parsed = fraction.parse::<u32>()?;
811                        let fraction_len = u32::try_from(fraction.len())
812                            .expect("fraction length is capped at 9 digits");
813                        let multiplier = 10_u32.pow(9 - fraction_len);
814                        Ok::<u32, std::num::ParseIntError>(parsed * multiplier)
815                    }),
816                )),
817            )),
818            opt(parse_timezone),
819        )
820            .verify_map(|(_, h, _, m, sec_part, tz)| {
821                let second = sec_part.map_or(0, |(_, s, _)| s);
822                time_utils::is_valid_24_hour_time(h, m, second).then_some((h, m, sec_part, tz))
823            }))
824        .parse_next(input)?;
825
826        let (hour, minute, second, nanosecond, timezone) =
827            if let Some((h, m, sec_part, tz)) = time_part {
828                // We have time components
829                let hour = Some(h);
830                let minute = Some(m);
831
832                // Extract seconds and fractional seconds if present
833                let (second, nanosecond) = if let Some((_, s, frac)) = sec_part {
834                    (Some(s), frac.map(|(_, n)| n))
835                } else {
836                    (None, None)
837                };
838
839                (hour, minute, second, nanosecond, tz)
840            } else {
841                // Date only, no time components
842                (None, None, None, None, None)
843            };
844
845        Ok(TimeExpression::Absolute(AbsoluteTime {
846            year,
847            month,
848            day,
849            hour,
850            minute,
851            second,
852            nanosecond,
853            timezone,
854        }))
855    }
856
857    /// Parse timezone specification.
858    ///
859    /// Supports:
860    /// - `Z` for UTC
861    /// - `+HH:MM` or `-HH:MM` for offsets
862    /// - `+HH` or `-HH` (minutes optional)
863    fn parse_timezone(input: &mut &str) -> winnow::Result<Timezone> {
864        alt(("Z".map(|_| Timezone::Utc), parse_offset_timezone)).parse_next(input)
865    }
866
867    /// Parse timezone offset in +/-HH:MM format.
868    ///
869    /// Examples: `+02:00`, `-05:30`, `+09`
870    fn parse_offset_timezone(input: &mut &str) -> winnow::Result<Timezone> {
871        (
872            one_of(['+', '-']),
873            parse_two_digit_number,
874            opt((':', parse_two_digit_number)).map(|minutes| minutes.map_or(0, |(_, m)| m)),
875        )
876            .verify_map(|(sign, hours, minutes)| {
877                let hours = i8::try_from(hours).ok()?;
878                let signed_hours = if sign == '+' { hours } else { -hours };
879                let offset = Timezone::Offset {
880                    hours: signed_hours,
881                    minutes,
882                };
883
884                let can_represent_offset = !(sign == '-' && hours == 0 && minutes > 0);
885
886                (can_represent_offset && time_utils::is_valid_timezone_offset(offset))
887                    .then_some(offset)
888            })
889            .parse_next(input)
890    }
891
892    /// Parse a two-digit number as u8.
893    ///
894    /// Used for parsing hours, minutes, days, months.
895    /// Accepts 1 or 2 digits (e.g., "5" or "05").
896    pub fn parse_two_digit_number(input: &mut &str) -> winnow::Result<u8> {
897        take_while(1..=2, |c: char| c.is_ascii_digit())
898            .try_map(|s: &str| s.parse::<u8>())
899            .parse_next(input)
900    }
901
902    /// Parse a four-digit number as u16.
903    ///
904    /// Used for parsing years.
905    /// Requires exactly 4 digits.
906    pub fn parse_four_digit_number(input: &mut &str) -> winnow::Result<u16> {
907        take_while(4..=4, |c: char| c.is_ascii_digit())
908            .try_map(|s: &str| s.parse::<u16>())
909            .parse_next(input)
910    }
911}
912
913// ===== Language Support =====
914
915/// Language-specific parser implementations.
916///
917/// Each submodule contains a parser for a specific language.
918/// All parsers implement the `LanguageParser` trait.
919pub mod language {
920    /// English language parser.
921    ///
922    /// Supports expressions like:
923    /// - "in 5 minutes", "3 days ago"
924    /// - "tomorrow at 3:30 pm"
925    /// - "next Monday", "last Friday"
926    pub mod english;
927
928    /// German language parser.
929    ///
930    /// Supports expressions like:
931    /// - "in 5 Minuten", "vor 3 Tagen"
932    /// - "morgen um 15:30"
933    /// - "nächsten Montag", "letzten Freitag"
934    pub mod german;
935}
936
937// ===== Main Parsing Function =====
938
939/// Parse a natural language time expression.
940///
941/// This is the main entry point for parsing time expressions. It takes
942/// a string input and a language, and returns a parsed `TimeExpression`.
943///
944/// # Arguments
945///
946/// * `input` - The natural language time expression to parse
947/// * `language` - The language to use for parsing
948///
949/// # Returns
950///
951/// Returns `Ok(TimeExpression)` if parsing succeeds, or `Err(TempsError)`
952/// if the input cannot be parsed.
953///
954/// # Examples
955///
956/// ```
957/// use temps_core::{parse, Language, TimeExpression};
958///
959/// // Parse English expressions
960/// let expr = parse("in 5 minutes", Language::English).unwrap();
961/// let expr = parse("tomorrow at 3:30 pm", Language::English).unwrap();
962/// let expr = parse("next Monday", Language::English).unwrap();
963///
964/// // Parse German expressions
965/// let expr = parse("in 5 Minuten", Language::German).unwrap();
966/// let expr = parse("morgen um 15:30", Language::German).unwrap();
967/// let expr = parse("nächsten Montag", Language::German).unwrap();
968///
969/// // Parse ISO datetime (works in any language)
970/// let expr = parse("2024-01-15T14:30:00Z", Language::English).unwrap();
971/// ```
972///
973/// # Supported Formats
974///
975/// ## Relative Time
976/// - "in 5 minutes", "5 minutes ago"
977/// - "in 2 hours", "an hour ago"
978/// - "in 3 days", "2 days ago"
979/// - "in a week", "2 weeks ago"
980/// - "in 6 months", "a month ago"
981/// - "in 2 years", "a year ago"
982///
983/// ## Day References
984/// - "today", "yesterday", "tomorrow"
985/// - "Monday", "Tuesday", etc.
986/// - "next Monday", "last Friday"
987///
988/// ## Times
989/// - "3:30 pm", "10:15 am"
990/// - "14:30", "09:00"
991///
992/// ## Dates
993/// - "15/03/2024", "31-12-2025"
994///
995/// ## Combined
996/// - "tomorrow at 3:30 pm"
997/// - "next Monday at 9:00 am"
998///
999/// ## ISO Format
1000/// - "2024-01-15T14:30:00Z"
1001/// - "2024-01-15T14:30:00+02:00"
1002/// - "2024-01-15T14:30:00.123Z"
1003pub fn parse(input: &str, language: Language) -> Result<TimeExpression> {
1004    match language {
1005        Language::English => language::english::EnglishParser.parse(input),
1006        Language::German => language::german::GermanParser.parse(input),
1007    }
1008}