Skip to main content

ferray_ufunc/ops/
datetime.rs

1// ferray-ufunc: datetime64 / timedelta64 predicates and arithmetic
2//
3// Provides:
4//   - isnat() for the time element types — the "is Not a Time"
5//     predicate that returns true where a value equals the NaT sentinel
6//     (i64::MIN).
7//   - Element-wise arithmetic kernels:
8//       datetime - datetime → timedelta
9//       datetime + timedelta → datetime
10//       datetime - timedelta → datetime
11//       timedelta + timedelta → timedelta
12//       timedelta - timedelta → timedelta
13//
14// Same-unit operands required (NumPy promotes to the finer unit; we
15// surface a ShapeMismatch with a clear message and let callers cast
16// explicitly — that matches ferray's "no implicit precision changes"
17// stance from the rest of the workspace).
18//
19// NaT propagation: any operand that is NaT yields NaT in the output
20// (matching NumPy).
21//
22// ## REQ status — datetime64/timedelta64 ufunc kernels (#942)
23//
24// SHIPPED:
25//   - `datetime64`/`timedelta64` element-wise ufunc kernels — NOT a standalone
26//     REQ-1..REQ-28 row in `.design/ferray-ufunc.md` (the numbered REQs cover
27//     the real/complex numeric ufunc families; the time dtypes are tracked by
28//     issue #942). Honest classification: this is the numpy time-arithmetic
29//     ufunc surface (`np.subtract`/`np.add` resolved to the datetime loops)
30//     plus the `np.isnat` predicate. Anchors: `pub fn isnat_datetime`/
31//     `pub fn isnat_timedelta` (the `is Not a Time` predicate, true where the
32//     value equals the `i64::MIN` NaT sentinel); arithmetic kernels
33//     `pub fn sub_datetime` (datetime - datetime -> timedelta),
34//     `pub fn add_datetime_timedelta`/`pub fn sub_datetime_timedelta`
35//     (datetime ± timedelta -> datetime), `pub fn add_timedelta`/
36//     `pub fn sub_timedelta` (timedelta ± timedelta -> timedelta); scalar
37//     multiply/divide `pub fn mul_timedelta_scalar_i64`/`_f64`,
38//     `pub fn div_timedelta_scalar_i64`/`_f64`, `pub fn truediv_timedelta`/
39//     `pub fn floordiv_timedelta`/`pub fn mod_timedelta`; and the unit-aware
40//     `*_promoted` variants (`pub fn sub_datetime_promoted`/
41//     `pub fn add_datetime_timedelta_promoted`/`pub fn add_timedelta_promoted`)
42//     that resolve mixed-unit operands to the finer unit. Same-unit operands
43//     are required for the non-promoted kernels (a `ShapeMismatch` surfaces
44//     otherwise — ferray's "no implicit precision changes" stance). NaT
45//     propagation matches numpy. Audited against numpy 2.4.x and green.
46//     Non-test production consumer: re-exported from the crate root (`lib.rs`
47//     `pub use ops::datetime::{add_datetime_timedelta,
48//     add_datetime_timedelta_promoted, add_timedelta, add_timedelta_promoted,
49//     isnat_datetime, isnat_timedelta, sub_datetime, sub_datetime_promoted,
50//     sub_datetime_timedelta, sub_timedelta}`), the public time-dtype ufunc
51//     surface and the ferray-python datetime binding target.
52//
53// NOT-STARTED: none — the #942 datetime/timedelta kernels are shipped.
54
55use ferray_core::Array;
56use ferray_core::dimension::Dimension;
57use ferray_core::dtype::{DateTime64, NAT, TimeUnit, Timedelta64};
58use ferray_core::error::{FerrayError, FerrayResult};
59
60/// Element-wise: true where the input is the NaT ("Not a Time") sentinel.
61///
62/// Equivalent to `numpy.isnat` on a `datetime64` array. NumPy raises
63/// `TypeError` for any other dtype; in ferray that constraint is
64/// statically enforced — call sites that try to pass a non-time array
65/// fail to type-check.
66///
67/// # Errors
68/// Returns an error only if internal array construction fails.
69pub fn isnat_datetime<D: Dimension>(input: &Array<DateTime64, D>) -> FerrayResult<Array<bool, D>> {
70    let data: Vec<bool> = input.iter().map(|v| v.is_nat()).collect();
71    Array::from_vec(input.dim().clone(), data)
72}
73
74/// Element-wise: true where the input is the NaT sentinel.
75///
76/// Equivalent to `numpy.isnat` on a `timedelta64` array.
77///
78/// # Errors
79/// Returns an error only if internal array construction fails.
80pub fn isnat_timedelta<D: Dimension>(
81    input: &Array<Timedelta64, D>,
82) -> FerrayResult<Array<bool, D>> {
83    let data: Vec<bool> = input.iter().map(|v| v.is_nat()).collect();
84    Array::from_vec(input.dim().clone(), data)
85}
86
87// ===========================================================================
88// Arithmetic kernels
89// ===========================================================================
90
91/// Helper: combine two i64 ticks with NaT propagation. If either side is
92/// NaT, the result is NaT; otherwise the closure runs.
93#[inline]
94fn nat_propagate(a: i64, b: i64, op: impl FnOnce(i64, i64) -> i64) -> i64 {
95    if a == NAT || b == NAT { NAT } else { op(a, b) }
96}
97
98fn check_same_shape<A, B, DA, DB>(
99    a: &Array<A, DA>,
100    b: &Array<B, DB>,
101    name: &str,
102) -> FerrayResult<()>
103where
104    A: ferray_core::Element,
105    B: ferray_core::Element,
106    DA: Dimension,
107    DB: Dimension,
108{
109    if a.shape() != b.shape() {
110        return Err(FerrayError::shape_mismatch(format!(
111            "{name}: shapes {:?} and {:?} differ",
112            a.shape(),
113            b.shape()
114        )));
115    }
116    Ok(())
117}
118
119/// Element-wise `datetime - datetime → timedelta` (same unit assumed).
120///
121/// NaT propagates: if either side is NaT at position `i`, the output is
122/// NaT at that position. Mirrors NumPy's `datetime64 - datetime64`
123/// semantics.
124///
125/// # Errors
126/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
127pub fn sub_datetime<D: Dimension>(
128    a: &Array<DateTime64, D>,
129    b: &Array<DateTime64, D>,
130) -> FerrayResult<Array<Timedelta64, D>> {
131    check_same_shape(a, b, "sub_datetime")?;
132    let data: Vec<Timedelta64> = a
133        .iter()
134        .zip(b.iter())
135        .map(|(x, y)| Timedelta64(nat_propagate(x.0, y.0, |p, q| p.wrapping_sub(q))))
136        .collect();
137    Array::from_vec(a.dim().clone(), data)
138}
139
140/// Element-wise `datetime + timedelta → datetime`.
141///
142/// # Errors
143/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
144pub fn add_datetime_timedelta<D: Dimension>(
145    a: &Array<DateTime64, D>,
146    b: &Array<Timedelta64, D>,
147) -> FerrayResult<Array<DateTime64, D>> {
148    check_same_shape(a, b, "add_datetime_timedelta")?;
149    let data: Vec<DateTime64> = a
150        .iter()
151        .zip(b.iter())
152        .map(|(x, y)| DateTime64(nat_propagate(x.0, y.0, |p, q| p.wrapping_add(q))))
153        .collect();
154    Array::from_vec(a.dim().clone(), data)
155}
156
157/// Element-wise `datetime - timedelta → datetime`.
158///
159/// # Errors
160/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
161pub fn sub_datetime_timedelta<D: Dimension>(
162    a: &Array<DateTime64, D>,
163    b: &Array<Timedelta64, D>,
164) -> FerrayResult<Array<DateTime64, D>> {
165    check_same_shape(a, b, "sub_datetime_timedelta")?;
166    let data: Vec<DateTime64> = a
167        .iter()
168        .zip(b.iter())
169        .map(|(x, y)| DateTime64(nat_propagate(x.0, y.0, |p, q| p.wrapping_sub(q))))
170        .collect();
171    Array::from_vec(a.dim().clone(), data)
172}
173
174/// Element-wise `timedelta + timedelta → timedelta`.
175///
176/// # Errors
177/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
178pub fn add_timedelta<D: Dimension>(
179    a: &Array<Timedelta64, D>,
180    b: &Array<Timedelta64, D>,
181) -> FerrayResult<Array<Timedelta64, D>> {
182    check_same_shape(a, b, "add_timedelta")?;
183    let data: Vec<Timedelta64> = a
184        .iter()
185        .zip(b.iter())
186        .map(|(x, y)| Timedelta64(nat_propagate(x.0, y.0, |p, q| p.wrapping_add(q))))
187        .collect();
188    Array::from_vec(a.dim().clone(), data)
189}
190
191/// Element-wise `timedelta - timedelta → timedelta`.
192///
193/// # Errors
194/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
195pub fn sub_timedelta<D: Dimension>(
196    a: &Array<Timedelta64, D>,
197    b: &Array<Timedelta64, D>,
198) -> FerrayResult<Array<Timedelta64, D>> {
199    check_same_shape(a, b, "sub_timedelta")?;
200    let data: Vec<Timedelta64> = a
201        .iter()
202        .zip(b.iter())
203        .map(|(x, y)| Timedelta64(nat_propagate(x.0, y.0, |p, q| p.wrapping_sub(q))))
204        .collect();
205    Array::from_vec(a.dim().clone(), data)
206}
207
208// ===========================================================================
209// Implicit unit promotion
210// ===========================================================================
211//
212// The promoted-* variants below take both operands together with their
213// units, rescale both to the finer unit using `TimeUnit::finer` /
214// `TimeUnit::scale_to`, and then call the same-unit kernel above. They
215// return `(result, target_unit)` so callers (e.g. DynArray dispatch in
216// ferray-numpy-interop) know which unit to tag the output array with.
217//
218// NaT propagation is preserved: rescaling treats `NAT` as "still NaT"
219// regardless of the multiplicative factor.
220
221#[inline]
222fn rescale_datetime(arr: &[DateTime64], factor: i64) -> Vec<DateTime64> {
223    arr.iter()
224        .map(|v| {
225            if v.is_nat() {
226                DateTime64(NAT)
227            } else {
228                DateTime64(v.0.wrapping_mul(factor))
229            }
230        })
231        .collect()
232}
233
234#[inline]
235fn rescale_timedelta(arr: &[Timedelta64], factor: i64) -> Vec<Timedelta64> {
236    arr.iter()
237        .map(|v| {
238            if v.is_nat() {
239                Timedelta64(NAT)
240            } else {
241                Timedelta64(v.0.wrapping_mul(factor))
242            }
243        })
244        .collect()
245}
246
247/// Element-wise `datetime[unit_a] - datetime[unit_b] → timedelta[finer]`.
248///
249/// Both operands are rescaled to `TimeUnit::finer(unit_a, unit_b)` before
250/// the per-element subtraction. Returns `(result, target_unit)`.
251///
252/// # Errors
253/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
254pub fn sub_datetime_promoted<D: Dimension>(
255    a: &Array<DateTime64, D>,
256    unit_a: TimeUnit,
257    b: &Array<DateTime64, D>,
258    unit_b: TimeUnit,
259) -> FerrayResult<(Array<Timedelta64, D>, TimeUnit)> {
260    check_same_shape(a, b, "sub_datetime_promoted")?;
261    let target = unit_a.finer(unit_b);
262    let scale_a = unit_a.scale_to(target).ok_or_else(|| {
263        FerrayError::invalid_value(format!(
264            "sub_datetime_promoted: cannot rescale {unit_a:?} → {target:?} (non-divisible)"
265        ))
266    })?;
267    let scale_b = unit_b.scale_to(target).ok_or_else(|| {
268        FerrayError::invalid_value(format!(
269            "sub_datetime_promoted: cannot rescale {unit_b:?} → {target:?} (non-divisible)"
270        ))
271    })?;
272    let a_data: Vec<DateTime64> = a.iter().copied().collect();
273    let b_data: Vec<DateTime64> = b.iter().copied().collect();
274    let a_rescaled = if scale_a == 1 {
275        a_data
276    } else {
277        rescale_datetime(&a_data, scale_a)
278    };
279    let b_rescaled = if scale_b == 1 {
280        b_data
281    } else {
282        rescale_datetime(&b_data, scale_b)
283    };
284    let data: Vec<Timedelta64> = a_rescaled
285        .iter()
286        .zip(b_rescaled.iter())
287        .map(|(x, y)| Timedelta64(nat_propagate(x.0, y.0, |p, q| p.wrapping_sub(q))))
288        .collect();
289    let arr = Array::from_vec(a.dim().clone(), data)?;
290    Ok((arr, target))
291}
292
293/// Element-wise `datetime[unit_a] + timedelta[unit_b] → datetime[finer]`.
294///
295/// # Errors
296/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
297pub fn add_datetime_timedelta_promoted<D: Dimension>(
298    a: &Array<DateTime64, D>,
299    unit_a: TimeUnit,
300    b: &Array<Timedelta64, D>,
301    unit_b: TimeUnit,
302) -> FerrayResult<(Array<DateTime64, D>, TimeUnit)> {
303    check_same_shape(a, b, "add_datetime_timedelta_promoted")?;
304    let target = unit_a.finer(unit_b);
305    let scale_a = unit_a.scale_to(target).ok_or_else(|| {
306        FerrayError::invalid_value(format!(
307            "add_datetime_timedelta_promoted: cannot rescale {unit_a:?} → {target:?}"
308        ))
309    })?;
310    let scale_b = unit_b.scale_to(target).ok_or_else(|| {
311        FerrayError::invalid_value(format!(
312            "add_datetime_timedelta_promoted: cannot rescale {unit_b:?} → {target:?}"
313        ))
314    })?;
315    let a_data: Vec<DateTime64> = a.iter().copied().collect();
316    let b_data: Vec<Timedelta64> = b.iter().copied().collect();
317    let a_rescaled = if scale_a == 1 {
318        a_data
319    } else {
320        rescale_datetime(&a_data, scale_a)
321    };
322    let b_rescaled = if scale_b == 1 {
323        b_data
324    } else {
325        rescale_timedelta(&b_data, scale_b)
326    };
327    let data: Vec<DateTime64> = a_rescaled
328        .iter()
329        .zip(b_rescaled.iter())
330        .map(|(x, y)| DateTime64(nat_propagate(x.0, y.0, |p, q| p.wrapping_add(q))))
331        .collect();
332    let arr = Array::from_vec(a.dim().clone(), data)?;
333    Ok((arr, target))
334}
335
336/// Element-wise `timedelta[unit_a] + timedelta[unit_b] → timedelta[finer]`.
337///
338/// # Errors
339/// Returns `FerrayError::ShapeMismatch` if the shapes differ.
340pub fn add_timedelta_promoted<D: Dimension>(
341    a: &Array<Timedelta64, D>,
342    unit_a: TimeUnit,
343    b: &Array<Timedelta64, D>,
344    unit_b: TimeUnit,
345) -> FerrayResult<(Array<Timedelta64, D>, TimeUnit)> {
346    check_same_shape(a, b, "add_timedelta_promoted")?;
347    let target = unit_a.finer(unit_b);
348    let scale_a = unit_a.scale_to(target).ok_or_else(|| {
349        FerrayError::invalid_value(format!(
350            "add_timedelta_promoted: cannot rescale {unit_a:?} → {target:?}"
351        ))
352    })?;
353    let scale_b = unit_b.scale_to(target).ok_or_else(|| {
354        FerrayError::invalid_value(format!(
355            "add_timedelta_promoted: cannot rescale {unit_b:?} → {target:?}"
356        ))
357    })?;
358    let a_data: Vec<Timedelta64> = a.iter().copied().collect();
359    let b_data: Vec<Timedelta64> = b.iter().copied().collect();
360    let a_rescaled = if scale_a == 1 {
361        a_data
362    } else {
363        rescale_timedelta(&a_data, scale_a)
364    };
365    let b_rescaled = if scale_b == 1 {
366        b_data
367    } else {
368        rescale_timedelta(&b_data, scale_b)
369    };
370    let data: Vec<Timedelta64> = a_rescaled
371        .iter()
372        .zip(b_rescaled.iter())
373        .map(|(x, y)| Timedelta64(nat_propagate(x.0, y.0, |p, q| p.wrapping_add(q))))
374        .collect();
375    let arr = Array::from_vec(a.dim().clone(), data)?;
376    Ok((arr, target))
377}
378
379// ===========================================================================
380// timedelta numeric arithmetic (REQ-2, #942)
381// ===========================================================================
382//
383// numpy registers these timedelta numeric loops (generate_umath.py:386-1046):
384//   multiply:     'mq'->'m', 'qm'->'m', 'md'->'m', 'dm'->'m'
385//   divide:       'mq'->'m', 'md'->'m', 'mm'->'d'
386//   floor_divide: 'mq'->'m', 'md'->'m', 'mm'->'q'
387//   remainder:    'mm'->'m'   (no 'mq'/'md' — td % int RAISES)
388// The scalar (int/float) kernels below cover the `mq`/`qm`/`md`/`dm` loops
389// (`mul_timedelta_scalar_*` / `div_timedelta_scalar_*`); the timedelta ÷
390// timedelta kernels cover `mm` (`truediv_timedelta` -> f64,
391// `floordiv_timedelta` -> i64, `mod_timedelta` -> timedelta).
392//
393// Semantics mirror the numpy C loops in
394// numpy/_core/src/umath/loops.c.src EXACTLY (verified live, numpy 2.4.5):
395//   * NaT (`i64::MIN`) propagates: any NaT operand -> NaT result (or `nan`
396//     for the float-ratio loop, or `0` for the floor-divide loop — which is
397//     how numpy's `TIMEDELTA_mm_q_floor_divide` handles NaT, loops.c.src:1110).
398//   * `td * float` / `td / float` cast the double result back to int64 via a
399//     C cast = TRUNCATION TOWARD ZERO (`TIMEDELTA_md_m_multiply`,
400//     loops.c.src:944 `(npy_timedelta)result`), NOT round-half-even; a
401//     non-finite double (inf/nan from `*0`-style overflow or `/0.0`) -> NaT.
402//   * `td / int` is integer division (`in1 / in2`, trunc toward zero); a zero
403//     divisor -> NaT (`TIMEDELTA_mq_m_divide`, loops.c.src:1014).
404//   * `td // td` is FLOOR division (round toward -inf); NaT either operand or a
405//     zero divisor -> `0` (`TIMEDELTA_mm_q_floor_divide`, loops.c.src:1104).
406//   * `td % td` is Python floor-modulo (the sign of the result follows the
407//     DIVISOR); NaT either operand or a zero divisor -> NaT
408//     (`TIMEDELTA_mm_m_remainder`, loops.c.src:1075 "handle mixed case the
409//     way Python does").
410// The `mm` ratio/floor/mod kernels promote both operands to the finer unit
411// first (`TimeUnit::finer` / `scale_to`), matching numpy's common-unit cast.
412
413/// Rescale a timedelta tick to a finer `target` unit, returning the rescaled
414/// `Vec<i64>` ticks (NaT preserved). Used by the `mm` kernels below.
415#[inline]
416fn rescale_td_ticks(arr: &[Timedelta64], factor: i64) -> Vec<i64> {
417    arr.iter()
418        .map(|v| {
419            if v.is_nat() {
420                NAT
421            } else {
422                v.0.wrapping_mul(factor)
423            }
424        })
425        .collect()
426}
427
428/// Promote two timedelta operands to their common finer unit, returning the
429/// rescaled int64 tick buffers `(a_ticks, b_ticks)` and the target unit.
430///
431/// Mirrors numpy's common-unit cast ahead of a `timedelta op timedelta` loop
432/// (`PyUFunc_DivisionTypeResolver` / `PyUFunc_RemainderTypeResolver` promote
433/// to the GCD unit; for the units ferray models that is `TimeUnit::finer`).
434///
435/// # Errors
436/// `FerrayError::ShapeMismatch` if the shapes differ;
437/// `FerrayError::InvalidValue` if a unit cannot be rescaled to the target.
438fn promote_td_pair<D: Dimension>(
439    a: &Array<Timedelta64, D>,
440    unit_a: TimeUnit,
441    b: &Array<Timedelta64, D>,
442    unit_b: TimeUnit,
443    name: &str,
444) -> FerrayResult<(Vec<i64>, Vec<i64>, TimeUnit)> {
445    check_same_shape(a, b, name)?;
446    let target = unit_a.finer(unit_b);
447    let scale_a = unit_a.scale_to(target).ok_or_else(|| {
448        FerrayError::invalid_value(format!("{name}: cannot rescale {unit_a:?} → {target:?}"))
449    })?;
450    let scale_b = unit_b.scale_to(target).ok_or_else(|| {
451        FerrayError::invalid_value(format!("{name}: cannot rescale {unit_b:?} → {target:?}"))
452    })?;
453    let a_data: Vec<Timedelta64> = a.iter().copied().collect();
454    let b_data: Vec<Timedelta64> = b.iter().copied().collect();
455    let a_ticks = if scale_a == 1 {
456        a_data.iter().map(|v| v.0).collect()
457    } else {
458        rescale_td_ticks(&a_data, scale_a)
459    };
460    let b_ticks = if scale_b == 1 {
461        b_data.iter().map(|v| v.0).collect()
462    } else {
463        rescale_td_ticks(&b_data, scale_b)
464    };
465    Ok((a_ticks, b_ticks, target))
466}
467
468/// Element-wise `timedelta * int → timedelta` (same unit; the integer is a
469/// pure scalar multiplier, no unit). NaT propagates.
470///
471/// Mirrors `TIMEDELTA_mq_m_multiply` / `TIMEDELTA_qm_m_multiply`
472/// (loops.c.src:902/918): `in1 * in2`, NaT -> NaT. The result keeps the
473/// timedelta's own unit.
474///
475/// # Errors
476/// Returns an error only if internal array construction fails.
477pub fn mul_timedelta_scalar_i64<D: Dimension>(
478    a: &Array<Timedelta64, D>,
479    k: i64,
480) -> FerrayResult<Array<Timedelta64, D>> {
481    let data: Vec<Timedelta64> = a
482        .iter()
483        .map(|v| {
484            if v.is_nat() {
485                Timedelta64(NAT)
486            } else {
487                Timedelta64(v.0.wrapping_mul(k))
488            }
489        })
490        .collect();
491    Array::from_vec(a.dim().clone(), data)
492}
493
494/// Element-wise `timedelta * float → timedelta` (same unit). The double
495/// product is cast back to int64 via TRUNCATION TOWARD ZERO; a non-finite
496/// product yields NaT. NaT propagates.
497///
498/// Mirrors `TIMEDELTA_md_m_multiply` / `TIMEDELTA_dm_m_multiply`
499/// (loops.c.src:933/954): `result = in1 * in2; isfinite ? (i64)result : NaT`.
500///
501/// # Errors
502/// Returns an error only if internal array construction fails.
503pub fn mul_timedelta_scalar_f64<D: Dimension>(
504    a: &Array<Timedelta64, D>,
505    k: f64,
506) -> FerrayResult<Array<Timedelta64, D>> {
507    let data: Vec<Timedelta64> = a
508        .iter()
509        .map(|v| {
510            if v.is_nat() {
511                Timedelta64(NAT)
512            } else {
513                let r = v.0 as f64 * k;
514                if r.is_finite() {
515                    Timedelta64(r as i64)
516                } else {
517                    Timedelta64(NAT)
518                }
519            }
520        })
521        .collect();
522    Array::from_vec(a.dim().clone(), data)
523}
524
525/// Element-wise `timedelta / int → timedelta` (same unit). Integer division,
526/// truncating toward zero; a zero divisor yields NaT. NaT propagates.
527///
528/// Mirrors `TIMEDELTA_mq_m_divide` (loops.c.src:976): `in1 / in2`, with
529/// `in1 == NaT || in2 == 0 -> NaT`.
530///
531/// # Errors
532/// Returns an error only if internal array construction fails.
533pub fn div_timedelta_scalar_i64<D: Dimension>(
534    a: &Array<Timedelta64, D>,
535    k: i64,
536) -> FerrayResult<Array<Timedelta64, D>> {
537    let data: Vec<Timedelta64> = a
538        .iter()
539        .map(|v| {
540            if v.is_nat() || k == 0 {
541                Timedelta64(NAT)
542            } else {
543                Timedelta64(v.0.wrapping_div(k))
544            }
545        })
546        .collect();
547    Array::from_vec(a.dim().clone(), data)
548}
549
550/// Element-wise `timedelta / float → timedelta` (same unit). The double
551/// quotient is cast back to int64 via TRUNCATION TOWARD ZERO; a non-finite
552/// quotient (e.g. divide-by-zero) yields NaT. NaT propagates.
553///
554/// Mirrors `TIMEDELTA_md_m_divide` (loops.c.src:1025): `result = in1 / in2;
555/// isfinite ? (i64)result : NaT`.
556///
557/// # Errors
558/// Returns an error only if internal array construction fails.
559pub fn div_timedelta_scalar_f64<D: Dimension>(
560    a: &Array<Timedelta64, D>,
561    k: f64,
562) -> FerrayResult<Array<Timedelta64, D>> {
563    let data: Vec<Timedelta64> = a
564        .iter()
565        .map(|v| {
566            if v.is_nat() {
567                Timedelta64(NAT)
568            } else {
569                let r = v.0 as f64 / k;
570                if r.is_finite() {
571                    Timedelta64(r as i64)
572                } else {
573                    Timedelta64(NAT)
574                }
575            }
576        })
577        .collect();
578    Array::from_vec(a.dim().clone(), data)
579}
580
581/// Element-wise `timedelta[ua] / timedelta[ub] → float64` (the ratio). Both
582/// operands are first promoted to the finer common unit; the ratio is then
583/// `a as f64 / b as f64`. NaT in either operand yields `nan`.
584///
585/// Mirrors `TIMEDELTA_mm_d_divide` (loops.c.src:1046): `NaT either -> nan`,
586/// else `(double)in1 / (double)in2` (a zero divisor therefore yields
587/// `±inf`/`nan`, exactly as the IEEE division does — verified live:
588/// `td/td0 -> inf`).
589///
590/// # Errors
591/// `FerrayError::ShapeMismatch` if the shapes differ; `InvalidValue` if a
592/// unit cannot be rescaled to the common finer unit.
593pub fn truediv_timedelta<D: Dimension>(
594    a: &Array<Timedelta64, D>,
595    unit_a: TimeUnit,
596    b: &Array<Timedelta64, D>,
597    unit_b: TimeUnit,
598) -> FerrayResult<Array<f64, D>> {
599    let (at, bt, _target) = promote_td_pair(a, unit_a, b, unit_b, "truediv_timedelta")?;
600    let data: Vec<f64> = at
601        .iter()
602        .zip(bt.iter())
603        .map(|(&x, &y)| {
604            if x == NAT || y == NAT {
605                f64::NAN
606            } else {
607                x as f64 / y as f64
608            }
609        })
610        .collect();
611    Array::from_vec(a.dim().clone(), data)
612}
613
614/// Element-wise `timedelta[ua] // timedelta[ub] → int64` (FLOOR division,
615/// round toward −∞). Both operands are promoted to the finer common unit.
616/// NaT in either operand, or a zero divisor, yields `0`.
617///
618/// Mirrors `TIMEDELTA_mm_q_floor_divide` (loops.c.src:1089): NaT either
619/// operand or a zero divisor -> `0`; otherwise the quotient is corrected
620/// downward for negative results so it floors (loops.c.src:1128).
621/// `i64::div_euclid` does NOT match (it rounds remainder toward +∞); the
622/// floor is computed directly.
623///
624/// # Errors
625/// `FerrayError::ShapeMismatch` if the shapes differ; `InvalidValue` if a
626/// unit cannot be rescaled to the common finer unit.
627pub fn floordiv_timedelta<D: Dimension>(
628    a: &Array<Timedelta64, D>,
629    unit_a: TimeUnit,
630    b: &Array<Timedelta64, D>,
631    unit_b: TimeUnit,
632) -> FerrayResult<Array<i64, D>> {
633    let (at, bt, _target) = promote_td_pair(a, unit_a, b, unit_b, "floordiv_timedelta")?;
634    let data: Vec<i64> = at
635        .iter()
636        .zip(bt.iter())
637        .map(|(&x, &y)| {
638            if x == NAT || y == NAT || y == 0 {
639                0
640            } else {
641                // Floor division (round toward -inf): trunc quotient, then
642                // step down when the signs differ and the division is inexact
643                // (mirrors numpy's loops.c.src:1128 correction).
644                let q = x.wrapping_div(y);
645                if (x % y != 0) && ((x < 0) != (y < 0)) {
646                    q - 1
647                } else {
648                    q
649                }
650            }
651        })
652        .collect();
653    Array::from_vec(a.dim().clone(), data)
654}
655
656/// Element-wise `timedelta[ua] % timedelta[ub] → timedelta[finer]` (Python
657/// floor-modulo: the sign of the result follows the DIVISOR). Both operands
658/// are promoted to the finer common unit; the result carries that unit.
659/// Returns `(result, target_unit)`. NaT in either operand, or a zero divisor,
660/// yields NaT.
661///
662/// Mirrors `TIMEDELTA_mm_m_remainder` (loops.c.src:1061): `rem = in1 % in2`,
663/// then if the operand signs differ and `rem != 0`, `rem + in2` — i.e. Python
664/// floor-modulo (`-7 % 2 == 1`, `7 % -2 == -1`, verified live).
665///
666/// # Errors
667/// `FerrayError::ShapeMismatch` if the shapes differ; `InvalidValue` if a
668/// unit cannot be rescaled to the common finer unit.
669pub fn mod_timedelta<D: Dimension>(
670    a: &Array<Timedelta64, D>,
671    unit_a: TimeUnit,
672    b: &Array<Timedelta64, D>,
673    unit_b: TimeUnit,
674) -> FerrayResult<(Array<Timedelta64, D>, TimeUnit)> {
675    let (at, bt, target) = promote_td_pair(a, unit_a, b, unit_b, "mod_timedelta")?;
676    let data: Vec<Timedelta64> = at
677        .iter()
678        .zip(bt.iter())
679        .map(|(&x, &y)| {
680            if x == NAT || y == NAT || y == 0 {
681                Timedelta64(NAT)
682            } else {
683                let rem = x.wrapping_rem(y);
684                if rem == 0 || ((x > 0) == (y > 0)) {
685                    Timedelta64(rem)
686                } else {
687                    Timedelta64(rem.wrapping_add(y))
688                }
689            }
690        })
691        .collect();
692    let arr = Array::from_vec(a.dim().clone(), data)?;
693    Ok((arr, target))
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use ferray_core::dimension::Ix1;
700
701    #[test]
702    fn isnat_datetime_flags_min_int() {
703        let data = vec![
704            DateTime64(0),
705            DateTime64::nat(),
706            DateTime64(1_700_000_000_000_000_000),
707            DateTime64::nat(),
708        ];
709        let arr = Array::<DateTime64, Ix1>::from_vec(Ix1::new([4]), data).unwrap();
710        let r = isnat_datetime(&arr).unwrap();
711        let v: Vec<bool> = r.iter().copied().collect();
712        assert_eq!(v, vec![false, true, false, true]);
713    }
714
715    #[test]
716    fn isnat_timedelta_flags_min_int() {
717        let data = vec![Timedelta64(1_000), Timedelta64::nat(), Timedelta64(0)];
718        let arr = Array::<Timedelta64, Ix1>::from_vec(Ix1::new([3]), data).unwrap();
719        let r = isnat_timedelta(&arr).unwrap();
720        let v: Vec<bool> = r.iter().copied().collect();
721        assert_eq!(v, vec![false, true, false]);
722    }
723
724    #[test]
725    fn isnat_datetime_all_finite() {
726        let data = vec![DateTime64(1), DateTime64(2), DateTime64(3)];
727        let arr = Array::<DateTime64, Ix1>::from_vec(Ix1::new([3]), data).unwrap();
728        let r = isnat_datetime(&arr).unwrap();
729        let v: Vec<bool> = r.iter().copied().collect();
730        assert_eq!(v, vec![false, false, false]);
731    }
732
733    #[test]
734    fn isnat_datetime_all_nat() {
735        let data = vec![DateTime64::nat(); 5];
736        let arr = Array::<DateTime64, Ix1>::from_vec(Ix1::new([5]), data).unwrap();
737        let r = isnat_datetime(&arr).unwrap();
738        let v: Vec<bool> = r.iter().copied().collect();
739        assert_eq!(v, vec![true; 5]);
740    }
741
742    // ---- Arithmetic kernels ----
743
744    #[test]
745    fn sub_datetime_basic() {
746        let a = Array::<DateTime64, Ix1>::from_vec(
747            Ix1::new([3]),
748            vec![DateTime64(100), DateTime64(200), DateTime64(50)],
749        )
750        .unwrap();
751        let b = Array::<DateTime64, Ix1>::from_vec(
752            Ix1::new([3]),
753            vec![DateTime64(40), DateTime64(150), DateTime64(50)],
754        )
755        .unwrap();
756        let r = sub_datetime(&a, &b).unwrap();
757        let v: Vec<i64> = r.iter().map(|x| x.0).collect();
758        assert_eq!(v, vec![60, 50, 0]);
759    }
760
761    #[test]
762    fn sub_datetime_propagates_nat() {
763        let a = Array::<DateTime64, Ix1>::from_vec(
764            Ix1::new([3]),
765            vec![DateTime64(100), DateTime64::nat(), DateTime64(50)],
766        )
767        .unwrap();
768        let b = Array::<DateTime64, Ix1>::from_vec(
769            Ix1::new([3]),
770            vec![DateTime64(40), DateTime64(150), DateTime64::nat()],
771        )
772        .unwrap();
773        let r = sub_datetime(&a, &b).unwrap();
774        let v: Vec<i64> = r.iter().map(|x| x.0).collect();
775        assert_eq!(v[0], 60);
776        assert_eq!(v[1], NAT);
777        assert_eq!(v[2], NAT);
778    }
779
780    #[test]
781    fn add_datetime_timedelta_basic() {
782        let a = Array::<DateTime64, Ix1>::from_vec(
783            Ix1::new([2]),
784            vec![DateTime64(1000), DateTime64(2000)],
785        )
786        .unwrap();
787        let b = Array::<Timedelta64, Ix1>::from_vec(
788            Ix1::new([2]),
789            vec![Timedelta64(50), Timedelta64::nat()],
790        )
791        .unwrap();
792        let r = add_datetime_timedelta(&a, &b).unwrap();
793        let v: Vec<i64> = r.iter().map(|x| x.0).collect();
794        assert_eq!(v[0], 1050);
795        assert_eq!(v[1], NAT); // NaT propagates
796    }
797
798    #[test]
799    fn sub_datetime_timedelta_basic() {
800        let a = Array::<DateTime64, Ix1>::from_vec(
801            Ix1::new([2]),
802            vec![DateTime64(1000), DateTime64(500)],
803        )
804        .unwrap();
805        let b = Array::<Timedelta64, Ix1>::from_vec(
806            Ix1::new([2]),
807            vec![Timedelta64(100), Timedelta64(50)],
808        )
809        .unwrap();
810        let r = sub_datetime_timedelta(&a, &b).unwrap();
811        let v: Vec<i64> = r.iter().map(|x| x.0).collect();
812        assert_eq!(v, vec![900, 450]);
813    }
814
815    #[test]
816    fn add_sub_timedelta_basic() {
817        let a = Array::<Timedelta64, Ix1>::from_vec(
818            Ix1::new([2]),
819            vec![Timedelta64(10), Timedelta64(20)],
820        )
821        .unwrap();
822        let b = Array::<Timedelta64, Ix1>::from_vec(
823            Ix1::new([2]),
824            vec![Timedelta64(3), Timedelta64(7)],
825        )
826        .unwrap();
827        let s = add_timedelta(&a, &b).unwrap();
828        let sv: Vec<i64> = s.iter().map(|x| x.0).collect();
829        assert_eq!(sv, vec![13, 27]);
830        let d = sub_timedelta(&a, &b).unwrap();
831        let dv: Vec<i64> = d.iter().map(|x| x.0).collect();
832        assert_eq!(dv, vec![7, 13]);
833    }
834
835    #[test]
836    fn arithmetic_shape_mismatch_errors() {
837        let a =
838            Array::<DateTime64, Ix1>::from_vec(Ix1::new([2]), vec![DateTime64(1), DateTime64(2)])
839                .unwrap();
840        let b = Array::<DateTime64, Ix1>::from_vec(
841            Ix1::new([3]),
842            vec![DateTime64(1), DateTime64(2), DateTime64(3)],
843        )
844        .unwrap();
845        assert!(sub_datetime(&a, &b).is_err());
846    }
847
848    // ---- Promoted (mixed-unit) arithmetic ----
849
850    #[test]
851    fn timeunit_finer_picks_finer() {
852        assert_eq!(TimeUnit::Ns.finer(TimeUnit::Us), TimeUnit::Ns);
853        assert_eq!(TimeUnit::Us.finer(TimeUnit::Ns), TimeUnit::Ns);
854        assert_eq!(TimeUnit::S.finer(TimeUnit::Ms), TimeUnit::Ms);
855        assert_eq!(TimeUnit::Ns.finer(TimeUnit::Ns), TimeUnit::Ns);
856    }
857
858    #[test]
859    fn timeunit_scale_to_correct_factors() {
860        assert_eq!(TimeUnit::Us.scale_to(TimeUnit::Ns), Some(1_000));
861        assert_eq!(TimeUnit::S.scale_to(TimeUnit::Us), Some(1_000_000));
862        assert_eq!(TimeUnit::Ns.scale_to(TimeUnit::S), None);
863        assert_eq!(TimeUnit::Ns.scale_to(TimeUnit::Ns), Some(1));
864    }
865
866    #[test]
867    fn sub_datetime_promoted_us_minus_ns() {
868        let a =
869            Array::<DateTime64, Ix1>::from_vec(Ix1::new([2]), vec![DateTime64(5), DateTime64(10)])
870                .unwrap();
871        let b = Array::<DateTime64, Ix1>::from_vec(
872            Ix1::new([2]),
873            vec![DateTime64(1234), DateTime64(7777)],
874        )
875        .unwrap();
876        let (result, unit) = sub_datetime_promoted(&a, TimeUnit::Us, &b, TimeUnit::Ns).unwrap();
877        assert_eq!(unit, TimeUnit::Ns);
878        let v: Vec<i64> = result.iter().map(|x| x.0).collect();
879        assert_eq!(v, vec![5_000 - 1_234, 10_000 - 7_777]);
880    }
881
882    #[test]
883    fn sub_datetime_promoted_propagates_nat() {
884        let a = Array::<DateTime64, Ix1>::from_vec(
885            Ix1::new([2]),
886            vec![DateTime64(5), DateTime64::nat()],
887        )
888        .unwrap();
889        let b = Array::<DateTime64, Ix1>::from_vec(
890            Ix1::new([2]),
891            vec![DateTime64(1000), DateTime64(2000)],
892        )
893        .unwrap();
894        let (result, _) = sub_datetime_promoted(&a, TimeUnit::Us, &b, TimeUnit::Ns).unwrap();
895        let v: Vec<i64> = result.iter().map(|x| x.0).collect();
896        assert_eq!(v[0], 5_000 - 1_000);
897        assert_eq!(v[1], NAT);
898    }
899
900    #[test]
901    fn add_datetime_timedelta_promoted_basic() {
902        let a = Array::<DateTime64, Ix1>::from_vec(Ix1::new([1]), vec![DateTime64(2)]).unwrap();
903        let b = Array::<Timedelta64, Ix1>::from_vec(Ix1::new([1]), vec![Timedelta64(500)]).unwrap();
904        let (result, unit) =
905            add_datetime_timedelta_promoted(&a, TimeUnit::S, &b, TimeUnit::Ms).unwrap();
906        assert_eq!(unit, TimeUnit::Ms);
907        // 2 s = 2000 ms + 500 ms = 2500 ms
908        assert_eq!(result.iter().next().unwrap().0, 2_500);
909    }
910
911    #[test]
912    fn add_timedelta_promoted_basic() {
913        let a = Array::<Timedelta64, Ix1>::from_vec(Ix1::new([1]), vec![Timedelta64(3)]).unwrap();
914        let b = Array::<Timedelta64, Ix1>::from_vec(Ix1::new([1]), vec![Timedelta64(250)]).unwrap();
915        let (result, unit) = add_timedelta_promoted(&a, TimeUnit::S, &b, TimeUnit::Ms).unwrap();
916        assert_eq!(unit, TimeUnit::Ms);
917        assert_eq!(result.iter().next().unwrap().0, 3_250);
918    }
919
920    // ---- Array-level operator overloads ----
921
922    #[test]
923    fn array_sub_datetime_via_operator() {
924        // The operator overload below routes to sub_datetime on `&Array - &Array`.
925        let a = Array::<DateTime64, Ix1>::from_vec(
926            Ix1::new([3]),
927            vec![DateTime64(100), DateTime64(200), DateTime64(50)],
928        )
929        .unwrap();
930        let b = Array::<DateTime64, Ix1>::from_vec(
931            Ix1::new([3]),
932            vec![DateTime64(40), DateTime64(150), DateTime64(50)],
933        )
934        .unwrap();
935        let result = (&a - &b).unwrap();
936        let v: Vec<i64> = result.iter().map(|x| x.0).collect();
937        assert_eq!(v, vec![60, 50, 0]);
938    }
939
940    #[test]
941    fn array_add_datetime_timedelta_via_operator() {
942        let a = Array::<DateTime64, Ix1>::from_vec(
943            Ix1::new([2]),
944            vec![DateTime64(1000), DateTime64(2000)],
945        )
946        .unwrap();
947        let b = Array::<Timedelta64, Ix1>::from_vec(
948            Ix1::new([2]),
949            vec![Timedelta64(50), Timedelta64(75)],
950        )
951        .unwrap();
952        let result = (&a + &b).unwrap();
953        let v: Vec<i64> = result.iter().map(|x| x.0).collect();
954        assert_eq!(v, vec![1050, 2075]);
955    }
956
957    #[test]
958    fn array_timedelta_arith_via_operators() {
959        let a = Array::<Timedelta64, Ix1>::from_vec(
960            Ix1::new([2]),
961            vec![Timedelta64(10), Timedelta64(20)],
962        )
963        .unwrap();
964        let b = Array::<Timedelta64, Ix1>::from_vec(
965            Ix1::new([2]),
966            vec![Timedelta64(3), Timedelta64(7)],
967        )
968        .unwrap();
969        let s = (&a + &b).unwrap();
970        let sv: Vec<i64> = s.iter().map(|x| x.0).collect();
971        assert_eq!(sv, vec![13, 27]);
972        let d = (&a - &b).unwrap();
973        let dv: Vec<i64> = d.iter().map(|x| x.0).collect();
974        assert_eq!(dv, vec![7, 13]);
975    }
976
977    #[test]
978    fn promoted_same_unit_passes_through() {
979        let a = Array::<DateTime64, Ix1>::from_vec(
980            Ix1::new([3]),
981            vec![DateTime64(100), DateTime64(200), DateTime64(50)],
982        )
983        .unwrap();
984        let b = Array::<DateTime64, Ix1>::from_vec(
985            Ix1::new([3]),
986            vec![DateTime64(40), DateTime64(150), DateTime64(50)],
987        )
988        .unwrap();
989        let basic = sub_datetime(&a, &b).unwrap();
990        let (promoted, unit) = sub_datetime_promoted(&a, TimeUnit::Ns, &b, TimeUnit::Ns).unwrap();
991        assert_eq!(unit, TimeUnit::Ns);
992        let bv: Vec<i64> = basic.iter().map(|x| x.0).collect();
993        let pv: Vec<i64> = promoted.iter().map(|x| x.0).collect();
994        assert_eq!(bv, pv);
995    }
996}
997
998#[cfg(test)]
999mod arith_tests {
1000    // timedelta numeric arithmetic (REQ-2, #942). Expected values derived LIVE
1001    // from numpy 2.4.5 (R-CHAR-3): np.timedelta64(6,'D')*3 -> 18; td*float /
1002    // td/float TRUNCATE toward zero; td/td -> float; td//td -> int (floor);
1003    // td%td -> td (Python floor-mod, sign follows divisor); NaT + zero divisor.
1004    use super::*;
1005    use ferray_core::dimension::Ix1;
1006
1007    fn td1(vals: &[i64]) -> Array<Timedelta64, Ix1> {
1008        let data: Vec<Timedelta64> = vals.iter().map(|&v| Timedelta64(v)).collect();
1009        Array::<Timedelta64, Ix1>::from_vec(Ix1::new([vals.len()]), data).unwrap()
1010    }
1011
1012    fn first_td(a: Array<Timedelta64, Ix1>) -> i64 {
1013        a.iter().next().unwrap().0
1014    }
1015
1016    fn first_i64(a: Array<i64, Ix1>) -> i64 {
1017        *a.iter().next().unwrap()
1018    }
1019
1020    #[test]
1021    fn mul_timedelta_scalar_i64_exact() {
1022        // np.timedelta64(6,'D')*3 == timedelta64(18,'D'); NaT propagates.
1023        let r = mul_timedelta_scalar_i64(&td1(&[6, 5, NAT]), 3).unwrap();
1024        let v: Vec<i64> = r.iter().map(|x| x.0).collect();
1025        assert_eq!(v, vec![18, 15, NAT]);
1026    }
1027
1028    #[test]
1029    fn mul_timedelta_scalar_f64_truncates_toward_zero() {
1030        // numpy casts the double product to int64 (trunc toward zero), live:
1031        //   td(5,'D')*1.5 -> 7 (7.5 trunc), td(5,'D')*2.5 -> 12 (12.5 trunc),
1032        //   td(1,'D')*0.5 -> 0, td(1,'D')*-2.9 -> -2.
1033        assert_eq!(
1034            first_td(mul_timedelta_scalar_f64(&td1(&[5]), 1.5).unwrap()),
1035            7
1036        );
1037        assert_eq!(
1038            first_td(mul_timedelta_scalar_f64(&td1(&[5]), 2.5).unwrap()),
1039            12
1040        );
1041        assert_eq!(
1042            first_td(mul_timedelta_scalar_f64(&td1(&[1]), 0.5).unwrap()),
1043            0
1044        );
1045        assert_eq!(
1046            first_td(mul_timedelta_scalar_f64(&td1(&[1]), -2.9).unwrap()),
1047            -2
1048        );
1049        assert_eq!(
1050            first_td(mul_timedelta_scalar_f64(&td1(&[NAT]), 2.0).unwrap()),
1051            NAT
1052        );
1053    }
1054
1055    #[test]
1056    fn div_timedelta_scalar_i64_trunc_and_zero() {
1057        // td(6,'D')/2 -> 3; td(-7,'D')/2 -> -3 (trunc toward zero, live);
1058        // zero divisor -> NaT; NaT -> NaT.
1059        assert_eq!(
1060            first_td(div_timedelta_scalar_i64(&td1(&[6]), 2).unwrap()),
1061            3
1062        );
1063        assert_eq!(
1064            first_td(div_timedelta_scalar_i64(&td1(&[-7]), 2).unwrap()),
1065            -3
1066        );
1067        assert_eq!(
1068            first_td(div_timedelta_scalar_i64(&td1(&[6]), 0).unwrap()),
1069            NAT
1070        );
1071        assert_eq!(
1072            first_td(div_timedelta_scalar_i64(&td1(&[NAT]), 2).unwrap()),
1073            NAT
1074        );
1075    }
1076
1077    #[test]
1078    fn div_timedelta_scalar_f64_trunc_and_nonfinite() {
1079        // td(5,'D')/2.0 -> 2 (2.5 trunc), td(5,'D')/2.5 -> 2; /0.0 -> NaT.
1080        assert_eq!(
1081            first_td(div_timedelta_scalar_f64(&td1(&[5]), 2.0).unwrap()),
1082            2
1083        );
1084        assert_eq!(
1085            first_td(div_timedelta_scalar_f64(&td1(&[5]), 2.5).unwrap()),
1086            2
1087        );
1088        assert_eq!(
1089            first_td(div_timedelta_scalar_f64(&td1(&[5]), 0.0).unwrap()),
1090            NAT
1091        );
1092    }
1093
1094    #[test]
1095    fn truediv_timedelta_ratio_and_nat() {
1096        // td(6,'D')/td(2,'D') -> 3.0; NaT either -> nan; td/td0 -> inf (live).
1097        let r = truediv_timedelta(&td1(&[6]), TimeUnit::D, &td1(&[2]), TimeUnit::D).unwrap();
1098        assert_eq!(*r.iter().next().unwrap(), 3.0);
1099        let rn = truediv_timedelta(&td1(&[NAT]), TimeUnit::D, &td1(&[2]), TimeUnit::D).unwrap();
1100        assert!(rn.iter().next().unwrap().is_nan());
1101        let rz = truediv_timedelta(&td1(&[5]), TimeUnit::D, &td1(&[0]), TimeUnit::D).unwrap();
1102        assert!(rz.iter().next().unwrap().is_infinite());
1103    }
1104
1105    #[test]
1106    fn truediv_timedelta_cross_unit() {
1107        // td(1,'D')/td(12,'h') -> 2.0 (promote D->h: 24h/12h). Live.
1108        let r = truediv_timedelta(&td1(&[1]), TimeUnit::D, &td1(&[12]), TimeUnit::H).unwrap();
1109        assert_eq!(*r.iter().next().unwrap(), 2.0);
1110    }
1111
1112    #[test]
1113    fn floordiv_timedelta_floor_and_zero() {
1114        // td(7,'D')//td(2,'D') -> 3; td(-7,'D')//td(2,'D') -> -4 (floor, live);
1115        // NaT either or zero divisor -> 0.
1116        assert_eq!(
1117            first_i64(
1118                floordiv_timedelta(&td1(&[7]), TimeUnit::D, &td1(&[2]), TimeUnit::D).unwrap()
1119            ),
1120            3
1121        );
1122        assert_eq!(
1123            first_i64(
1124                floordiv_timedelta(&td1(&[-7]), TimeUnit::D, &td1(&[2]), TimeUnit::D).unwrap()
1125            ),
1126            -4
1127        );
1128        assert_eq!(
1129            first_i64(
1130                floordiv_timedelta(&td1(&[5]), TimeUnit::D, &td1(&[0]), TimeUnit::D).unwrap()
1131            ),
1132            0
1133        );
1134        assert_eq!(
1135            first_i64(
1136                floordiv_timedelta(&td1(&[NAT]), TimeUnit::D, &td1(&[2]), TimeUnit::D).unwrap()
1137            ),
1138            0
1139        );
1140    }
1141
1142    #[test]
1143    fn floordiv_timedelta_cross_unit() {
1144        // td(1,'D')//td(5,'h') -> 4 (24h // 5h). Live.
1145        assert_eq!(
1146            first_i64(
1147                floordiv_timedelta(&td1(&[1]), TimeUnit::D, &td1(&[5]), TimeUnit::H).unwrap()
1148            ),
1149            4
1150        );
1151    }
1152
1153    #[test]
1154    fn mod_timedelta_python_floor_mod() {
1155        // td(7,'D')%td(4,'D') -> 3; Python floor-mod: -7%2 -> 1, 7%-2 -> -1
1156        // (sign follows divisor, live); NaT either or zero divisor -> NaT.
1157        let (r, u) = mod_timedelta(&td1(&[7]), TimeUnit::D, &td1(&[4]), TimeUnit::D).unwrap();
1158        assert_eq!(u, TimeUnit::D);
1159        assert_eq!(first_td(r), 3);
1160        assert_eq!(
1161            first_td(
1162                mod_timedelta(&td1(&[-7]), TimeUnit::D, &td1(&[2]), TimeUnit::D)
1163                    .unwrap()
1164                    .0
1165            ),
1166            1
1167        );
1168        assert_eq!(
1169            first_td(
1170                mod_timedelta(&td1(&[7]), TimeUnit::D, &td1(&[-2]), TimeUnit::D)
1171                    .unwrap()
1172                    .0
1173            ),
1174            -1
1175        );
1176        assert_eq!(
1177            first_td(
1178                mod_timedelta(&td1(&[NAT]), TimeUnit::D, &td1(&[2]), TimeUnit::D)
1179                    .unwrap()
1180                    .0
1181            ),
1182            NAT
1183        );
1184        assert_eq!(
1185            first_td(
1186                mod_timedelta(&td1(&[5]), TimeUnit::D, &td1(&[0]), TimeUnit::D)
1187                    .unwrap()
1188                    .0
1189            ),
1190            NAT
1191        );
1192    }
1193
1194    #[test]
1195    fn mod_timedelta_cross_unit() {
1196        // td(1,'D')%td(5,'h') -> timedelta64(4,'h') (24h % 5h = 4h, finer unit).
1197        let (r, u) = mod_timedelta(&td1(&[1]), TimeUnit::D, &td1(&[5]), TimeUnit::H).unwrap();
1198        assert_eq!(u, TimeUnit::H);
1199        assert_eq!(first_td(r), 4);
1200    }
1201}