use std::{
cmp::Ordering,
collections::hash_map::DefaultHasher,
fmt::Write,
hash::{Hash, Hasher},
mem,
};
use chrono::TimeDelta as ChronoTimeDelta;
use crate::{
args::{ArgValues, FromArgs, FromValue, FromValueFail, is_long_int},
bytecode::{CallResult, VM},
exception_private::{ExcType, RunError, RunResult, SimpleException},
hash::HashValue,
heap::{HeapData, HeapId, HeapItem, HeapRead, HeapReadOutput},
intern::StaticStrings,
resource::ResourceTracker,
types::{CmpOrder, LazyHeapSet, PyTrait, Type, str::allocate_string},
value::{EitherStr, Value},
};
pub(crate) const MIN_TIMEDELTA_DAYS: i32 = -999_999_999;
pub(crate) const MAX_TIMEDELTA_DAYS: i32 = 999_999_999;
const DAY_SECONDS: i32 = 86_400;
pub(crate) const SECONDS_PER_HOUR: i32 = 3_600;
pub(crate) const SECONDS_PER_MINUTE: i32 = 60;
pub(crate) const MICROSECONDS_PER_SECOND: i128 = 1_000_000;
const MILLISECONDS_PER_SECOND: i128 = 1_000;
const DAY_MICROSECONDS: i128 = (DAY_SECONDS as i128) * MICROSECONDS_PER_SECOND;
const HOUR_MICROSECONDS: i128 = (SECONDS_PER_HOUR as i128) * MICROSECONDS_PER_SECOND;
const MINUTE_MICROSECONDS: i128 = (SECONDS_PER_MINUTE as i128) * MICROSECONDS_PER_SECOND;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
pub(crate) struct TimeDelta(pub(crate) ChronoTimeDelta);
pub(crate) fn new(days: i32, seconds: i32, microseconds: i32) -> RunResult<TimeDelta> {
if !(MIN_TIMEDELTA_DAYS..=MAX_TIMEDELTA_DAYS).contains(&days) {
return Err(SimpleException::new_msg(
ExcType::OverflowError,
format!("days={days}; must have magnitude <= 999999999"),
)
.into());
}
if !(0..DAY_SECONDS).contains(&seconds)
|| !(0..i32::try_from(MICROSECONDS_PER_SECOND).unwrap()).contains(µseconds)
{
return Err(SimpleException::new_msg(ExcType::ValueError, "timedelta normalized fields out of range").into());
}
let total_microseconds =
i128::from(days) * DAY_MICROSECONDS + i128::from(seconds) * MICROSECONDS_PER_SECOND + i128::from(microseconds);
from_total_microseconds(total_microseconds)
}
#[must_use]
pub(crate) fn components(delta: &TimeDelta) -> (i32, i32, i32) {
let total_microseconds = total_microseconds(delta);
let days = total_microseconds.div_euclid(DAY_MICROSECONDS);
let rem = total_microseconds.rem_euclid(DAY_MICROSECONDS);
let seconds = rem / MICROSECONDS_PER_SECOND;
let micros = rem % MICROSECONDS_PER_SECOND;
(
i32::try_from(days).expect("chrono day range fits CPython i32 day bounds"),
i32::try_from(seconds).expect("seconds are bounded by one day"),
i32::try_from(micros).expect("microseconds are bounded by one second"),
)
}
#[must_use]
pub(crate) fn total_microseconds(delta: &TimeDelta) -> i128 {
let seconds = i128::from(delta.0.num_seconds());
let microseconds =
i128::from(delta.0.subsec_nanos() / i32::try_from(MILLISECONDS_PER_SECOND).expect("1000 fits in i32"));
seconds * MICROSECONDS_PER_SECOND + microseconds
}
#[must_use]
pub(crate) fn total_seconds(delta: &TimeDelta) -> f64 {
total_microseconds(delta) as f64 / (MICROSECONDS_PER_SECOND as f64)
}
#[must_use]
pub(crate) fn exact_total_seconds(delta: &TimeDelta) -> Option<i128> {
let (days, seconds, microseconds) = components(delta);
if microseconds == 0 {
Some(i128::from(days) * i128::from(DAY_SECONDS) + i128::from(seconds))
} else {
None
}
}
#[must_use]
pub(crate) fn chrono_delta(delta: &TimeDelta) -> ChronoTimeDelta {
delta.0
}
#[must_use]
pub(crate) fn div_microseconds_round_ties_even(total_microseconds: i128, divisor: i128) -> i128 {
debug_assert_ne!(divisor, 0);
let negative = total_microseconds.is_negative() ^ divisor.is_negative();
let numerator = total_microseconds.abs();
let denominator = divisor.abs();
let quotient = numerator / denominator;
let remainder = numerator % denominator;
let rounded = match (remainder * 2).cmp(&denominator) {
Ordering::Less => quotient,
Ordering::Greater => quotient + 1,
Ordering::Equal => {
if quotient % 2 == 0 {
quotient
} else {
quotient + 1
}
}
};
if negative { -rounded } else { rounded }
}
pub(crate) fn from_chrono(delta: ChronoTimeDelta) -> RunResult<TimeDelta> {
from_total_microseconds(
i128::from(delta.num_seconds()) * MICROSECONDS_PER_SECOND
+ i128::from(delta.subsec_nanos() / i32::try_from(MILLISECONDS_PER_SECOND).expect("1000 fits in i32")),
)
}
pub(crate) fn from_total_microseconds(total_microseconds: i128) -> RunResult<TimeDelta> {
let days = total_microseconds.div_euclid(DAY_MICROSECONDS);
if !(i128::from(MIN_TIMEDELTA_DAYS)..=i128::from(MAX_TIMEDELTA_DAYS)).contains(&days) {
return Err(SimpleException::new_msg(
ExcType::OverflowError,
format!("days={days}; must have magnitude <= 999999999"),
)
.into());
}
let seconds = total_microseconds.div_euclid(MICROSECONDS_PER_SECOND);
let micros = total_microseconds.rem_euclid(MICROSECONDS_PER_SECOND);
let seconds = i64::try_from(seconds)
.map_err(|_| SimpleException::new_msg(ExcType::OverflowError, "timedelta value out of range"))?;
let nanos = u32::try_from(micros * MILLISECONDS_PER_SECOND)
.expect("microsecond remainder is in 0..1_000_000 and fits u32 nanoseconds");
let delta = ChronoTimeDelta::new(seconds, nanos)
.ok_or_else(|| SimpleException::new_msg(ExcType::OverflowError, "timedelta value out of range"))?;
Ok(TimeDelta(delta))
}
pub(crate) fn init(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let TimedeltaArgs {
days,
seconds,
microseconds,
milliseconds,
minutes,
hours,
weeks,
} = TimedeltaArgs::from_args(args, vm)?;
let total_microseconds = checked_component(weeks, 7 * DAY_MICROSECONDS)?
+ checked_component(days, DAY_MICROSECONDS)?
+ checked_component(hours, HOUR_MICROSECONDS)?
+ checked_component(minutes, MINUTE_MICROSECONDS)?
+ checked_component(seconds, MICROSECONDS_PER_SECOND)?
+ checked_component(milliseconds, MILLISECONDS_PER_SECOND)?
+ microseconds.0;
let delta = from_total_microseconds(total_microseconds)?;
Ok(Value::Ref(vm.heap.allocate(HeapData::TimeDelta(delta))?))
}
#[derive(FromArgs)]
#[from_args(name = "timedelta")]
struct TimedeltaArgs {
#[from_args(default)]
days: DeltaComponent,
#[from_args(default)]
seconds: DeltaComponent,
#[from_args(default)]
microseconds: DeltaComponent,
#[from_args(kw_only, default)]
milliseconds: DeltaComponent,
#[from_args(kw_only, default)]
minutes: DeltaComponent,
#[from_args(kw_only, default)]
hours: DeltaComponent,
#[from_args(kw_only, default)]
weeks: DeltaComponent,
}
#[derive(Clone, Copy, Default)]
struct DeltaComponent(i128);
impl FromValue for DeltaComponent {
const EXPECTED_TYPE_NAME: Option<&'static str> = Some("int");
fn from_value(value: Value, vm: &mut VM<'_, impl ResourceTracker>) -> Result<Self, FromValueFail> {
let result = match value {
Value::Bool(b) => Ok(Self(i128::from(b))),
Value::Int(i) => Ok(Self(i128::from(i))),
_ if is_long_int(&value, vm) => Err(FromValueFail::Raise(ExcType::overflow_c_int())),
_ => Err(FromValueFail::WrongType),
};
value.drop_with_heap(vm);
result
}
fn type_error(got: &str) -> RunError {
ExcType::type_error_not_integer(got)
}
}
fn checked_component(value: DeltaComponent, unit_microseconds: i128) -> RunResult<i128> {
value.0.checked_mul(unit_microseconds).ok_or_else(|| {
SimpleException::new_msg(ExcType::OverflowError, "timedelta argument overflow while normalizing").into()
})
}
#[must_use]
pub(crate) fn format_repr(delta: &TimeDelta) -> String {
let (days, seconds, microseconds) = components(delta);
if days == 0 && seconds == 0 && microseconds == 0 {
return "datetime.timedelta(0)".to_owned();
}
let mut repr = String::from("datetime.timedelta(");
let mut first = true;
if days != 0 {
write!(repr, "days={days}").expect("writing to String cannot fail");
first = false;
}
if seconds != 0 {
if !first {
repr.push_str(", ");
}
write!(repr, "seconds={seconds}").expect("writing to String cannot fail");
first = false;
}
if microseconds != 0 {
if !first {
repr.push_str(", ");
}
write!(repr, "microseconds={microseconds}").expect("writing to String cannot fail");
}
repr.push(')');
repr
}
impl HeapItem for TimeDelta {
fn py_estimate_size(&self) -> usize {
mem::size_of::<Self>()
}
fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {}
}
impl<'h> PyTrait<'h> for HeapRead<'h, TimeDelta> {
fn py_type(&self, _vm: &VM<'h, impl ResourceTracker>) -> Type {
Type::TimeDelta
}
fn py_len(&self, _vm: &VM<'h, impl ResourceTracker>) -> Option<usize> {
None
}
fn py_eq_impl(&self, other: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<bool>> {
let Some(HeapReadOutput::TimeDelta(other)) = other.read_heap(vm) else {
return Ok(None);
};
Ok(Some(
total_microseconds(self.get(vm.heap)) == total_microseconds(other.get(vm.heap)),
))
}
fn py_hash(&self, _self_id: HeapId, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<HashValue>> {
let mut hasher = DefaultHasher::new();
self.get(vm.heap).hash(&mut hasher);
Ok(Some(HashValue::new(hasher.finish())))
}
fn py_cmp(&self, other: &Self, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<CmpOrder> {
Ok(CmpOrder::Ordered(
total_microseconds(self.get(vm.heap)).cmp(&total_microseconds(other.get(vm.heap))),
))
}
fn py_bool(&self, vm: &mut VM<'h, impl ResourceTracker>) -> bool {
total_microseconds(self.get(vm.heap)) != 0
}
fn py_repr_fmt(
&self,
f: &mut impl Write,
vm: &mut VM<'h, impl ResourceTracker>,
_heap_ids: &mut LazyHeapSet,
) -> RunResult<()> {
f.write_str(&format_repr(self.get(vm.heap)))?;
Ok(())
}
fn py_str(&self, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let (days, seconds, microseconds) = components(self.get(vm.heap));
let hours = seconds / SECONDS_PER_HOUR;
let minutes = (seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
let second = seconds % SECONDS_PER_MINUTE;
let time = if microseconds == 0 {
format!("{hours}:{minutes:02}:{second:02}")
} else {
format!("{hours}:{minutes:02}:{second:02}.{microseconds:06}")
};
let s = if days == 0 {
time
} else {
let day_word = if days.abs() == 1 { "day" } else { "days" };
format!("{days} {day_word}, {time}")
};
Ok(allocate_string(s, vm.heap)?)
}
fn py_call_attr(
&mut self,
_self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
attr: &EitherStr,
args: ArgValues,
) -> RunResult<CallResult> {
if attr.string_id() == Some(StaticStrings::TotalSeconds.into()) {
let td = *self.get(vm.heap);
args.check_zero_args("timedelta.total_seconds", vm.heap)?;
return Ok(CallResult::Value(Value::Float(total_seconds(&td))));
}
Err(ExcType::attribute_error(Type::TimeDelta, attr.as_str(vm.interns)))
}
fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<CallResult>> {
let (days, seconds, microseconds) = components(self.get(vm.heap));
match attr.string_id() {
Some(id) if id == StaticStrings::Days => Ok(Some(CallResult::Value(Value::Int(i64::from(days))))),
Some(id) if id == StaticStrings::Seconds => Ok(Some(CallResult::Value(Value::Int(i64::from(seconds))))),
Some(id) if id == StaticStrings::Microseconds => {
Ok(Some(CallResult::Value(Value::Int(i64::from(microseconds)))))
}
_ => Ok(None),
}
}
}