279279 @test Dates. DateTime (s2) == DateTime (1970 , 1 , 1 , 1 , 0 , 0 )
280280end
281281
282- @testitem " DateTime64 from DateTime" begin
282+ @testitem " DateTime64 from Dates. DateTime" begin
283283 using Dates
284284 using PythonCall: NumpyDates
285285
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
447448end
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 "\1 PythonCall.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
0 commit comments