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, Clone)]
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, Clone)]
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, Clone)]
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, Clone)]
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, Clone)]
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, Clone)]
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, Clone)]
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, Clone)]
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, 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, 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, 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, Clone, Copy)]
404pub enum WeekdayModifier {
405    Last,
406    Next,
407}
408
409/// AM/PM indicator for 12-hour time format.
410#[derive(Debug, PartialEq, 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, Clone, Copy, PartialEq)]
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    /// Format error message for invalid date with components
568    pub fn format_invalid_date(year: u16, month: u8, day: u8) -> String {
569        format!("Invalid date: {year}-{month}-{day}")
570    }
571
572    /// Format error message for invalid time with components
573    pub fn format_invalid_time(hour: u8, minute: u8, second: u8) -> String {
574        format!("Invalid time: {hour}:{minute}:{second}")
575    }
576
577    /// Format error message for invalid timezone offset
578    pub fn format_invalid_timezone_offset(hours: i8, minutes: u8) -> String {
579        format!("Invalid timezone offset: {hours}:{minutes}")
580    }
581}
582
583// ===== Time Utils Module =====
584
585pub mod time_utils {
586    //! Time conversion and calculation utilities
587
588    use crate::{
589        Meridiem, WeekdayModifier,
590        constants::{SECONDS_PER_HOUR, SECONDS_PER_MINUTE},
591    };
592
593    /// Convert 12-hour time format to 24-hour format
594    ///
595    /// # Examples
596    /// ```
597    /// use temps_core::{Meridiem, time_utils::convert_12_to_24_hour};
598    ///
599    /// assert_eq!(convert_12_to_24_hour(12, Some(&Meridiem::AM)), 0);  // 12 AM -> 0
600    /// assert_eq!(convert_12_to_24_hour(12, Some(&Meridiem::PM)), 12); // 12 PM -> 12
601    /// assert_eq!(convert_12_to_24_hour(3, Some(&Meridiem::PM)), 15);  // 3 PM -> 15
602    /// assert_eq!(convert_12_to_24_hour(14, None), 14);                // 24-hour format
603    /// ```
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    pub fn calculate_timezone_offset_seconds(hours: i8, minutes: u8) -> i32 {
628        let hour_seconds = (hours as i32).saturating_mul(SECONDS_PER_HOUR);
629        let minute_seconds = (minutes as i32).saturating_mul(SECONDS_PER_MINUTE);
630        hour_seconds.saturating_add(minute_seconds)
631    }
632
633    /// Calculate the day offset for weekday calculations
634    ///
635    /// Returns the number of days to add/subtract to reach the target weekday
636    ///
637    /// # Arguments
638    /// * `current_day_offset` - Current weekday as offset from Monday (0-6)
639    /// * `target_day_offset` - Target weekday as offset from Monday (0-6)
640    /// * `modifier` - Whether to get next, last, or closest occurrence
641    pub fn calculate_weekday_offset(
642        current_day_offset: i64,
643        target_day_offset: i64,
644        modifier: Option<WeekdayModifier>,
645    ) -> i64 {
646        let days_diff = target_day_offset - current_day_offset;
647
648        match modifier {
649            None => {
650                // Get the next occurrence (including today if it matches)
651                if days_diff >= 0 {
652                    days_diff
653                } else {
654                    7 + days_diff
655                }
656            }
657            Some(WeekdayModifier::Next) => {
658                // Next occurrence (not including today)
659                if days_diff > 0 {
660                    days_diff
661                } else {
662                    7 + days_diff
663                }
664            }
665            Some(WeekdayModifier::Last) => {
666                // Previous occurrence (not including today)
667                if days_diff < 0 {
668                    days_diff
669                } else {
670                    days_diff - 7
671                }
672            }
673        }
674    }
675}
676
677// ===== Common Parsing Module =====
678
679/// Common parsing utilities shared across language implementations.
680///
681/// This module contains parser functions that are shared between
682/// different language implementations, such as ISO datetime parsing
683/// and number parsing.
684pub mod common {
685
686    use super::*;
687
688    /// Parse a sequence of digits as an i64.
689    ///
690    /// Used for parsing numeric amounts in time expressions.
691    ///
692    /// # Examples
693    ///
694    /// This parses "123" -> 123, "5" -> 5, etc.
695    pub fn parse_digit_number(input: &mut &str) -> winnow::Result<i64> {
696        digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
697    }
698
699    /// Parse ISO 8601 datetime format.
700    ///
701    /// Supports various ISO datetime formats:
702    /// - Date only: `2024-01-15`
703    /// - Date and time: `2024-01-15T14:30:00`
704    /// - With timezone: `2024-01-15T14:30:00Z`
705    /// - With offset: `2024-01-15T14:30:00+02:00`
706    /// - With fractional seconds: `2024-01-15T14:30:00.123Z`
707    ///
708    /// # Examples
709    ///
710    /// ```ignore
711    /// // Parses: "2024-01-15T14:30:00Z"
712    /// // Into: TimeExpression::Absolute(AbsoluteTime { ... })
713    /// ```
714    pub fn parse_iso_datetime(input: &mut &str) -> winnow::Result<TimeExpression> {
715        // Parse date components
716        let year = parse_four_digit_number.parse_next(input)?;
717        '-'.parse_next(input)?;
718        let month = parse_two_digit_number.parse_next(input)?;
719        '-'.parse_next(input)?;
720        let day = parse_two_digit_number.parse_next(input)?;
721
722        // Parse optional time components
723        let time_part = opt((
724            one_of(['T', ' ']),
725            parse_two_digit_number, // hour
726            ':',
727            parse_two_digit_number, // minute
728            opt((
729                ':',
730                parse_two_digit_number, // second
731                opt((
732                    '.',
733                    digit1.try_map(|s: &str| {
734                        // Convert fractional seconds to nanoseconds
735                        let fraction = if s.len() > 9 { &s[..9] } else { s };
736
737                        // Parse the fraction and multiply by appropriate power of 10
738                        let parsed = fraction.parse::<u32>()?;
739                        let multiplier = 10_u32.pow(9 - fraction.len() as u32);
740                        Ok::<u32, std::num::ParseIntError>(parsed * multiplier)
741                    }),
742                )),
743            )),
744            opt(parse_timezone),
745        ))
746        .parse_next(input)?;
747
748        let (hour, minute, second, nanosecond, timezone) =
749            if let Some((_, h, _, m, sec_part, tz)) = time_part {
750                // We have time components
751                let hour = Some(h);
752                let minute = Some(m);
753
754                // Extract seconds and fractional seconds if present
755                let (second, nanosecond) = if let Some((_, s, frac)) = sec_part {
756                    (Some(s), frac.map(|(_, n)| n))
757                } else {
758                    (None, None)
759                };
760
761                (hour, minute, second, nanosecond, tz)
762            } else {
763                // Date only, no time components
764                (None, None, None, None, None)
765            };
766
767        Ok(TimeExpression::Absolute(AbsoluteTime {
768            year,
769            month,
770            day,
771            hour,
772            minute,
773            second,
774            nanosecond,
775            timezone,
776        }))
777    }
778
779    /// Parse timezone specification.
780    ///
781    /// Supports:
782    /// - `Z` for UTC
783    /// - `+HH:MM` or `-HH:MM` for offsets
784    /// - `+HH` or `-HH` (minutes optional)
785    fn parse_timezone(input: &mut &str) -> winnow::Result<Timezone> {
786        alt(("Z".map(|_| Timezone::Utc), parse_offset_timezone)).parse_next(input)
787    }
788
789    /// Parse timezone offset in +/-HH:MM format.
790    ///
791    /// Examples: `+02:00`, `-05:30`, `+09`
792    fn parse_offset_timezone(input: &mut &str) -> winnow::Result<Timezone> {
793        let sign = one_of(['+', '-']).parse_next(input)?;
794        let hours = parse_two_digit_number.parse_next(input)?;
795        let minutes = opt((':', parse_two_digit_number))
796            .parse_next(input)?
797            .map(|(_, m)| m)
798            .unwrap_or(0);
799
800        let hours = if sign == '+' {
801            hours as i8
802        } else {
803            -(hours as i8)
804        };
805
806        Ok(Timezone::Offset { hours, minutes })
807    }
808
809    /// Parse a two-digit number as u8.
810    ///
811    /// Used for parsing hours, minutes, days, months.
812    /// Accepts 1 or 2 digits (e.g., "5" or "05").
813    pub fn parse_two_digit_number(input: &mut &str) -> winnow::Result<u8> {
814        take_while(1..=2, |c: char| c.is_ascii_digit())
815            .try_map(|s: &str| s.parse::<u8>())
816            .parse_next(input)
817    }
818
819    /// Parse a four-digit number as u16.
820    ///
821    /// Used for parsing years.
822    /// Requires exactly 4 digits.
823    pub fn parse_four_digit_number(input: &mut &str) -> winnow::Result<u16> {
824        take_while(4..=4, |c: char| c.is_ascii_digit())
825            .try_map(|s: &str| s.parse::<u16>())
826            .parse_next(input)
827    }
828}
829
830// ===== Language Support =====
831
832/// Language-specific parser implementations.
833///
834/// Each submodule contains a parser for a specific language.
835/// All parsers implement the `LanguageParser` trait.
836pub mod language {
837    /// English language parser.
838    ///
839    /// Supports expressions like:
840    /// - "in 5 minutes", "3 days ago"
841    /// - "tomorrow at 3:30 pm"
842    /// - "next Monday", "last Friday"
843    pub mod english;
844
845    /// German language parser.
846    ///
847    /// Supports expressions like:
848    /// - "in 5 Minuten", "vor 3 Tagen"
849    /// - "morgen um 15:30"
850    /// - "nächsten Montag", "letzten Freitag"
851    pub mod german;
852}
853
854// ===== Main Parsing Function =====
855
856/// Parse a natural language time expression.
857///
858/// This is the main entry point for parsing time expressions. It takes
859/// a string input and a language, and returns a parsed `TimeExpression`.
860///
861/// # Arguments
862///
863/// * `input` - The natural language time expression to parse
864/// * `language` - The language to use for parsing
865///
866/// # Returns
867///
868/// Returns `Ok(TimeExpression)` if parsing succeeds, or `Err(TempsError)`
869/// if the input cannot be parsed.
870///
871/// # Examples
872///
873/// ```
874/// use temps_core::{parse, Language, TimeExpression};
875///
876/// // Parse English expressions
877/// let expr = parse("in 5 minutes", Language::English).unwrap();
878/// let expr = parse("tomorrow at 3:30 pm", Language::English).unwrap();
879/// let expr = parse("next Monday", Language::English).unwrap();
880///
881/// // Parse German expressions
882/// let expr = parse("in 5 Minuten", Language::German).unwrap();
883/// let expr = parse("morgen um 15:30", Language::German).unwrap();
884/// let expr = parse("nächsten Montag", Language::German).unwrap();
885///
886/// // Parse ISO datetime (works in any language)
887/// let expr = parse("2024-01-15T14:30:00Z", Language::English).unwrap();
888/// ```
889///
890/// # Supported Formats
891///
892/// ## Relative Time
893/// - "in 5 minutes", "5 minutes ago"
894/// - "in 2 hours", "an hour ago"
895/// - "in 3 days", "2 days ago"
896/// - "in a week", "2 weeks ago"
897/// - "in 6 months", "a month ago"
898/// - "in 2 years", "a year ago"
899///
900/// ## Day References
901/// - "today", "yesterday", "tomorrow"
902/// - "Monday", "Tuesday", etc.
903/// - "next Monday", "last Friday"
904///
905/// ## Times
906/// - "3:30 pm", "10:15 am"
907/// - "14:30", "09:00"
908///
909/// ## Dates
910/// - "15/03/2024", "31-12-2025"
911///
912/// ## Combined
913/// - "tomorrow at 3:30 pm"
914/// - "next Monday at 9:00 am"
915///
916/// ## ISO Format
917/// - "2024-01-15T14:30:00Z"
918/// - "2024-01-15T14:30:00+02:00"
919/// - "2024-01-15T14:30:00.123Z"
920pub fn parse(input: &str, language: Language) -> Result<TimeExpression> {
921    match language {
922        Language::English => language::english::EnglishParser.parse(input),
923        Language::German => language::german::GermanParser.parse(input),
924    }
925}