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