use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use crate::{
error::{EvalError, EvalResult, InterpreterError},
eval::modules::{arg_str, need_arg, value_error},
value::Value,
};
pub fn has_function(name: &str) -> bool {
matches!(name, "date" | "datetime" | "time" | "timedelta" | "timezone")
}
#[must_use]
pub fn type_classmethod(type_name: &str, method: &str) -> Option<&'static str> {
match (type_name, method) {
("datetime", "strptime") => Some("strptime"),
_ => None,
}
}
fn construct_naive_date(year: i32, month: u32, day: u32) -> Result<NaiveDate, EvalError> {
if !(1..=9999).contains(&year) {
return Err(value_error(format!("year {year} is out of range")));
}
if !(1..=12).contains(&month) {
return Err(value_error("month must be in 1..12"));
}
NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| value_error("day is out of range for month"))
}
pub fn call(func: &str, args: &[Value]) -> EvalResult {
match func {
"date" => {
let year = arg_i32(func, args, 0)?;
let month = arg_u32(func, args, 1)?;
let day = arg_u32(func, args, 2)?;
let date = construct_naive_date(year, month, day)?;
Ok(Value::Date(date))
}
"datetime" => {
let year = arg_i32(func, args, 0)?;
let month = arg_u32(func, args, 1)?;
let day = arg_u32(func, args, 2)?;
let hour = opt_u32(args, 3)?.unwrap_or(0);
let minute = opt_u32(args, 4)?.unwrap_or(0);
let second = opt_u32(args, 5)?.unwrap_or(0);
let microsecond = opt_u32(args, 6)?.unwrap_or(0);
let date = construct_naive_date(year, month, day)?;
let time = NaiveTime::from_hms_micro_opt(hour, minute, second, microsecond)
.ok_or_else(|| value_error("time component out of range"))?;
let dt = NaiveDateTime::new(date, time);
let tz_offset_secs = match args.get(7) {
None | Some(Value::None) => None,
Some(Value::TimeZone(secs)) => Some(*secs),
Some(other) => {
return Err(InterpreterError::TypeError(format!(
"datetime() tzinfo must be a datetime.timezone (got '{}')",
other.type_name()
))
.into());
}
};
Ok(Value::DateTime { dt, tz_offset_secs })
}
"time" => {
let hour = opt_u32(args, 0)?.unwrap_or(0);
let minute = opt_u32(args, 1)?.unwrap_or(0);
let second = opt_u32(args, 2)?.unwrap_or(0);
let microsecond = opt_u32(args, 3)?.unwrap_or(0);
let t = NaiveTime::from_hms_micro_opt(hour, minute, second, microsecond)
.ok_or_else(|| value_error("time component out of range"))?;
Ok(Value::Time(t))
}
"timedelta" => {
let days = opt_i64(args, 0)?.unwrap_or(0);
let seconds = opt_i64(args, 1)?.unwrap_or(0);
let microseconds = opt_i64(args, 2)?.unwrap_or(0);
let milliseconds = opt_i64(args, 3)?.unwrap_or(0);
let minutes = opt_i64(args, 4)?.unwrap_or(0);
let hours = opt_i64(args, 5)?.unwrap_or(0);
let weeks = opt_i64(args, 6)?.unwrap_or(0);
let micros = days
.checked_mul(86_400_000_000)
.and_then(|d| weeks.checked_mul(7 * 86_400_000_000).and_then(|w| d.checked_add(w)))
.and_then(|x| hours.checked_mul(3_600_000_000).and_then(|h| x.checked_add(h)))
.and_then(|x| minutes.checked_mul(60_000_000).and_then(|m| x.checked_add(m)))
.and_then(|x| seconds.checked_mul(1_000_000).and_then(|s| x.checked_add(s)))
.and_then(|x| milliseconds.checked_mul(1_000).and_then(|ms| x.checked_add(ms)))
.and_then(|x| x.checked_add(microseconds))
.ok_or_else(|| value_error("timedelta overflow"))?;
Ok(Value::TimeDelta(micros))
}
"timezone" => {
let offset = need_arg(func, args, 0)?;
let secs = match offset {
Value::TimeDelta(micros) => {
let secs_i64 = micros / 1_000_000;
i32::try_from(secs_i64)
.map_err(|_| value_error("timezone offset out of range"))?
}
_ => {
return Err(InterpreterError::TypeError(format!(
"timezone() argument 1 must be a timedelta (got '{}')",
offset.type_name()
))
.into());
}
};
Ok(Value::TimeZone(secs))
}
"strptime" => {
let s = arg_str("strptime", args, 0)?;
let fmt = arg_str("strptime", args, 1)?;
parse_strptime(s, fmt)
}
_ => Err(InterpreterError::AttributeError(format!(
"module 'datetime' has no attribute '{func}'"
))
.into()),
}
}
fn parse_strptime(s: &str, fmt: &str) -> EvalResult {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Ok(Value::DateTime { dt, tz_offset_secs: None });
}
if let Ok(date) = NaiveDate::parse_from_str(s, fmt) {
let Some(time) = NaiveTime::from_hms_opt(0, 0, 0) else {
return Err(value_error("internal: failed to build midnight time"));
};
return Ok(Value::DateTime { dt: NaiveDateTime::new(date, time), tz_offset_secs: None });
}
if let Ok(time) = NaiveTime::parse_from_str(s, fmt) {
let Some(date) = NaiveDate::from_ymd_opt(1900, 1, 1) else {
return Err(value_error("internal: failed to build 1900-01-01"));
};
return Ok(Value::DateTime { dt: NaiveDateTime::new(date, time), tz_offset_secs: None });
}
Err(value_error(format!("time data '{s}' does not match format '{fmt}'")))
}
pub fn date_attribute(date: NaiveDate, attr: &str) -> EvalResult {
match attr {
"year" => Ok(Value::Int(i64::from(date.year()))),
"month" => Ok(Value::Int(i64::from(date.month()))),
"day" => Ok(Value::Int(i64::from(date.day()))),
_ => Err(InterpreterError::AttributeError(format!(
"'date' object has no attribute '{attr}'"
))
.into()),
}
}
pub fn datetime_attribute(
dt: NaiveDateTime,
tz_offset_secs: Option<i32>,
attr: &str,
) -> EvalResult {
match attr {
"year" => Ok(Value::Int(i64::from(dt.year()))),
"month" => Ok(Value::Int(i64::from(dt.month()))),
"day" => Ok(Value::Int(i64::from(dt.day()))),
"hour" => Ok(Value::Int(i64::from(dt.hour()))),
"minute" => Ok(Value::Int(i64::from(dt.minute()))),
"second" => Ok(Value::Int(i64::from(dt.second()))),
"microsecond" => Ok(Value::Int(i64::from(dt.nanosecond() / 1_000))),
"tzinfo" => Ok(tz_offset_secs.map_or(Value::None, Value::TimeZone)),
_ => Err(InterpreterError::AttributeError(format!(
"'datetime' object has no attribute '{attr}'"
))
.into()),
}
}
pub fn time_attribute(t: NaiveTime, attr: &str) -> EvalResult {
match attr {
"hour" => Ok(Value::Int(i64::from(t.hour()))),
"minute" => Ok(Value::Int(i64::from(t.minute()))),
"second" => Ok(Value::Int(i64::from(t.second()))),
"microsecond" => Ok(Value::Int(i64::from(t.nanosecond() / 1_000))),
_ => Err(InterpreterError::AttributeError(format!(
"'time' object has no attribute '{attr}'"
))
.into()),
}
}
pub fn timedelta_attribute(micros: i64, attr: &str) -> EvalResult {
let secs_total = micros.div_euclid(1_000_000);
let us = micros.rem_euclid(1_000_000);
let days = secs_total.div_euclid(86_400);
let secs = secs_total.rem_euclid(86_400);
match attr {
"days" => Ok(Value::Int(days)),
"seconds" => Ok(Value::Int(secs)),
"microseconds" => Ok(Value::Int(us)),
_ => Err(InterpreterError::AttributeError(format!(
"'timedelta' object has no attribute '{attr}'"
))
.into()),
}
}
pub fn dispatch_date_method(
date: NaiveDate,
method: &str,
args: &[Value],
kwargs: &indexmap::IndexMap<String, Value>,
) -> EvalResult {
crate::eval::functions::reject_kwargs(method, kwargs)?;
match method {
"isoformat" => Ok(Value::String(date.format("%Y-%m-%d").to_string().into())),
"weekday" => Ok(Value::Int(i64::from(date.weekday().num_days_from_monday()))),
"isoweekday" => Ok(Value::Int(i64::from(date.weekday().number_from_monday()))),
"strftime" => {
let fmt = arg_str("strftime", args, 0)?;
Ok(Value::String(date.format(fmt).to_string().into()))
}
"replace" => {
let year = opt_i32(args, 0)?.unwrap_or_else(|| date.year());
let month = opt_u32(args, 1)?.unwrap_or_else(|| date.month());
let day = opt_u32(args, 2)?.unwrap_or_else(|| date.day());
let replaced = construct_naive_date(year, month, day)?;
Ok(Value::Date(replaced))
}
_ => Err(InterpreterError::AttributeError(format!(
"'date' object has no attribute '{method}'"
))
.into()),
}
}
pub fn dispatch_datetime_method(
dt: NaiveDateTime,
tz_offset_secs: Option<i32>,
method: &str,
args: &[Value],
kwargs: &indexmap::IndexMap<String, Value>,
) -> EvalResult {
crate::eval::functions::reject_kwargs(method, kwargs)?;
match method {
"isoformat" => {
let mut s = dt.format("%Y-%m-%dT%H:%M:%S").to_string();
let us = dt.nanosecond() / 1_000;
if us != 0 {
s = format!("{s}.{us:06}");
}
if let Some(secs) = tz_offset_secs {
let sign = if secs < 0 { '-' } else { '+' };
let abs = secs.unsigned_abs();
let h = abs / 3600;
let m = (abs % 3600) / 60;
s = format!("{s}{sign}{h:02}:{m:02}");
}
Ok(Value::String(s.into()))
}
"date" => Ok(Value::Date(dt.date())),
"time" => Ok(Value::Time(dt.time())),
"weekday" => Ok(Value::Int(i64::from(dt.weekday().num_days_from_monday()))),
"isoweekday" => Ok(Value::Int(i64::from(dt.weekday().number_from_monday()))),
"strftime" => {
let fmt = arg_str("strftime", args, 0)?;
Ok(Value::String(dt.format(fmt).to_string().into()))
}
"timestamp" => {
#[expect(
clippy::cast_precision_loss,
reason = "matches CPython's timestamp() return shape: f64 seconds since epoch. Loss matters only past 2^53 seconds (~285M years post-epoch)."
)]
let mut ts = dt.and_utc().timestamp_millis() as f64 / 1000.0;
if let Some(secs) = tz_offset_secs {
ts -= f64::from(secs);
}
Ok(Value::Float(ts))
}
_ => Err(InterpreterError::AttributeError(format!(
"'datetime' object has no attribute '{method}'"
))
.into()),
}
}
pub fn dispatch_time_method(
t: NaiveTime,
method: &str,
args: &[Value],
kwargs: &indexmap::IndexMap<String, Value>,
) -> EvalResult {
crate::eval::functions::reject_kwargs(method, kwargs)?;
match method {
"isoformat" => {
let mut s = t.format("%H:%M:%S").to_string();
let us = t.nanosecond() / 1_000;
if us != 0 {
s = format!("{s}.{us:06}");
}
Ok(Value::String(s.into()))
}
"strftime" => {
let fmt = arg_str("strftime", args, 0)?;
Ok(Value::String(t.format(fmt).to_string().into()))
}
_ => Err(InterpreterError::AttributeError(format!(
"'time' object has no attribute '{method}'"
))
.into()),
}
}
pub fn dispatch_timedelta_method(
micros: i64,
method: &str,
_args: &[Value],
kwargs: &indexmap::IndexMap<String, Value>,
) -> EvalResult {
crate::eval::functions::reject_kwargs(method, kwargs)?;
match method {
"total_seconds" => {
#[expect(
clippy::cast_precision_loss,
reason = "matches CPython's total_seconds() return shape: f64 seconds. Loss matters only past 2^53 microseconds (~285 years)."
)]
let secs = micros as f64 / 1_000_000.0;
Ok(Value::Float(secs))
}
_ => Err(InterpreterError::AttributeError(format!(
"'timedelta' object has no attribute '{method}'"
))
.into()),
}
}
pub fn try_arith(op: &str, lhs: &Value, rhs: &Value) -> Option<EvalResult> {
match (op, lhs, rhs) {
("+", Value::Date(d), Value::TimeDelta(us))
| ("+", Value::TimeDelta(us), Value::Date(d)) => {
let days = i32::try_from(us / 86_400_000_000).ok()?;
let result = d.checked_add_signed(Duration::days(i64::from(days)))?;
Some(Ok(Value::Date(result)))
}
("-", Value::Date(d), Value::TimeDelta(us)) => {
let days = i32::try_from(us / 86_400_000_000).ok()?;
let result = d.checked_sub_signed(Duration::days(i64::from(days)))?;
Some(Ok(Value::Date(result)))
}
("-", Value::Date(a), Value::Date(b)) => {
let delta = a.signed_duration_since(*b);
Some(Ok(Value::TimeDelta(delta.num_microseconds()?)))
}
("+", Value::DateTime { dt, tz_offset_secs }, Value::TimeDelta(us))
| ("+", Value::TimeDelta(us), Value::DateTime { dt, tz_offset_secs }) => {
let result = dt.checked_add_signed(Duration::microseconds(*us))?;
Some(Ok(Value::DateTime { dt: result, tz_offset_secs: *tz_offset_secs }))
}
("-", Value::DateTime { dt, tz_offset_secs }, Value::TimeDelta(us)) => {
let result = dt.checked_sub_signed(Duration::microseconds(*us))?;
Some(Ok(Value::DateTime { dt: result, tz_offset_secs: *tz_offset_secs }))
}
(
"-",
Value::DateTime { dt: a, tz_offset_secs: tz_a },
Value::DateTime { dt: b, tz_offset_secs: tz_b },
) => {
if tz_a.is_some() != tz_b.is_some() {
return Some(Err(InterpreterError::TypeError(
"can't subtract offset-naive and offset-aware datetimes".into(),
)
.into()));
}
let delta = a.signed_duration_since(*b);
Some(Ok(Value::TimeDelta(delta.num_microseconds()?)))
}
("+", Value::TimeDelta(a), Value::TimeDelta(b)) => {
Some(Ok(Value::TimeDelta(a.checked_add(*b)?)))
}
("-", Value::TimeDelta(a), Value::TimeDelta(b)) => {
Some(Ok(Value::TimeDelta(a.checked_sub(*b)?)))
}
("*", Value::TimeDelta(a), Value::Int(n)) | ("*", Value::Int(n), Value::TimeDelta(a)) => {
Some(Ok(Value::TimeDelta(a.checked_mul(*n)?)))
}
("//", Value::TimeDelta(a), Value::Int(n)) if *n != 0 => Some(Ok(Value::TimeDelta(a / n))),
_ => None,
}
}
fn arg_i32(func: &str, args: &[Value], index: usize) -> Result<i32, EvalError> {
int_arg(need_arg(func, args, index)?, func, index)
.and_then(|n| i32::try_from(n).map_err(|_| value_error("year out of range")))
}
fn arg_u32(func: &str, args: &[Value], index: usize) -> Result<u32, EvalError> {
int_arg(need_arg(func, args, index)?, func, index)
.and_then(|n| u32::try_from(n).map_err(|_| value_error("value out of range")))
}
fn opt_i32(args: &[Value], index: usize) -> Result<Option<i32>, EvalError> {
match args.get(index) {
None => Ok(None),
Some(v) => Ok(Some(
int_arg(v, "replace", index)
.and_then(|n| i32::try_from(n).map_err(|_| value_error("year out of range")))?,
)),
}
}
fn opt_u32(args: &[Value], index: usize) -> Result<Option<u32>, EvalError> {
match args.get(index) {
None => Ok(None),
Some(v) => Ok(Some(
int_arg(v, "replace", index)
.and_then(|n| u32::try_from(n).map_err(|_| value_error("value out of range")))?,
)),
}
}
fn opt_i64(args: &[Value], index: usize) -> Result<Option<i64>, EvalError> {
match args.get(index) {
None | Some(Value::None) => Ok(None),
Some(v) => int_arg(v, "timedelta", index).map(Some),
}
}
fn int_arg(value: &Value, func: &str, index: usize) -> Result<i64, EvalError> {
match value {
Value::Int(i) => Ok(*i),
Value::Bool(b) => Ok(i64::from(*b)),
_ => Err(InterpreterError::TypeError(format!(
"{func}() expected an integer at position {index}"
))
.into()),
}
}
pub struct DatetimeModule;
#[async_trait::async_trait]
impl crate::eval::modules::Module for DatetimeModule {
fn name(&self) -> &'static str {
"datetime"
}
fn has_function(&self, name: &str) -> bool {
has_function(name)
}
async fn call(
&self,
_state: &mut crate::state::InterpreterState,
func: &str,
args: &[Value],
_kwargs: &indexmap::IndexMap<String, Value>,
_tools: &crate::tools::Tools,
) -> EvalResult {
call(func, args)
}
}