6
6
7
7
use std:: { fmt:: Debug , marker:: PhantomData } ;
8
8
9
+ use calendrical_calculations:: rata_die:: RataDie ;
9
10
use potential_utf:: PotentialUtf16 ;
10
11
use resb:: binary:: BinaryDeserializerError ;
11
12
use serde:: Deserialize ;
@@ -57,25 +58,25 @@ impl Debug for TzZoneData<'_> {
57
58
}
58
59
59
60
write ! ( f, "transitions/offsets: [" ) ?;
60
- write ! ( f, "{:?}, " , self . type_offsets[ 0 ] ) ?;
61
+ let ( std, rule) = self . type_offsets [ 0 ] ;
62
+ write ! ( f, "{:?}, " , ( std as f64 / 3600.0 , rule as f64 / 3600.0 ) ) ?;
61
63
let mut i = 0 ;
62
- for & ( lo, hi) in self . trans_pre32 {
63
- dbg_timestamp ( f, ( lo as i64 ) << 32 | hi as i64 ) ?;
64
- write ! ( f, "{:?}, " , self . type_offsets[ self . type_map[ i] as usize ] ) ?;
64
+ for & ( hi, lo) in self . trans_pre32 {
65
+ dbg_timestamp ( f, ( ( hi as u32 as u64 ) << 32 | ( lo as u32 as u64 ) ) as i64 ) ?;
66
+ let ( std, rule) = self . type_offsets [ self . type_map [ i] as usize ] ;
67
+ write ! ( f, "{:?}, " , ( std as f64 / 3600.0 , rule as f64 / 3600.0 ) ) ?;
65
68
i += 1 ;
66
69
}
67
70
for & t in self . trans {
68
71
dbg_timestamp ( f, t as i64 ) ?;
69
72
let ( std, rule) = self . type_offsets [ self . type_map [ i] as usize ] ;
70
- let std = std as f64 / 3600.0 ;
71
- let rule = rule as f64 / 3600.0 ;
72
- write ! ( f, "{:?}, " , ( std, rule) ) ?;
73
+ write ! ( f, "{:?}, " , ( std as f64 / 3600.0 , rule as f64 / 3600.0 ) ) ?;
73
74
i += 1 ;
74
75
}
75
-
76
- for & ( lo , hi ) in self . trans_post32 {
77
- dbg_timestamp ( f , ( lo as i64 ) << 32 | hi as i64 ) ? ;
78
- write ! ( f, "{:?}, " , self . type_offsets [ self . type_map [ i ] as usize ] ) ?;
76
+ for & ( hi , lo ) in self . trans_post32 {
77
+ dbg_timestamp ( f , ( ( hi as u32 as u64 ) << 32 | ( lo as u32 as u64 ) ) as i64 ) ? ;
78
+ let ( std , rule ) = self . type_offsets [ self . type_map [ i ] as usize ] ;
79
+ write ! ( f, "{:?}, " , ( std as f64 / 3600.0 , rule as f64 / 3600.0 ) ) ?;
79
80
i += 1 ;
80
81
}
81
82
write ! ( f, "], " ) ?;
@@ -423,16 +424,23 @@ struct Rule<'a> {
423
424
inner : & ' a TzRule ,
424
425
}
425
426
426
- #[ derive( Debug , Clone , Copy ) ]
427
- pub struct Transition {
428
- pub transition : i64 ,
427
+ #[ derive( Debug , Clone , Copy , PartialEq ) ]
428
+ pub struct Offset {
429
+ pub since : i64 ,
429
430
pub offset : UtcOffset ,
430
431
pub rule_applies : bool ,
431
432
}
432
433
434
+ pub enum PossibleOffset {
435
+ Single ( Offset ) ,
436
+ Ambiguous ( Offset , Offset ) ,
437
+ None ,
438
+ }
439
+
433
440
impl Zone < ' _ > {
434
- fn previous_transition ( & self , seconds_since_epoch : i64 ) -> Transition {
435
- let idx = if seconds_since_epoch < i32:: MIN as i64 {
441
+ // Returns the index of the previous transition. As this can be -1, it returns an isize.
442
+ fn offset_idx ( & self , seconds_since_epoch : i64 ) -> isize {
443
+ if seconds_since_epoch < i32:: MIN as i64 {
436
444
self . simple
437
445
. trans_pre32
438
446
. binary_search ( & (
@@ -464,14 +472,16 @@ impl Zone<'_> {
464
472
. map ( |i| i as isize )
465
473
// binary_search returns the index of the next (higher) transition, so we subtract one
466
474
. unwrap_or_else ( |i| i as isize - 1 )
467
- } ;
475
+ }
476
+ }
468
477
478
+ fn offset_at ( & self , idx : isize , seconds_since_epoch : i64 ) -> Offset {
469
479
// before first transition don't use `type_map`, just the first entry in `type_offsets`
470
- if idx == - 1 {
480
+ if idx < 0 || self . simple . type_map . is_empty ( ) {
471
481
#[ expect( clippy:: unwrap_used) ] // type_offsets non-empty by invariant
472
482
let & ( standard, rule_additional) = self . simple . type_offsets . first ( ) . unwrap ( ) ;
473
- return Transition {
474
- transition : i64:: MIN ,
483
+ return Offset {
484
+ since : i64:: MIN ,
475
485
offset : UtcOffset :: from_seconds_unchecked ( standard + rule_additional) ,
476
486
rule_applies : rule_additional > 0 ,
477
487
} ;
@@ -480,13 +490,13 @@ impl Zone<'_> {
480
490
let idx = idx as usize ;
481
491
482
492
// after the last transition, respect the rule
483
- if idx = = self . simple . type_map . len ( ) - 1 {
493
+ if idx > = self . simple . type_map . len ( ) - 1 {
484
494
if let Some ( rule) = self . final_rule . as_ref ( ) {
485
495
let ( additional_offset_seconds, transition) = rule
486
496
. inner
487
497
. additional_offset_since ( seconds_since_epoch, rule. start_year ) ;
488
- return Transition {
489
- transition,
498
+ return Offset {
499
+ since : transition,
490
500
rule_applies : additional_offset_seconds != 0 ,
491
501
offset : UtcOffset :: from_seconds_unchecked (
492
502
rule. standard_offset_seconds + additional_offset_seconds,
@@ -495,29 +505,102 @@ impl Zone<'_> {
495
505
}
496
506
}
497
507
508
+ let idx = core:: cmp:: min ( idx, self . simple . type_map . len ( ) - 1 ) ;
509
+
498
510
#[ expect( clippy:: indexing_slicing) ]
499
511
// type_map has length sum(trans*), and type_map values are validated to be valid indices in type_offsets
500
512
let ( standard, rule_additional) =
501
513
self . simple . type_offsets [ self . simple . type_map [ idx] as usize ] ;
502
514
503
515
#[ expect( clippy:: indexing_slicing) ] // by guards or invariant
504
- let transition = if idx < self . simple . trans_pre32 . len ( ) {
505
- let ( lo , hi ) = self . simple . trans_pre32 [ idx] ;
506
- ( lo as i64 ) << 32 | hi as i64
516
+ let since = if idx < self . simple . trans_pre32 . len ( ) {
517
+ let ( hi , lo ) = self . simple . trans_pre32 [ idx] ;
518
+ ( ( hi as u32 as u64 ) << 32 | ( lo as u32 as u64 ) ) as i64
507
519
} else if idx - self . simple . trans_pre32 . len ( ) < self . simple . trans . len ( ) {
508
520
self . simple . trans [ idx - self . simple . trans_pre32 . len ( ) ] as i64
509
521
} else {
510
- let ( lo , hi ) = self . simple . trans_post32
522
+ let ( hi , lo ) = self . simple . trans_post32
511
523
[ idx - self . simple . trans_pre32 . len ( ) - self . simple . trans . len ( ) ] ;
512
- ( lo as i64 ) << 32 | hi as i64
524
+ ( ( hi as u32 as u64 ) << 32 | ( lo as u32 as u64 ) ) as i64
513
525
} ;
514
526
515
- Transition {
516
- transition ,
527
+ Offset {
528
+ since ,
517
529
offset : UtcOffset :: from_seconds_unchecked ( standard + rule_additional) ,
518
530
rule_applies : rule_additional > 0 ,
519
531
}
520
532
}
533
+
534
+ pub fn for_date_time (
535
+ & self ,
536
+ year : i32 ,
537
+ month : u8 ,
538
+ day : u8 ,
539
+ hour : u8 ,
540
+ minute : u8 ,
541
+ second : u8 ,
542
+ ) -> PossibleOffset {
543
+ const EPOCH : RataDie = calendrical_calculations:: iso:: const_fixed_from_iso ( 1970 , 1 , 1 ) ;
544
+ let seconds_since_local_epoch =
545
+ ( ( ( calendrical_calculations:: iso:: fixed_from_iso ( year, month, day) - EPOCH ) * 24
546
+ + hour as i64 )
547
+ * 60
548
+ + minute as i64 )
549
+ * 60
550
+ + second as i64 ;
551
+
552
+ // Pretend date time is UTC to get a candidate
553
+ let idx = self . offset_idx ( seconds_since_local_epoch) ;
554
+
555
+ let candidate = self . offset_at ( idx, seconds_since_local_epoch) ;
556
+ let before_candidate = self . offset_at ( idx - 1 , seconds_since_local_epoch) ;
557
+ let after_candidate = self . offset_at ( idx + 1 , seconds_since_local_epoch) ;
558
+
559
+ let before_candidate_local_until = candidate
560
+ . since
561
+ . saturating_add ( before_candidate. offset . to_seconds ( ) as i64 ) ;
562
+
563
+ let candidate_local_since = candidate
564
+ . since
565
+ . saturating_add ( candidate. offset . to_seconds ( ) as i64 ) ;
566
+ let candidate_local_until = after_candidate
567
+ . since
568
+ . saturating_add ( candidate. offset . to_seconds ( ) as i64 ) ;
569
+
570
+ let after_candidate_local_since = after_candidate
571
+ . since
572
+ . saturating_add ( after_candidate. offset . to_seconds ( ) as i64 ) ;
573
+
574
+ if seconds_since_local_epoch < before_candidate_local_until
575
+ && seconds_since_local_epoch >= candidate_local_since
576
+ && before_candidate != candidate
577
+ {
578
+ return PossibleOffset :: Ambiguous ( before_candidate, candidate) ;
579
+ }
580
+
581
+ if seconds_since_local_epoch < candidate_local_until
582
+ && seconds_since_local_epoch >= after_candidate_local_since
583
+ && candidate != after_candidate
584
+ {
585
+ return PossibleOffset :: Ambiguous ( candidate, after_candidate) ;
586
+ }
587
+
588
+ if seconds_since_local_epoch < before_candidate_local_until {
589
+ return PossibleOffset :: Single ( before_candidate) ;
590
+ }
591
+ if seconds_since_local_epoch < candidate_local_until {
592
+ return PossibleOffset :: Single ( candidate) ;
593
+ }
594
+ if seconds_since_local_epoch >= after_candidate_local_since {
595
+ return PossibleOffset :: Single ( after_candidate) ;
596
+ }
597
+
598
+ PossibleOffset :: None
599
+ }
600
+
601
+ pub fn for_timestamp ( & self , seconds_since_epoch : i64 ) -> Offset {
602
+ self . offset_at ( self . offset_idx ( seconds_since_epoch) , seconds_since_epoch)
603
+ }
521
604
}
522
605
523
606
#[ test]
@@ -546,7 +629,13 @@ fn test() {
546
629
547
630
// TODO
548
631
let max_working_timestamp = if zoneinfo64. final_rule . is_some ( ) {
549
- zoneinfo64. simple . trans . last ( ) . copied ( ) . unwrap_or ( i32:: MAX ) as i64
632
+ zoneinfo64
633
+ . simple
634
+ . trans
635
+ . len ( )
636
+ . checked_sub ( 2 )
637
+ . map ( |i| zoneinfo64. simple . trans [ i] )
638
+ . unwrap_or ( i32:: MAX ) as i64
550
639
} else {
551
640
FUTURE
552
641
} ;
@@ -555,15 +644,26 @@ fn test() {
555
644
let utc_datetime = chrono:: DateTime :: from_timestamp ( seconds_since_epoch, 0 )
556
645
. unwrap ( )
557
646
. naive_utc ( ) ;
558
- let zoneinfo64_date = zoneinfo64. offset_from_utc_datetime ( & utc_datetime) ;
559
- let chrono_date = chrono. offset_from_utc_datetime ( & utc_datetime) ;
560
647
648
+ let zoneinfo64_date = zoneinfo64. from_utc_datetime ( & utc_datetime) ;
649
+ let chrono_date = chrono. from_utc_datetime ( & utc_datetime) ;
561
650
assert_eq ! (
562
- zoneinfo64_date. fix( ) ,
563
- chrono_date. fix( ) ,
651
+ zoneinfo64_date. offset ( ) . fix( ) ,
652
+ chrono_date. offset ( ) . fix( ) ,
564
653
"{seconds_since_epoch}, {iana:?}" ,
565
654
) ;
566
655
656
+ let local_datetime = chrono_date. naive_local ( ) ;
657
+ assert_eq ! (
658
+ zoneinfo64
659
+ . offset_from_local_datetime( & local_datetime)
660
+ . map( |o| o. fix( ) ) ,
661
+ chrono
662
+ . offset_from_local_datetime( & local_datetime)
663
+ . map( |o| o. fix( ) ) ,
664
+ "{seconds_since_epoch}, {zoneinfo64:?} {local_datetime}" ,
665
+ ) ;
666
+
567
667
// Rearguard / vanguard diffs
568
668
if [
569
669
"Africa/Casablanca" ,
@@ -580,8 +680,8 @@ fn test() {
580
680
}
581
681
582
682
assert_eq ! (
583
- zoneinfo64_date. rule_applies( ) ,
584
- !chrono_date. dst_offset( ) . is_zero( ) ,
683
+ zoneinfo64_date. offset ( ) . rule_applies( ) ,
684
+ !chrono_date. offset ( ) . dst_offset( ) . is_zero( ) ,
585
685
"{seconds_since_epoch}, {iana:?}" ,
586
686
) ;
587
687
}
0 commit comments