use minijinja::{
Environment, Error, ErrorKind,
value::{Kwargs, Value},
};
use time::{
Duration, OffsetDateTime, UtcOffset, format_description::FormatItem, macros::format_description,
};
use crate::localization::{self, keys};
mod format;
use self::format::{TimeDeltaValue, TimestampValue};
const SECONDS_PER_MINUTE: i64 = 60;
const SECONDS_PER_HOUR: i64 = 60 * SECONDS_PER_MINUTE;
const SECONDS_PER_DAY: i64 = 24 * SECONDS_PER_HOUR;
const SECONDS_PER_WEEK: i64 = 7 * SECONDS_PER_DAY;
const NANOS_PER_MICROSECOND: i64 = 1_000;
const NANOS_PER_MILLISECOND: i64 = 1_000 * NANOS_PER_MICROSECOND;
const SECONDS_PER_MINUTE_I32: i32 = 60;
const SECONDS_PER_HOUR_I32: i32 = 3_600;
const OFFSET_FMT: &[FormatItem<'static>] =
format_description!("[offset_hour]:[offset_minute][optional [:[offset_second]]]");
pub(crate) fn register_functions(env: &mut Environment<'_>) {
env.add_function("now", |kwargs: Kwargs| now(&kwargs));
register_query_functions(env);
}
pub(crate) fn register_query_functions(env: &mut Environment<'_>) {
env.add_function("timedelta", |kwargs: Kwargs| timedelta(&kwargs));
}
fn now(kwargs: &Kwargs) -> Result<Value, Error> {
let offset_spec: Option<String> = kwargs.get("offset")?;
kwargs.assert_all_used()?;
let mut timestamp = OffsetDateTime::now_utc();
if let Some(raw) = offset_spec {
let parsed = parse_offset(&raw)?;
timestamp = timestamp.to_offset(parsed);
}
Ok(Value::from_object(TimestampValue::new(timestamp)))
}
fn parse_offset(raw: &str) -> Result<UtcOffset, Error> {
let trimmed = raw.trim();
if trimmed.eq_ignore_ascii_case("z") {
return Ok(UtcOffset::UTC);
}
if !trimmed.starts_with(['+', '-']) {
return Err(invalid_offset(raw));
}
let parsed = UtcOffset::parse(trimmed, OFFSET_FMT).map_err(|_| invalid_offset(raw))?;
if parsed.whole_hours().abs() >= 24 {
return Err(invalid_offset(raw));
}
Ok(parsed)
}
fn invalid_offset(raw: &str) -> Error {
Error::new(
ErrorKind::InvalidOperation,
localization::message(keys::STDLIB_TIME_OFFSET_INVALID)
.with_arg("offset", raw)
.to_string(),
)
}
const COMPONENT_SPECS: &[(&str, ComponentSpec)] = &[
(
"weeks",
ComponentSpec {
multiplier: SECONDS_PER_WEEK,
constructor: Duration::seconds,
label_key: keys::STDLIB_TIME_LABEL_WEEKS,
},
),
(
"days",
ComponentSpec {
multiplier: SECONDS_PER_DAY,
constructor: Duration::seconds,
label_key: keys::STDLIB_TIME_LABEL_DAYS,
},
),
(
"hours",
ComponentSpec {
multiplier: SECONDS_PER_HOUR,
constructor: Duration::seconds,
label_key: keys::STDLIB_TIME_LABEL_HOURS,
},
),
(
"minutes",
ComponentSpec {
multiplier: SECONDS_PER_MINUTE,
constructor: Duration::seconds,
label_key: keys::STDLIB_TIME_LABEL_MINUTES,
},
),
(
"seconds",
ComponentSpec {
multiplier: 1,
constructor: Duration::seconds,
label_key: keys::STDLIB_TIME_LABEL_SECONDS,
},
),
(
"milliseconds",
ComponentSpec {
multiplier: NANOS_PER_MILLISECOND,
constructor: Duration::nanoseconds,
label_key: keys::STDLIB_TIME_LABEL_MILLISECONDS,
},
),
(
"microseconds",
ComponentSpec {
multiplier: NANOS_PER_MICROSECOND,
constructor: Duration::nanoseconds,
label_key: keys::STDLIB_TIME_LABEL_MICROSECONDS,
},
),
(
"nanoseconds",
ComponentSpec {
multiplier: 1,
constructor: Duration::nanoseconds,
label_key: keys::STDLIB_TIME_LABEL_NANOSECONDS,
},
),
];
#[derive(Clone, Copy)]
struct ComponentSpec {
multiplier: i64,
constructor: fn(i64) -> Duration,
label_key: &'static str,
}
fn add_component(
mut total: Duration,
amount: Option<i64>,
spec: ComponentSpec,
) -> Result<Duration, Error> {
if let Some(value) = amount {
let scaled = value
.checked_mul(spec.multiplier)
.ok_or_else(|| overflow_error(spec.label_key))?;
let component = (spec.constructor)(scaled);
total = total
.checked_add(component)
.ok_or_else(|| overflow_error(spec.label_key))?;
}
Ok(total)
}
fn overflow_error(label_key: &'static str) -> Error {
let component = localization::message(label_key).to_string();
Error::new(
ErrorKind::InvalidOperation,
localization::message(keys::STDLIB_TIME_OVERFLOW)
.with_arg("component", component)
.to_string(),
)
}
fn timedelta(kwargs: &Kwargs) -> Result<Value, Error> {
let mut total = Duration::ZERO;
for (name, spec) in COMPONENT_SPECS {
let amount: Option<i64> = kwargs.get(name)?;
total = add_component(total, amount, *spec)?;
}
kwargs.assert_all_used()?;
Ok(Value::from_object(TimeDeltaValue::new(total)))
}
#[cfg(test)]
mod tests;