@@ -607,3 +607,101 @@ def test_batch_split_independence(self):
607607 hash_split = digester .finalize ()
608608
609609 assert hash_combined == hash_split
610+
611+
612+ # ---------------------------------------------------------------------------
613+ # Timestamp and Duration hashing — golden values match Rust (ITL-438)
614+ # ---------------------------------------------------------------------------
615+
616+
617+ class TestTimestampDurationHashing :
618+ """Golden-hash parity for timestamp and duration types.
619+
620+ All expected values were generated by the Rust starfix test suite
621+ (timestamp_array_hashing / duration_array_hashing in tests/arrow_digester.rs).
622+ Array contents: [0, None, 1_000] as raw i64 values.
623+ """
624+
625+ # -- Timestamp (tz-aware UTC) ------------------------------------------
626+
627+ def test_timestamp_second_utc (self ):
628+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("s" , tz = "UTC" ))
629+ assert ArrowDigester .hash_array (arr ).hex () == "0000017cef6166a14741f94906bd3146ffa197d1b5756d594eb46304b678b21e44c40b"
630+
631+ def test_timestamp_millisecond_utc (self ):
632+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("ms" , tz = "UTC" ))
633+ assert ArrowDigester .hash_array (arr ).hex () == "000001fd253a835218d5605ce8bb348699f973b93e29fbe21fe6e532468f988ece4ce5"
634+
635+ def test_timestamp_microsecond_utc (self ):
636+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("us" , tz = "UTC" ))
637+ assert ArrowDigester .hash_array (arr ).hex () == "0000013af741ca0fe5bd48d0bd39e1993c60e7c032f23c4cf4be6c11cc4b50ce56013e"
638+
639+ def test_timestamp_nanosecond_utc (self ):
640+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("ns" , tz = "UTC" ))
641+ assert ArrowDigester .hash_array (arr ).hex () == "000001f5b86904b4b7f2c6a18c7c4febda142bf622e4d01752f42b8f832c0a68a4d9c0"
642+
643+ # -- Timestamp (tz-naive) ----------------------------------------------
644+
645+ def test_timestamp_second_naive (self ):
646+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("s" ))
647+ assert ArrowDigester .hash_array (arr ).hex () == "0000016ee5249d352c03cc820af39e98b7043993d467207cb6ca34c8d13cfacf141e30"
648+
649+ def test_timestamp_millisecond_naive (self ):
650+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("ms" ))
651+ assert ArrowDigester .hash_array (arr ).hex () == "000001ef46619d2be8259335ed0943f42ea3ced19ff6d75471a64b2f03aea89afa4842"
652+
653+ def test_timestamp_microsecond_naive (self ):
654+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("us" ))
655+ assert ArrowDigester .hash_array (arr ).hex () == "0000017e624eb2847f79f940d7123980b076aea1c1ecd3adaa34bdae24fa00f05afda3"
656+
657+ def test_timestamp_nanosecond_naive (self ):
658+ arr = pa .array ([0 , None , 1_000 ], type = pa .timestamp ("ns" ))
659+ assert ArrowDigester .hash_array (arr ).hex () == "000001f677ef0e42e9e40ea092471f82283212833ab6b4ff9bb45e86e88590f8d15796"
660+
661+ # -- Timestamp: unit and tz produce different hashes -------------------
662+
663+ def test_timestamp_units_differ (self ):
664+ values = [1_000 , 2_000 ]
665+ hashes = [
666+ ArrowDigester .hash_array (pa .array (values , type = pa .timestamp ("s" ))).hex (),
667+ ArrowDigester .hash_array (pa .array (values , type = pa .timestamp ("ms" ))).hex (),
668+ ArrowDigester .hash_array (pa .array (values , type = pa .timestamp ("us" ))).hex (),
669+ ArrowDigester .hash_array (pa .array (values , type = pa .timestamp ("ns" ))).hex (),
670+ ]
671+ assert len (set (hashes )) == 4 , "all 4 timestamp units must produce distinct hashes"
672+
673+ def test_timestamp_tz_differs (self ):
674+ values = [1_000 , 2_000 ]
675+ naive = ArrowDigester .hash_array (pa .array (values , type = pa .timestamp ("us" ))).hex ()
676+ utc = ArrowDigester .hash_array (
677+ pa .array (values , type = pa .timestamp ("us" , tz = "UTC" ))
678+ ).hex ()
679+ assert naive != utc , "tz-naive and tz=UTC must produce different hashes"
680+
681+ # -- Duration ----------------------------------------------------------
682+
683+ def test_duration_second (self ):
684+ arr = pa .array ([0 , None , 1_000 ], type = pa .duration ("s" ))
685+ assert ArrowDigester .hash_array (arr ).hex () == "0000013540d23a4abf1dfbc939a9e5514be69e364cec466ef900fa35050dc3cf2994fa"
686+
687+ def test_duration_millisecond (self ):
688+ arr = pa .array ([0 , None , 1_000 ], type = pa .duration ("ms" ))
689+ assert ArrowDigester .hash_array (arr ).hex () == "000001564a87e1e07898af07a4a212e3db83f911ac287134f28c772bf5fb4da683402c"
690+
691+ def test_duration_microsecond (self ):
692+ arr = pa .array ([0 , None , 1_000 ], type = pa .duration ("us" ))
693+ assert ArrowDigester .hash_array (arr ).hex () == "0000017904faf3043cf870f0c139cba0282cdaf11c590aeb80f2a0d6b103893ba5da2a"
694+
695+ def test_duration_nanosecond (self ):
696+ arr = pa .array ([0 , None , 1_000 ], type = pa .duration ("ns" ))
697+ assert ArrowDigester .hash_array (arr ).hex () == "00000189a38c5a7adf4b19b3e0d467f042ab2389fed8ae5f1d2af8555dd6131478a49b"
698+
699+ def test_duration_units_differ (self ):
700+ values = [1_000 , 2_000 ]
701+ hashes = [
702+ ArrowDigester .hash_array (pa .array (values , type = pa .duration ("s" ))).hex (),
703+ ArrowDigester .hash_array (pa .array (values , type = pa .duration ("ms" ))).hex (),
704+ ArrowDigester .hash_array (pa .array (values , type = pa .duration ("us" ))).hex (),
705+ ArrowDigester .hash_array (pa .array (values , type = pa .duration ("ns" ))).hex (),
706+ ]
707+ assert len (set (hashes )) == 4 , "all 4 duration units must produce distinct hashes"
0 commit comments