use jiff::{Span, Zoned};
use temps_core::{
DayReference, Direction, Language, Result, TempsError, TimeExpression, TimeParser, TimeUnit,
Weekday,
errors::*,
time_utils::{
calculate_timezone_offset_seconds, calculate_weekday_offset, convert_12_to_24_hour,
is_valid_time, is_valid_timezone_offset,
},
};
pub struct JiffProvider;
fn jiff_date_components(year: u16, month: u8, day: u8) -> Result<(i16, i8, i8)> {
Ok((
i16::try_from(year).map_err(|_| TempsError::invalid_date(year, month, day))?,
i8::try_from(month).map_err(|_| TempsError::invalid_date(year, month, day))?,
i8::try_from(day).map_err(|_| TempsError::invalid_date(year, month, day))?,
))
}
fn jiff_time_components(
hour: u8,
minute: u8,
second: u8,
nanosecond: u32,
) -> Result<(i8, i8, i8, i32)> {
Ok((
i8::try_from(hour).map_err(|_| TempsError::invalid_time(hour, minute, second))?,
i8::try_from(minute).map_err(|_| TempsError::invalid_time(hour, minute, second))?,
i8::try_from(second).map_err(|_| TempsError::invalid_time(hour, minute, second))?,
i32::try_from(nanosecond)
.map_err(|_| TempsError::backend_error("Invalid nanosecond component", "jiff"))?,
))
}
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) => {
if rel.amount < 0 {
return Err(TempsError::date_calculation(
ERR_RELATIVE_AMOUNT_NON_NEGATIVE,
));
}
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(ERR_DATE_CALC_ERROR, e.to_string())
}),
Direction::Future => now.checked_add(span).map_err(|e| {
TempsError::date_calculation_with_source(ERR_DATE_CALC_ERROR, e.to_string())
}),
}
}
TimeExpression::Absolute(abs) => {
use jiff::civil::{Date, DateTime, Time};
use jiff::tz::{Offset, TimeZone};
let (year, month, day) = jiff_date_components(abs.year, abs.month, abs.day)?;
let date = Date::new(year, month, day)
.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
&& second > 59
{
return Err(TempsError::invalid_time(hour, minute, second));
}
let second = abs.second.unwrap_or(0);
let nanosecond = abs.nanosecond.unwrap_or(0);
let (hour, minute, second, nanosecond) =
jiff_time_components(hour, minute, second, nanosecond)?;
let time = Time::new(hour, minute, second, nanosecond)
.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!("{ERR_TIMEZONE_CONVERSION}: {e}"),
"jiff",
)
}),
Some(temps_core::Timezone::Offset { hours, minutes }) => {
if !is_valid_timezone_offset(temps_core::Timezone::Offset {
hours: *hours,
minutes: *minutes,
}) {
return Err(TempsError::invalid_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!("{ERR_TIMEZONE_CONVERSION}: {e}"),
"jiff",
)
})
}
None => {
datetime.to_zoned(TimeZone::system()).map_err(|e| {
TempsError::backend_error(
format!("{ERR_TIMEZONE_CONVERSION}: {e}"),
"jiff",
)
})
}
}
} else {
let datetime = date.at(0, 0, 0, 0);
datetime.to_zoned(TimeZone::system()).map_err(|e| {
TempsError::backend_error(format!("{ERR_TIMEZONE_CONVERSION}: {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();
if !is_valid_time(time.hour, time.minute, time.second, time.meridiem) {
return Err(TempsError::invalid_time(
time.hour,
time.minute,
time.second,
));
}
let hour = convert_12_to_24_hour(time.hour, time.meridiem.as_ref());
let (hour, minute, second, nanosecond) =
jiff_time_components(hour, time.minute, time.second, 0)?;
date.at(hour, minute, second, nanosecond)
.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))?;
let date = day_result.date();
if !is_valid_time(
day_time.time.hour,
day_time.time.minute,
day_time.time.second,
day_time.time.meridiem,
) {
return Err(TempsError::invalid_time(
day_time.time.hour,
day_time.time.minute,
day_time.time.second,
));
}
let hour =
convert_12_to_24_hour(day_time.time.hour, day_time.time.meridiem.as_ref());
let (hour, minute, second, nanosecond) =
jiff_time_components(hour, day_time.time.minute, day_time.time.second, 0)?;
date.at(hour, minute, second, nanosecond)
.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 (year, month, day) = jiff_date_components(date.year, date.month, date.day)?;
let jiff_date = Date::new(year, month, day)
.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)
}