Skip to content

Commit 34c5662

Browse files
author
Christopher Doris
committed
test(numpydates): add tests for timedelta64
1 parent 7f2a0b5 commit 34c5662

File tree

5 files changed

+292
-4
lines changed

5 files changed

+292
-4
lines changed

src/NumpyDates/DateTime64.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ function Base.show(io::IO, d::DateTime64)
138138
show(io, typeof(d))
139139
print(io, "(")
140140
showvalue(io, d)
141-
print(io, ", ", unitparam(unitpair(d)), ")")
141+
print(io, ", ")
142+
show(io, unitparam(unitpair(d)))
143+
print(io, ")")
142144
end
143145
nothing
144146
end

src/NumpyDates/InlineTimeDelta64.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ function InlineTimeDelta64{U}(v::Dates.Period) where {U}
4040
InlineTimeDelta64{U}(value(TimeDelta64(v, U)))
4141
end
4242

43+
function InlineTimeDelta64{U}(v::Integer) where {U}
44+
InlineTimeDelta64{U}(convert(Int, v))
45+
end
46+
4347
function InlineTimeDelta64(v::AbstractTimeDelta64, u::UnitArg = defaultunit(v))
4448
InlineTimeDelta64{unitparam(u)}(v)
4549
end
@@ -52,6 +56,10 @@ function InlineTimeDelta64(v::Dates.Period, u::UnitArg = defaultunit(v))
5256
InlineTimeDelta64{unitparam(u)}(v)
5357
end
5458

59+
function InlineTimeDelta64(v::Integer, u::UnitArg)
60+
InlineTimeDelta64{unitparam(u)}(v)
61+
end
62+
5563
# show
5664

5765
function Base.show(io::IO, d::InlineTimeDelta64)

src/NumpyDates/TimeDelta64.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ function Base.show(io::IO, d::TimeDelta64)
145145
show(io, typeof(d))
146146
print(io, "(")
147147
showvalue(io, d)
148-
print(io, ", ", unitparam(unitpair(d)), ")")
148+
print(io, ", ")
149+
show(io, unitparam(unitpair(d)))
150+
print(io, ")")
149151
end
150152
nothing
151153
end

test/NumpyDates.jl

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ end
279279
@test Dates.DateTime(s2) == DateTime(1970, 1, 1, 1, 0, 0)
280280
end
281281

282-
@testitem "DateTime64 from DateTime" begin
282+
@testitem "DateTime64 from Dates.DateTime" begin
283283
using Dates
284284
using PythonCall: NumpyDates
285285

@@ -434,7 +434,8 @@ end
434434

435435
# default show checks
436436
s_def = show_string(x)
437-
@test s_def == "PythonCall.NumpyDates.DateTime64($expected_val, $ustr)"
437+
@test s_def ==
438+
"PythonCall.NumpyDates.DateTime64($expected_val, PythonCall.NumpyDates.$ustr)"
438439

439440
# and again with InlineDateTime64
440441
x2 = NumpyDates.InlineDateTime64(v, usym)
@@ -445,3 +446,226 @@ end
445446
"PythonCall.NumpyDates.InlineDateTime64{PythonCall.NumpyDates.$ustr}($expected_val)"
446447
end
447448
end
449+
450+
@testitem "TimeDelta64 from Dates.Period" begin
451+
using Dates
452+
using PythonCall: NumpyDates
453+
454+
# Data generated by: uv run test/scripts/np_timedeltas.py
455+
# Representative cases drawn from the script output.
456+
# Each case is: (period, unit_symbol, expected_numpy_integer)
457+
cases = [
458+
# negative one day
459+
(Day(-1), :W, -1),
460+
(Day(-1), :D, -1),
461+
(Day(-1), :h, -24),
462+
(Day(-1), :m, -1440),
463+
(Day(-1), :s, -86400),
464+
(Day(-1), :ms, -86400000),
465+
(Day(-1), :us, -86400000000),
466+
(Day(-1), :ns, -86400000000000),
467+
# negative one week
468+
(Week(-1), :W, -1),
469+
(Week(-1), :D, -7),
470+
(Week(-1), :h, -168),
471+
(Week(-1), :m, -10080),
472+
(Week(-1), :s, -604800),
473+
(Week(-1), :ms, -604800000),
474+
(Week(-1), :us, -604800000000),
475+
(Week(-1), :ns, -604800000000000),
476+
# sub-second basis
477+
(Millisecond(-1000), :s, -1),
478+
(Microsecond(-1_000_000), :s, -1),
479+
(Nanosecond(-1_000_000_000), :s, -1),
480+
(Second(-3600), :h, -1),
481+
(Minute(-60), :h, -1),
482+
# positive unit-expansion
483+
(Second(1), :ms, 1_000),
484+
(Second(1), :us, 1_000_000),
485+
(Second(1), :ns, 1_000_000_000),
486+
(Second(1), :ps, 1_000_000_000_000),
487+
(Second(1), :fs, 1_000_000_000_000_000),
488+
(Second(1), :as, 1_000_000_000_000_000_000),
489+
(Nanosecond(1), :ns, 1),
490+
(Nanosecond(1), :ps, 1_000),
491+
(Nanosecond(1), :fs, 1_000_000),
492+
(Nanosecond(1), :as, 1_000_000_000),
493+
# mixed scale-ups
494+
(Millisecond(1_000), :s, 1),
495+
(Microsecond(1_000_000), :s, 1),
496+
(Nanosecond(1_000_000_000), :s, 1),
497+
# calendar-like periods (handled specially)
498+
(Month(12), :M, 12),
499+
(Year(1), :Y, 1),
500+
(Year(2), :M, 24),
501+
]
502+
503+
@testset "$p => $usym" for (p, usym, expected) in cases
504+
# TimeDelta64 from Dates.Period
505+
td = NumpyDates.TimeDelta64(p, usym)
506+
@test Dates.value(td) == expected
507+
508+
# Inline typed
509+
Uconst = NumpyDates.Unit(usym)
510+
inline_typed = NumpyDates.InlineTimeDelta64{Uconst}(p)
511+
@test Dates.value(inline_typed) == expected
512+
513+
# Inline dynamic
514+
inline_dyn = NumpyDates.InlineTimeDelta64(p, usym)
515+
@test Dates.value(inline_dyn) == expected
516+
@test NumpyDates.unitpair(inline_dyn) == NumpyDates.unitpair(usym)
517+
end
518+
end
519+
520+
@testitem "TimeDelta64 from String (NaT only)" begin
521+
using Dates
522+
using PythonCall: NumpyDates
523+
524+
# Only NaT-like strings are supported for TimeDelta64(string, unit)
525+
@testset "$nat -> $u" for nat in ("NaT", "nan", "NAN"),
526+
u in (:W, :D, :h, :m, :s, :ms, :us, :ns, :ps, :fs, :as, :M, :Y)
527+
528+
x = NumpyDates.TimeDelta64(nat, u)
529+
@test isnan(x)
530+
@test NumpyDates.unitpair(x) == NumpyDates.unitpair(u)
531+
532+
# Inline typed and dynamic also accept NaT strings
533+
Uconst = NumpyDates.Unit(u)
534+
xi = NumpyDates.InlineTimeDelta64{Uconst}(nat)
535+
@test isnan(xi)
536+
xid = NumpyDates.InlineTimeDelta64(nat, u)
537+
@test isnan(xid)
538+
@test NumpyDates.unitpair(xid) == NumpyDates.unitpair(u)
539+
end
540+
end
541+
542+
@testitem "TimeDelta64 from AbstractTimeDelta64" begin
543+
using Dates
544+
using PythonCall: NumpyDates
545+
546+
# Pass-through when units match
547+
base = NumpyDates.TimeDelta64(Day(1), :D)
548+
y = NumpyDates.TimeDelta64(base, :D)
549+
@test Dates.value(y) == 1
550+
@test NumpyDates.unitpair(y) == NumpyDates.unitpair(:D)
551+
552+
# NaT changes unit and remains NaT
553+
nat_td = NumpyDates.TimeDelta64("NaT", :s)
554+
z = NumpyDates.TimeDelta64(nat_td, :ns)
555+
@test isnan(z)
556+
@test NumpyDates.unitpair(z) == NumpyDates.unitpair(:ns)
557+
end
558+
559+
@testitem "TimeDelta64 from Integer" begin
560+
using Dates
561+
using PythonCall: NumpyDates
562+
563+
# Raw constructor: value and unit
564+
x = NumpyDates.TimeDelta64(3_600, :s)
565+
@test Dates.value(x) == 3_600
566+
567+
# Convert to nanoseconds via Dates to sanity check: 3_600 s -> 3_600_000_000_000 ns
568+
# Note: TimeDelta64 is a Period; we check only value here and rely on conversion tests above.
569+
570+
# Inline typed raw value
571+
s1 = NumpyDates.InlineTimeDelta64{NumpyDates.SECONDS}(3_600)
572+
@test Dates.value(s1) == 3_600
573+
574+
# Inline with multiplier in unit parameter
575+
s2 = NumpyDates.InlineTimeDelta64{(NumpyDates.SECONDS, 2)}(1_800) # 1_800 * 2s = 3_600s
576+
@test Dates.value(s2) == 1_800
577+
end
578+
579+
@testitem "TimeDelta64 show" begin
580+
using Dates
581+
using PythonCall: NumpyDates
582+
583+
# Helper to get "showvalue" form by setting :typeinfo to the concrete type
584+
function showvalue_string(x)
585+
io = IOBuffer()
586+
show(IOContext(io, :typeinfo => typeof(x)), x)
587+
String(take!(io))
588+
end
589+
590+
# Helper to get the default Base.show output (with type wrapper)
591+
function show_string(x)
592+
io = IOBuffer()
593+
show(io, x)
594+
String(take!(io))
595+
end
596+
597+
# Cases: (value_or_nat, unit_symbol_or_tuple, expected_value_string, expected_unit_string_for_default_show, is_inline_tuple?)
598+
# - expected_value_string matches showvalue_string output (integers unquoted; NaT quoted)
599+
# - expected_unit_string_for_default_show is the rhs inside the default show, e.g. "SECONDS" or "(SECONDS, 2)"
600+
cases = [
601+
# simple seconds
602+
(0, :s, "0", "SECONDS", false),
603+
(3_600, :s, "3600", "SECONDS", false),
604+
(-86_400, :s, "-86400", "SECONDS", false),
605+
606+
# micro/nano/pico/femto/atto seconds
607+
(1, :us, "1", "MICROSECONDS", false),
608+
(1, :ns, "1", "NANOSECONDS", false),
609+
(1, :ps, "1", "PICOSECONDS", false),
610+
(1, :fs, "1", "FEMTOSECONDS", false),
611+
(1, :as, "1", "ATTOSECONDS", false),
612+
613+
# minutes/hours/days/weeks
614+
(60, :m, "60", "MINUTES", false),
615+
(24, :h, "24", "HOURS", false),
616+
(7, :D, "7", "DAYS", false),
617+
(2, :W, "2", "WEEKS", false),
618+
619+
# calendar units (identity semantics for "value" field)
620+
(12, :M, "12", "MONTHS", false),
621+
(1, :Y, "1", "YEARS", false),
622+
623+
# NaT
624+
("NaT", :ns, "\"NaT\"", "NANOSECONDS", false),
625+
626+
# multiplier tuple unit for TimeDelta64 and InlineTimeDelta64
627+
(1_800, (:s, 2), "1800", "(SECONDS, 2)", true),
628+
]
629+
630+
@testset "$v $u" for (v, u, expected_val, ustr, is_tuple) in cases
631+
# Construct TimeDelta64
632+
td = NumpyDates.TimeDelta64(v, u)
633+
634+
# showvalue checks (integers unquoted; NaT quoted)
635+
s_val = showvalue_string(td)
636+
@test s_val == expected_val
637+
638+
# default show checks
639+
s_def = show_string(td)
640+
@test s_def ==
641+
"PythonCall.NumpyDates.TimeDelta64($expected_val, $(replace(ustr, r"^(\(?)" => s"\1PythonCall.NumpyDates.")))"
642+
643+
# InlineTimeDelta64 forms
644+
# Dynamic (value, unit)
645+
inline_dyn = NumpyDates.InlineTimeDelta64(v, u)
646+
s_val2 = showvalue_string(inline_dyn)
647+
@test s_val2 == expected_val
648+
s_def2 = show_string(inline_dyn)
649+
# Inline default show embeds the fully-qualified unit in the type parameter.
650+
if is_tuple
651+
@test s_def2 ==
652+
"PythonCall.NumpyDates.InlineTimeDelta64{(PythonCall.NumpyDates.SECONDS, 2)}($expected_val)"
653+
else
654+
@test s_def2 ==
655+
"PythonCall.NumpyDates.InlineTimeDelta64{PythonCall.NumpyDates.$ustr}($expected_val)"
656+
end
657+
658+
# Typed Inline
659+
Uconst = u isa Tuple ? (NumpyDates.Unit(u[1]), u[2]) : NumpyDates.Unit(u)
660+
inline_typed = NumpyDates.InlineTimeDelta64{Uconst}(v)
661+
@test showvalue_string(inline_typed) == expected_val
662+
s_def3 = show_string(inline_typed)
663+
if is_tuple
664+
@test s_def3 ==
665+
"PythonCall.NumpyDates.InlineTimeDelta64{(PythonCall.NumpyDates.SECONDS, 2)}($expected_val)"
666+
else
667+
@test s_def3 ==
668+
"PythonCall.NumpyDates.InlineTimeDelta64{PythonCall.NumpyDates.$ustr}($expected_val)"
669+
end
670+
end
671+
end

test/scripts/np_timedeltas.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python
2+
import numpy as np
3+
4+
# Base periods to generate from, as (value, base_unit)
5+
# Avoid calendar 'Y'/'M' as base for numpy timedelta (ambiguous); handle those separately in tests.
6+
base_periods = [
7+
(-1_000_000_000, "ns"),
8+
(-1_000_000, "us"),
9+
(-1_000, "ms"),
10+
(-3600, "s"),
11+
(-60, "m"),
12+
(-1, "h"),
13+
(-1, "D"),
14+
(-1, "W"),
15+
(0, "ns"),
16+
(1, "ns"),
17+
(1, "us"),
18+
(1, "ms"),
19+
(1, "s"),
20+
(60, "s"),
21+
(3600, "s"),
22+
(1, "m"),
23+
(1, "h"),
24+
(1, "D"),
25+
(7, "D"),
26+
(2, "W"),
27+
(1_000, "ms"),
28+
(1_000_000, "us"),
29+
(1_000_000_000, "ns"),
30+
]
31+
32+
# Target units to cast to, including sub-ns
33+
targets = ["W", "D", "h", "m", "s", "ms", "us", "ns", "ps", "fs", "as"]
34+
35+
# Output format:
36+
# "<VALUE> <BASE_UNIT> -> <TARGET_UNIT> <INT_VALUE>"
37+
for v, b in base_periods:
38+
tb = np.timedelta64(v, b)
39+
for t in targets:
40+
try:
41+
out = int(tb.astype(f"timedelta64[{t}]").astype("int64"))
42+
print(f"{v} {b} -> {t} {out}")
43+
except Exception:
44+
# Some casts may be invalid in older numpy; skip
45+
pass
46+
47+
# Calendar-like units for numpy timedelta: 'M' (months) and 'Y' (years) exist, but semantics differ.
48+
# We only provide identity conversions here to validate unit-count semantics where appropriate.
49+
for v in [-100, -12, -1, 0, 1, 12, 100]:
50+
print(f"{v} M -> M {v}")
51+
for v in [-100, -1, 0, 1, 100]:
52+
print(f"{v} Y -> Y {v}")

0 commit comments

Comments
 (0)