use winnow::{
ascii::digit1,
combinator::{alt, opt},
prelude::*,
token::{one_of, take_while},
};
pub mod error;
pub use error::{Result, TempsError};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum TimeExpression {
Now,
Relative(RelativeTime),
Absolute(AbsoluteTime),
Day(DayReference),
Time(Time),
Date(StandardDate),
DayTime(DayTime),
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct RelativeTime {
pub amount: i64,
pub unit: TimeUnit,
pub direction: Direction,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct AbsoluteTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: Option<u8>,
pub minute: Option<u8>,
pub second: Option<u8>,
pub nanosecond: Option<u32>,
pub timezone: Option<Timezone>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Timezone {
Utc,
Offset {
hours: i8,
minutes: u8,
},
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum DayReference {
Today,
Yesterday,
Tomorrow,
Weekday {
day: Weekday,
modifier: Option<WeekdayModifier>,
},
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct Time {
pub hour: u8,
pub minute: u8,
pub second: u8,
pub meridiem: Option<Meridiem>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct StandardDate {
pub day: u8,
pub month: u8,
pub year: u16,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct DayTime {
pub day: DayReference,
pub time: Time,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum TimeUnit {
Second,
Minute,
Hour,
Day,
Week,
Month,
Year,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Direction {
Past,
Future,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum WeekdayModifier {
Last,
Next,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Meridiem {
AM,
PM,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Language {
English,
German,
}
pub trait TimeParser {
type DateTime;
fn now(&self) -> Self::DateTime;
fn parse_expression(&self, expr: TimeExpression) -> Result<Self::DateTime>;
}
pub trait LanguageParser {
fn parse(&self, input: &str) -> Result<TimeExpression>;
}
pub mod constants {
pub const SECONDS_PER_HOUR: i32 = 3600;
pub const SECONDS_PER_MINUTE: i32 = 60;
pub const MINUTES_PER_HOUR: i32 = 60;
pub const HOURS_PER_DAY: i32 = 24;
pub const DAYS_PER_WEEK: i32 = 7;
pub const MONTHS_PER_YEAR: i32 = 12;
}
pub mod errors {
pub const ERR_MONTH_POSITIVE: &str = "Month amount must be a positive number";
pub const ERR_YEAR_POSITIVE: &str = "Year amount must be a positive number";
pub const ERR_DATE_CALC_INVALID: &str = "Date calculation resulted in invalid date";
pub const ERR_YEAR_OVERFLOW: &str = "Year calculation overflow";
pub const ERR_INVALID_DATE: &str = "Invalid date";
pub const ERR_INVALID_TIME: &str = "Invalid time";
pub const ERR_AMBIGUOUS_TIME: &str = "Ambiguous or invalid local time";
pub const ERR_MIDNIGHT_FAILED: &str = "Failed to create midnight time";
pub const ERR_DATE_CALC_ERROR: &str = "Date calculation error";
pub const ERR_TIMEZONE_CONVERSION: &str = "Timezone conversion error";
pub const ERR_RELATIVE_AMOUNT_NON_NEGATIVE: &str = "Relative amount must be non-negative";
#[must_use]
pub fn format_invalid_date(year: u16, month: u8, day: u8) -> String {
format!("Invalid date: {year}-{month}-{day}")
}
#[must_use]
pub fn format_invalid_time(hour: u8, minute: u8, second: u8) -> String {
format!("Invalid time: {hour}:{minute}:{second}")
}
#[must_use]
pub fn format_invalid_timezone_offset(hours: i8, minutes: u8) -> String {
format!("Invalid timezone offset: {hours}:{minutes}")
}
}
pub mod time_utils {
use crate::{
Meridiem, Timezone, WeekdayModifier,
constants::{SECONDS_PER_HOUR, SECONDS_PER_MINUTE},
};
#[must_use]
pub fn convert_12_to_24_hour(hour: u8, meridiem: Option<&Meridiem>) -> u8 {
match meridiem {
Some(Meridiem::AM) => {
if hour == 12 {
0
} else {
hour
}
}
Some(Meridiem::PM) => {
if hour == 12 {
hour
} else {
hour + 12
}
}
None => hour,
}
}
#[must_use]
pub fn calculate_timezone_offset_seconds(hours: i8, minutes: u8) -> i32 {
let hour_seconds = i32::from(hours).saturating_mul(SECONDS_PER_HOUR);
let minute_seconds = i32::from(minutes).saturating_mul(SECONDS_PER_MINUTE);
let minute_seconds = if hours < 0 {
-minute_seconds
} else {
minute_seconds
};
hour_seconds.saturating_add(minute_seconds)
}
#[must_use]
pub fn is_valid_calendar_date(year: u16, month: u8, day: u8) -> bool {
let days_in_month = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => return false,
};
(1..=days_in_month).contains(&day)
}
#[must_use]
pub fn is_valid_24_hour_time(hour: u8, minute: u8, second: u8) -> bool {
hour <= 23 && minute <= 59 && second <= 59
}
#[must_use]
pub fn is_valid_time(hour: u8, minute: u8, second: u8, meridiem: Option<Meridiem>) -> bool {
match meridiem {
Some(_) => (1..=12).contains(&hour) && minute <= 59 && second <= 59,
None => is_valid_24_hour_time(hour, minute, second),
}
}
#[must_use]
pub fn is_valid_timezone_offset(offset: Timezone) -> bool {
match offset {
Timezone::Utc => true,
Timezone::Offset { hours, minutes } => {
minutes <= 59
&& match hours {
-12 => minutes == 0,
-11..=13 => true,
14 => minutes == 0,
_ => false,
}
}
}
}
#[must_use]
pub fn calculate_weekday_offset(
current_day_offset: i64,
target_day_offset: i64,
modifier: Option<WeekdayModifier>,
) -> i64 {
let days_diff = target_day_offset - current_day_offset;
match modifier {
None => {
if days_diff >= 0 {
days_diff
} else {
7 + days_diff
}
}
Some(WeekdayModifier::Next) => {
if days_diff > 0 {
days_diff
} else {
7 + days_diff
}
}
Some(WeekdayModifier::Last) => {
if days_diff < 0 {
days_diff
} else {
days_diff - 7
}
}
}
}
#[must_use]
fn is_leap_year(year: u16) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}
}
pub mod common {
use super::*;
pub fn parse_digit_number(input: &mut &str) -> winnow::Result<i64> {
digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
}
pub fn parse_iso_datetime(input: &mut &str) -> winnow::Result<TimeExpression> {
let (year, month, day) = (
parse_four_digit_number,
'-',
parse_two_digit_number,
'-',
parse_two_digit_number,
)
.verify_map(|(year, _, month, _, day)| {
time_utils::is_valid_calendar_date(year, month, day).then_some((year, month, day))
})
.parse_next(input)?;
let time_part = opt((
one_of(['T', ' ']),
parse_two_digit_number, ':',
parse_two_digit_number, opt((
':',
parse_two_digit_number, opt((
'.',
digit1.try_map(|s: &str| {
let fraction = if s.len() > 9 { &s[..9] } else { s };
let parsed = fraction.parse::<u32>()?;
let fraction_len = u32::try_from(fraction.len())
.expect("fraction length is capped at 9 digits");
let multiplier = 10_u32.pow(9 - fraction_len);
Ok::<u32, std::num::ParseIntError>(parsed * multiplier)
}),
)),
)),
opt(parse_timezone),
)
.verify_map(|(_, h, _, m, sec_part, tz)| {
let second = sec_part.map_or(0, |(_, s, _)| s);
time_utils::is_valid_24_hour_time(h, m, second).then_some((h, m, sec_part, tz))
}))
.parse_next(input)?;
let (hour, minute, second, nanosecond, timezone) =
if let Some((h, m, sec_part, tz)) = time_part {
let hour = Some(h);
let minute = Some(m);
let (second, nanosecond) = if let Some((_, s, frac)) = sec_part {
(Some(s), frac.map(|(_, n)| n))
} else {
(None, None)
};
(hour, minute, second, nanosecond, tz)
} else {
(None, None, None, None, None)
};
Ok(TimeExpression::Absolute(AbsoluteTime {
year,
month,
day,
hour,
minute,
second,
nanosecond,
timezone,
}))
}
fn parse_timezone(input: &mut &str) -> winnow::Result<Timezone> {
alt(("Z".map(|_| Timezone::Utc), parse_offset_timezone)).parse_next(input)
}
fn parse_offset_timezone(input: &mut &str) -> winnow::Result<Timezone> {
(
one_of(['+', '-']),
parse_two_digit_number,
opt((':', parse_two_digit_number)).map(|minutes| minutes.map_or(0, |(_, m)| m)),
)
.verify_map(|(sign, hours, minutes)| {
let hours = i8::try_from(hours).ok()?;
let signed_hours = if sign == '+' { hours } else { -hours };
let offset = Timezone::Offset {
hours: signed_hours,
minutes,
};
let can_represent_offset = !(sign == '-' && hours == 0 && minutes > 0);
(can_represent_offset && time_utils::is_valid_timezone_offset(offset))
.then_some(offset)
})
.parse_next(input)
}
pub fn parse_two_digit_number(input: &mut &str) -> winnow::Result<u8> {
take_while(1..=2, |c: char| c.is_ascii_digit())
.try_map(|s: &str| s.parse::<u8>())
.parse_next(input)
}
pub fn parse_four_digit_number(input: &mut &str) -> winnow::Result<u16> {
take_while(4..=4, |c: char| c.is_ascii_digit())
.try_map(|s: &str| s.parse::<u16>())
.parse_next(input)
}
}
pub mod language {
pub mod english;
pub mod german;
}
pub fn parse(input: &str, language: Language) -> Result<TimeExpression> {
match language {
Language::English => language::english::EnglishParser.parse(input),
Language::German => language::german::GermanParser.parse(input),
}
}