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