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}