1use winnow::{
2 ascii::digit1,
3 combinator::{alt, opt},
4 error::ContextError,
5 prelude::*,
6 token::{one_of, take_while},
7};
8
9#[derive(Debug, PartialEq, Clone)]
12pub enum TimeExpression {
13 Now,
14 Relative(RelativeTime),
15 Absolute(AbsoluteTime),
16 Day(DayReference),
17 Time(Time),
18 Date(StandardDate),
19 DayTime(DayTime),
20}
21
22#[derive(Debug, PartialEq, Clone)]
23pub struct RelativeTime {
24 pub amount: i64,
25 pub unit: TimeUnit,
26 pub direction: Direction,
27}
28
29#[derive(Debug, PartialEq, Clone)]
30pub struct AbsoluteTime {
31 pub year: u16,
32 pub month: u8,
33 pub day: u8,
34 pub hour: Option<u8>,
35 pub minute: Option<u8>,
36 pub second: Option<u8>,
37 pub nanosecond: Option<u32>,
38 pub timezone: Option<Timezone>,
39}
40
41#[derive(Debug, PartialEq, Clone)]
42pub enum Timezone {
43 Utc,
44 Offset { hours: i8, minutes: u8 },
45}
46
47#[derive(Debug, PartialEq, Clone)]
48pub enum DayReference {
49 Today,
50 Yesterday,
51 Tomorrow,
52 Weekday {
53 day: Weekday,
54 modifier: Option<WeekdayModifier>,
55 },
56}
57
58#[derive(Debug, PartialEq, Clone)]
59pub struct Time {
60 pub hour: u8,
61 pub minute: u8,
62 pub second: u8,
63 pub meridiem: Option<Meridiem>,
64}
65
66#[derive(Debug, PartialEq, Clone)]
67pub struct StandardDate {
68 pub day: u8,
69 pub month: u8,
70 pub year: u16,
71}
72
73#[derive(Debug, PartialEq, Clone)]
74pub struct DayTime {
75 pub day: DayReference,
76 pub time: Time,
77}
78
79#[derive(Debug, PartialEq, Clone, Copy)]
80pub enum TimeUnit {
81 Second,
82 Minute,
83 Hour,
84 Day,
85 Week,
86 Month,
87 Year,
88}
89
90#[derive(Debug, PartialEq, Clone, Copy)]
91pub enum Direction {
92 Past,
93 Future,
94}
95
96#[derive(Debug, PartialEq, Clone, Copy)]
97pub enum Weekday {
98 Monday,
99 Tuesday,
100 Wednesday,
101 Thursday,
102 Friday,
103 Saturday,
104 Sunday,
105}
106
107#[derive(Debug, PartialEq, Clone, Copy)]
108pub enum WeekdayModifier {
109 Last,
110 Next,
111}
112
113#[derive(Debug, PartialEq, Clone, Copy)]
114pub enum Meridiem {
115 AM,
116 PM,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq)]
120pub enum Language {
121 English,
122 German,
123}
124
125pub trait TimeParser {
128 type DateTime;
129 type Error;
130
131 fn now(&self) -> Self::DateTime;
132 fn parse_expression(&self, expr: TimeExpression) -> Result<Self::DateTime, Self::Error>;
133}
134
135pub trait LanguageParser {
136 fn parse<'a>(
137 &self,
138 input: &'a str,
139 ) -> Result<TimeExpression, winnow::error::ParseError<&'a str, ContextError>>;
140}
141
142pub mod constants {
145 pub const SECONDS_PER_HOUR: i32 = 3600;
149
150 pub const SECONDS_PER_MINUTE: i32 = 60;
152
153 pub const MINUTES_PER_HOUR: i32 = 60;
155
156 pub const HOURS_PER_DAY: i32 = 24;
158
159 pub const DAYS_PER_WEEK: i32 = 7;
161
162 pub const MONTHS_PER_YEAR: i32 = 12;
164}
165
166pub mod errors {
169 pub const ERR_MONTH_POSITIVE: &str = "Month amount must be a positive number";
173
174 pub const ERR_YEAR_POSITIVE: &str = "Year amount must be a positive number";
176
177 pub const ERR_DATE_CALC_INVALID: &str = "Date calculation resulted in invalid date";
179
180 pub const ERR_YEAR_OVERFLOW: &str = "Year calculation overflow";
182
183 pub const ERR_INVALID_DATE: &str = "Invalid date";
185
186 pub const ERR_INVALID_TIME: &str = "Invalid time";
188
189 pub const ERR_AMBIGUOUS_TIME: &str = "Ambiguous or invalid local time";
191
192 pub const ERR_MIDNIGHT_FAILED: &str = "Failed to create midnight time";
194
195 pub const ERR_DATE_CALC_ERROR: &str = "Date calculation error";
197
198 pub const ERR_TIMEZONE_CONVERSION: &str = "Timezone conversion error";
200
201 pub fn format_invalid_date(year: u16, month: u8, day: u8) -> String {
203 format!("Invalid date: {}-{}-{}", year, month, day)
204 }
205
206 pub fn format_invalid_time(hour: u8, minute: u8, second: u8) -> String {
208 format!("Invalid time: {}:{}:{}", hour, minute, second)
209 }
210
211 pub fn format_invalid_timezone_offset(hours: i8, minutes: u8) -> String {
213 format!("Invalid timezone offset: {}:{}", hours, minutes)
214 }
215}
216
217pub mod time_utils {
220 use crate::{
223 Meridiem, WeekdayModifier,
224 constants::{SECONDS_PER_HOUR, SECONDS_PER_MINUTE},
225 };
226
227 pub fn convert_12_to_24_hour(hour: u8, meridiem: Option<&Meridiem>) -> u8 {
239 match meridiem {
240 Some(Meridiem::AM) => {
241 if hour == 12 {
242 0
243 } else {
244 hour
245 }
246 }
247 Some(Meridiem::PM) => {
248 if hour == 12 {
249 hour
250 } else {
251 hour + 12
252 }
253 }
254 None => hour,
255 }
256 }
257
258 pub fn calculate_timezone_offset_seconds(hours: i8, minutes: u8) -> i32 {
262 let hour_seconds = (hours as i32).saturating_mul(SECONDS_PER_HOUR);
263 let minute_seconds = (minutes as i32).saturating_mul(SECONDS_PER_MINUTE);
264 hour_seconds.saturating_add(minute_seconds)
265 }
266
267 pub fn calculate_weekday_offset(
276 current_day_offset: i64,
277 target_day_offset: i64,
278 modifier: Option<WeekdayModifier>,
279 ) -> i64 {
280 let days_diff = target_day_offset - current_day_offset;
281
282 match modifier {
283 None => {
284 if days_diff >= 0 {
286 days_diff
287 } else {
288 7 + days_diff
289 }
290 }
291 Some(WeekdayModifier::Next) => {
292 if days_diff > 0 {
294 days_diff
295 } else {
296 7 + days_diff
297 }
298 }
299 Some(WeekdayModifier::Last) => {
300 if days_diff < 0 {
302 days_diff
303 } else {
304 days_diff - 7
305 }
306 }
307 }
308 }
309}
310
311pub mod common {
314 use super::*;
315
316 pub fn parse_digit_number(input: &mut &str) -> winnow::Result<i64> {
318 digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
319 }
320
321 pub fn parse_iso_datetime(input: &mut &str) -> winnow::Result<TimeExpression> {
323 let year = parse_four_digit_number.parse_next(input)?;
325 '-'.parse_next(input)?;
326 let month = parse_two_digit_number.parse_next(input)?;
327 '-'.parse_next(input)?;
328 let day = parse_two_digit_number.parse_next(input)?;
329
330 let time_part = opt((
332 one_of(['T', ' ']),
333 parse_two_digit_number, ':',
335 parse_two_digit_number, opt((
337 ':',
338 parse_two_digit_number, opt((
340 '.',
341 digit1.try_map(|s: &str| {
342 let fraction = if s.len() > 9 { &s[..9] } else { s };
344
345 let parsed = fraction.parse::<u32>()?;
347 let multiplier = 10_u32.pow(9 - fraction.len() as u32);
348 Ok::<u32, std::num::ParseIntError>(parsed * multiplier)
349 }),
350 )),
351 )),
352 opt(parse_timezone),
353 ))
354 .parse_next(input)?;
355
356 let (hour, minute, second, nanosecond, timezone) =
357 if let Some((_, h, _, m, sec_part, tz)) = time_part {
358 let hour = Some(h);
360 let minute = Some(m);
361
362 let (second, nanosecond) = if let Some((_, s, frac)) = sec_part {
364 (Some(s), frac.map(|(_, n)| n))
365 } else {
366 (None, None)
367 };
368
369 (hour, minute, second, nanosecond, tz)
370 } else {
371 (None, None, None, None, None)
373 };
374
375 Ok(TimeExpression::Absolute(AbsoluteTime {
376 year,
377 month,
378 day,
379 hour,
380 minute,
381 second,
382 nanosecond,
383 timezone,
384 }))
385 }
386
387 fn parse_timezone(input: &mut &str) -> winnow::Result<Timezone> {
389 alt(("Z".map(|_| Timezone::Utc), parse_offset_timezone)).parse_next(input)
390 }
391
392 fn parse_offset_timezone(input: &mut &str) -> winnow::Result<Timezone> {
394 let sign = one_of(['+', '-']).parse_next(input)?;
395 let hours = parse_two_digit_number.parse_next(input)?;
396 let minutes = opt((':', parse_two_digit_number))
397 .parse_next(input)?
398 .map(|(_, m)| m)
399 .unwrap_or(0);
400
401 let hours = if sign == '+' {
402 hours as i8
403 } else {
404 -(hours as i8)
405 };
406
407 Ok(Timezone::Offset { hours, minutes })
408 }
409
410 pub fn parse_two_digit_number(input: &mut &str) -> winnow::Result<u8> {
412 take_while(1..=2, |c: char| c.is_ascii_digit())
413 .try_map(|s: &str| s.parse::<u8>())
414 .parse_next(input)
415 }
416
417 pub fn parse_four_digit_number(input: &mut &str) -> winnow::Result<u16> {
419 take_while(4..=4, |c: char| c.is_ascii_digit())
420 .try_map(|s: &str| s.parse::<u16>())
421 .parse_next(input)
422 }
423}
424
425pub mod language {
428 pub mod english;
429 pub mod german;
430}
431
432pub fn parse(
435 input: &str,
436 language: Language,
437) -> Result<TimeExpression, winnow::error::ParseError<&str, winnow::error::ContextError>> {
438 match language {
439 Language::English => language::english::EnglishParser.parse(input),
440 Language::German => language::german::GermanParser.parse(input),
441 }
442}