@@ -364,35 +364,73 @@ fn resolve_posix_tz_string_for_epoch_seconds(
364
364
365
365
let transition =
366
366
compute_tz_for_epoch_seconds ( is_transition_day, transition, seconds, dst_variant) ;
367
- let offset = match transition {
368
- TransitionType :: Dst => {
369
- LocalTimeRecord :: from_daylight_savings_time ( & dst_variant . variant_info )
370
- }
371
- TransitionType :: Std => LocalTimeRecord :: from_standard_time ( & posix_tz_string . std_info ) ,
367
+ let std_offset = LocalTimeRecord :: from_standard_time ( & posix_tz_string . std_info ) . offset ;
368
+ let dst_offset = LocalTimeRecord :: from_daylight_savings_time ( & dst_variant . variant_info ) . offset ;
369
+ let ( old_offset , new_offset ) = match transition {
370
+ TransitionType :: Dst => ( std_offset , dst_offset ) ,
371
+ TransitionType :: Std => ( dst_offset , std_offset ) ,
372
372
} ;
373
373
let transition = match transition {
374
374
TransitionType :: Dst => start,
375
375
TransitionType :: Std => end,
376
376
} ;
377
- let year = utils:: epoch_time_to_epoch_year ( seconds) ;
377
+ let year = utils:: epoch_time_to_epoch_year ( seconds * 1000 ) ;
378
378
let year_epoch = utils:: epoch_days_for_year ( year) * 86400 ;
379
- let leap_days = utils:: mathematical_days_in_year ( year ) - 365 ;
379
+ let leap_day = utils:: mathematical_in_leap_year ( seconds * 1000 ) as u16 ;
380
380
381
381
let days = match transition. day {
382
- TransitionDay :: NoLeap ( day) if day > 59 => i32:: from ( day) - 1 + leap_days,
383
- TransitionDay :: NoLeap ( day) => i32:: from ( day) - 1 ,
384
- TransitionDay :: WithLeap ( day) => i32:: from ( day) ,
385
- TransitionDay :: Mwd ( _month, _week, _day) => {
386
- // TODO: build transition epoch from month, week and day.
387
- return Ok ( TimeZoneOffset {
388
- offset : offset. offset ,
389
- transition_epoch : None ,
390
- } ) ;
382
+ TransitionDay :: NoLeap ( day) if day > 59 => day - 1 + leap_day,
383
+ TransitionDay :: NoLeap ( day) => day - 1 ,
384
+ TransitionDay :: WithLeap ( day) => day,
385
+ TransitionDay :: Mwd ( month, week, day) => {
386
+ let days_to_month = utils:: month_to_day ( ( month - 1 ) as u8 , leap_day) ;
387
+ let days_in_month = u16:: from ( utils:: iso_days_in_month ( year, month as u8 ) - 1 ) ;
388
+
389
+ // Month starts in the day...
390
+ let day_offset =
391
+ ( u16:: from ( utils:: epoch_seconds_to_day_of_week ( i64:: from ( year_epoch) ) )
392
+ + days_to_month)
393
+ . rem_euclid ( 7 ) ;
394
+
395
+ // EXAMPLE:
396
+ //
397
+ // 0 1 2 3 4 5 6
398
+ // sun mon tue wed thu fri sat
399
+ // - - - 0 1 2 3
400
+ // 4 5 6 7 8 9 10
401
+ // 11 12 13 14 15 16 17
402
+ // 18 19 20 21 22 23 24
403
+ // 25 26 27 28 29 30 -
404
+ //
405
+ // The day_offset = 3, since the month starts on a wednesday.
406
+ //
407
+ // We're looking for the second friday of the month. Thus, since the month started before
408
+ // a friday, we need to start counting from week 0:
409
+ //
410
+ // day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset = (2 - 1) * 7 + 5 - 3 = 9
411
+ //
412
+ // This works if the month started on a day before the day we want (day_offset <= day). However, if that's not the
413
+ // case, we need to start counting on week 1. For example, calculate the day of the month for the third monday
414
+ // of the month:
415
+ //
416
+ // day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset = (3 - 0) * 7 + 1 - 3 = 19
417
+ let mut day_of_month = ( week - u16:: from ( day_offset <= day) ) * 7 + day - day_offset;
418
+
419
+ // If we're on week 5, we need to clamp to the last valid day.
420
+ if day_of_month > days_in_month - 1 {
421
+ day_of_month -= 7
422
+ }
423
+
424
+ days_to_month + day_of_month
391
425
}
392
426
} ;
393
- let transition_epoch = i64:: from ( year_epoch) + i64:: from ( days) * 3600 + transition. time . 0 ;
427
+
428
+ // Transition time is on local time, so we need to add the UTC offset to get the correct UTC timestamp
429
+ // for the transition.
430
+ let transition_epoch =
431
+ i64:: from ( year_epoch) + i64:: from ( days) * 86400 + transition. time . 0 - old_offset;
394
432
Ok ( TimeZoneOffset {
395
- offset : offset . offset ,
433
+ offset : new_offset ,
396
434
transition_epoch : Some ( transition_epoch) ,
397
435
} )
398
436
}
@@ -490,7 +528,7 @@ impl Mwd {
490
528
let day_of_month = utils:: epoch_seconds_to_day_of_month ( seconds) ;
491
529
let week_of_month = day_of_month / 7 + 1 ;
492
530
let day_of_week = utils:: epoch_seconds_to_day_of_week ( seconds) ;
493
- Self ( month, week_of_month, day_of_week)
531
+ Self ( month, week_of_month, u16 :: from ( day_of_week) )
494
532
}
495
533
}
496
534
@@ -1033,4 +1071,60 @@ mod tests {
1033
1071
let locals = sydney. v2_estimate_tz_pair ( & Seconds ( seconds) ) . unwrap ( ) ;
1034
1072
assert ! ( matches!( locals, LocalTimeRecordResult :: Single ( _) ) ) ;
1035
1073
}
1074
+
1075
+ #[ test]
1076
+ fn mwd_transition_epoch ( ) {
1077
+ #[ cfg( not( target_os = "windows" ) ) ]
1078
+ let tzif = Tzif :: read_tzif ( "Europe/Berlin" ) . unwrap ( ) ;
1079
+ #[ cfg( target_os = "windows" ) ]
1080
+ let tzif = Tzif :: from_bytes ( jiff_tzdb:: get ( "Europe/Berlin" ) . unwrap ( ) . 1 ) . unwrap ( ) ;
1081
+
1082
+ let start_date = crate :: iso:: IsoDate {
1083
+ year : 2028 ,
1084
+ month : 3 ,
1085
+ day : 30 ,
1086
+ } ;
1087
+ let start_time = crate :: iso:: IsoTime {
1088
+ hour : 6 ,
1089
+ minute : 0 ,
1090
+ second : 0 ,
1091
+ millisecond : 0 ,
1092
+ microsecond : 0 ,
1093
+ nanosecond : 0 ,
1094
+ } ;
1095
+ let start_dt = IsoDateTime :: new ( start_date, start_time) . unwrap ( ) ;
1096
+ let start_dt_secs = ( start_dt. as_nanoseconds ( ) . unwrap ( ) . 0 / 1_000_000_000 ) as i64 ;
1097
+
1098
+ let start_seconds = & Seconds ( start_dt_secs) ;
1099
+
1100
+ assert_eq ! (
1101
+ tzif. get( start_seconds) . unwrap( ) . transition_epoch. unwrap( ) ,
1102
+ // Sun, Mar 26 at 2:00 am
1103
+ 1837645200
1104
+ ) ;
1105
+
1106
+ let end_date = crate :: iso:: IsoDate {
1107
+ year : 2028 ,
1108
+ month : 10 ,
1109
+ day : 29 ,
1110
+ } ;
1111
+ let end_time = crate :: iso:: IsoTime {
1112
+ hour : 6 ,
1113
+ minute : 0 ,
1114
+ second : 0 ,
1115
+ millisecond : 0 ,
1116
+ microsecond : 0 ,
1117
+ nanosecond : 0 ,
1118
+ } ;
1119
+ let end_dt = IsoDateTime :: new ( end_date, end_time) . unwrap ( ) ;
1120
+ let end_dt_secs = ( end_dt. as_nanoseconds ( ) . unwrap ( ) . 0 / 1_000_000_000 ) as i64 ;
1121
+
1122
+ let end_seconds = & Seconds ( end_dt_secs) ;
1123
+
1124
+ assert_eq ! (
1125
+ tzif. get( end_seconds) . unwrap( ) . transition_epoch. unwrap( ) ,
1126
+ // Sun, Oct 29 at 3:00 am
1127
+ 1856394000
1128
+ ) ;
1129
+ }
1036
1130
}
0 commit comments