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";
#[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, 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 = (hours as i32).saturating_mul(SECONDS_PER_HOUR);
let minute_seconds = (minutes as i32).saturating_mul(SECONDS_PER_MINUTE);
hour_seconds.saturating_add(minute_seconds)
}
#[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
}
}
}
}
}
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 = parse_four_digit_number.parse_next(input)?;
'-'.parse_next(input)?;
let month = parse_two_digit_number.parse_next(input)?;
'-'.parse_next(input)?;
let day = parse_two_digit_number.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 multiplier = 10_u32.pow(9 - fraction.len() as u32);
Ok::<u32, std::num::ParseIntError>(parsed * multiplier)
}),
)),
)),
opt(parse_timezone),
))
.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> {
let sign = one_of(['+', '-']).parse_next(input)?;
let hours = parse_two_digit_number.parse_next(input)?;
let minutes = opt((':', parse_two_digit_number))
.parse_next(input)?
.map(|(_, m)| m)
.unwrap_or(0);
let hours = if sign == '+' {
hours as i8
} else {
-(hours as i8)
};
Ok(Timezone::Offset { hours, minutes })
}
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),
}
}