use jiff::{Span, Zoned};
use temps_core::{
DayReference, Direction, Language, Result, TempsError, TimeExpression, TimeParser, TimeUnit,
Weekday,
time_utils::{
calculate_timezone_offset_seconds, calculate_weekday_offset, convert_12_to_24_hour,
},
};
pub struct JiffProvider;
impl TimeParser for JiffProvider {
type DateTime = Zoned;
fn now(&self) -> Self::DateTime {
Zoned::now()
}
fn parse_expression(&self, expr: TimeExpression) -> Result<Self::DateTime> {
match expr {
TimeExpression::Now => Ok(self.now()),
TimeExpression::Relative(rel) => {
let now = self.now();
let span = match rel.unit {
TimeUnit::Second => Span::new().seconds(rel.amount),
TimeUnit::Minute => Span::new().minutes(rel.amount),
TimeUnit::Hour => Span::new().hours(rel.amount),
TimeUnit::Day => Span::new().days(rel.amount),
TimeUnit::Week => Span::new().weeks(rel.amount),
TimeUnit::Month => Span::new().months(rel.amount),
TimeUnit::Year => Span::new().years(rel.amount),
};
match rel.direction {
Direction::Past => now.checked_sub(span).map_err(|e| {
TempsError::date_calculation_with_source(
"Date calculation error",
e.to_string(),
)
}),
Direction::Future => now.checked_add(span).map_err(|e| {
TempsError::date_calculation_with_source(
"Date calculation error",
e.to_string(),
)
}),
}
}
TimeExpression::Absolute(abs) => {
use jiff::civil::{Date, DateTime, Time};
use jiff::tz::{Offset, TimeZone};
let date = Date::new(abs.year as i16, abs.month as i8, abs.day as i8)
.map_err(|e| TempsError::backend_error(e.to_string(), "jiff"))?;
if let (Some(hour), Some(minute)) = (abs.hour, abs.minute) {
if hour > 23 {
return Err(TempsError::invalid_time(
hour,
minute,
abs.second.unwrap_or(0),
));
}
if minute > 59 {
return Err(TempsError::invalid_time(
hour,
minute,
abs.second.unwrap_or(0),
));
}
if let Some(second) = abs.second {
if second > 59 {
return Err(TempsError::invalid_time(hour, minute, second));
}
}
let time = Time::new(
hour as i8,
minute as i8,
abs.second.unwrap_or(0) as i8,
abs.nanosecond.unwrap_or(0) as i32,
)
.map_err(|e| TempsError::backend_error(e.to_string(), "jiff"))?;
let datetime = DateTime::from_parts(date, time);
match &abs.timezone {
Some(temps_core::Timezone::Utc) => datetime
.to_zoned(TimeZone::UTC)
.map(|z| z.with_time_zone(TimeZone::system()))
.map_err(|e| {
TempsError::backend_error(
format!("Timezone conversion error: {}", e),
"jiff",
)
}),
Some(temps_core::Timezone::Offset { hours, minutes }) => {
let total_seconds = calculate_timezone_offset_seconds(*hours, *minutes);
let offset = Offset::from_seconds(total_seconds).map_err(|_| {
TempsError::invalid_timezone_offset(*hours, *minutes)
})?;
datetime
.to_zoned(TimeZone::fixed(offset))
.map(|z| z.with_time_zone(TimeZone::system()))
.map_err(|e| {
TempsError::backend_error(
format!("Timezone conversion error: {}", e),
"jiff",
)
})
}
None => {
datetime.to_zoned(TimeZone::system()).map_err(|e| {
TempsError::backend_error(
format!("Timezone conversion error: {}", e),
"jiff",
)
})
}
}
} else {
let datetime = date.at(0, 0, 0, 0);
datetime.to_zoned(TimeZone::system()).map_err(|e| {
TempsError::backend_error(
format!("Timezone conversion error: {}", e),
"jiff",
)
})
}
}
TimeExpression::Day(day_ref) => {
let now = self.now();
match day_ref {
DayReference::Today => {
let date = now.date();
date.at(0, 0, 0, 0)
.to_zoned(now.time_zone().clone())
.map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to create today's date",
e.to_string(),
)
})
}
DayReference::Yesterday => {
let yesterday = now.checked_sub(Span::new().days(1)).map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to calculate yesterday",
e.to_string(),
)
})?;
let date = yesterday.date();
date.at(0, 0, 0, 0)
.to_zoned(now.time_zone().clone())
.map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to create yesterday's date",
e.to_string(),
)
})
}
DayReference::Tomorrow => {
let tomorrow = now.checked_add(Span::new().days(1)).map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to calculate tomorrow",
e.to_string(),
)
})?;
let date = tomorrow.date();
date.at(0, 0, 0, 0)
.to_zoned(now.time_zone().clone())
.map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to create tomorrow's date",
e.to_string(),
)
})
}
DayReference::Weekday { day, modifier } => {
let target_weekday = match day {
Weekday::Monday => jiff::civil::Weekday::Monday,
Weekday::Tuesday => jiff::civil::Weekday::Tuesday,
Weekday::Wednesday => jiff::civil::Weekday::Wednesday,
Weekday::Thursday => jiff::civil::Weekday::Thursday,
Weekday::Friday => jiff::civil::Weekday::Friday,
Weekday::Saturday => jiff::civil::Weekday::Saturday,
Weekday::Sunday => jiff::civil::Weekday::Sunday,
};
let current_weekday = now.weekday();
let current_offset = current_weekday.to_monday_zero_offset() as i64;
let target_offset = target_weekday.to_monday_zero_offset() as i64;
let days_to_add =
calculate_weekday_offset(current_offset, target_offset, modifier);
let target_date = now.checked_add(Span::new().days(days_to_add));
let target = target_date.map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to calculate weekday",
e.to_string(),
)
})?;
let date = target.date();
date.at(0, 0, 0, 0)
.to_zoned(now.time_zone().clone())
.map_err(|e| {
TempsError::date_calculation_with_source(
"Failed to create weekday date",
e.to_string(),
)
})
}
}
}
TimeExpression::Time(time) => {
let now = self.now();
let date = now.date();
let hour = convert_12_to_24_hour(time.hour, time.meridiem.as_ref());
if hour > 23 || time.minute > 59 || time.second > 59 {
return Err(TempsError::invalid_time(hour, time.minute, time.second));
}
date.at(hour as i8, time.minute as i8, time.second as i8, 0)
.to_zoned(now.time_zone().clone())
.map_err(|e| {
TempsError::backend_error(format!("Failed to create time: {}", e), "jiff")
})
}
TimeExpression::DayTime(day_time) => {
let day_result =
self.parse_expression(TimeExpression::Day(day_time.day.clone()))?;
let date = day_result.date();
let hour =
convert_12_to_24_hour(day_time.time.hour, day_time.time.meridiem.as_ref());
if hour > 23 || day_time.time.minute > 59 || day_time.time.second > 59 {
return Err(TempsError::invalid_time(
hour,
day_time.time.minute,
day_time.time.second,
));
}
date.at(
hour as i8,
day_time.time.minute as i8,
day_time.time.second as i8,
0,
)
.to_zoned(day_result.time_zone().clone())
.map_err(|e| {
TempsError::backend_error(format!("Failed to create day time: {}", e), "jiff")
})
}
TimeExpression::Date(date) => {
use jiff::civil::Date;
let jiff_date = Date::new(date.year as i16, date.month as i8, date.day as i8)
.map_err(|_| TempsError::invalid_date(date.year, date.month, date.day))?;
jiff_date
.at(0, 0, 0, 0)
.to_zoned(jiff::tz::TimeZone::system())
.map_err(|e| {
TempsError::backend_error(format!("Failed to create date: {}", e), "jiff")
})
}
}
}
}
pub fn parse_to_zoned(input: &str, language: Language) -> Result<Zoned> {
let expr = temps_core::parse(input, language)?;
JiffProvider.parse_expression(expr)
}