@@ -88,7 +88,10 @@ const NS_MIN = JSBI.multiply(JSBI.BigInt(-86400), JSBI.BigInt(1e17));
8888const NS_MAX = JSBI . multiply ( JSBI . BigInt ( 86400 ) , JSBI . BigInt ( 1e17 ) ) ;
8989const YEAR_MIN = - 271821 ;
9090const YEAR_MAX = 275760 ;
91- const BEFORE_FIRST_DST = JSBI . multiply ( JSBI . BigInt ( - 388152 ) , JSBI . BigInt ( 1e13 ) ) ; // 1847-01-01T00:00:00Z
91+ const BEFORE_FIRST_OFFSET_TRANSITION = JSBI . multiply ( JSBI . BigInt ( - 388152 ) , JSBI . BigInt ( 1e13 ) ) ; // 1847-01-01T00:00:00Z
92+ const ABOUT_TEN_YEARS_NANOS = JSBI . multiply ( DAY_NANOS , JSBI . BigInt ( 366 * 10 ) ) ;
93+ const ABOUT_ONE_YEAR_NANOS = JSBI . multiply ( DAY_NANOS , JSBI . BigInt ( 366 * 1 ) ) ;
94+ const TWO_WEEKS_NANOS = JSBI . multiply ( DAY_NANOS , JSBI . BigInt ( 2 * 7 ) ) ;
9295
9396function IsInteger ( value : unknown ) : value is number {
9497 if ( typeof value !== 'number' || ! NumberIsFinite ( value ) ) return false ;
@@ -2835,14 +2838,45 @@ export function GetIANATimeZoneDateTimeParts(epochNanoseconds: JSBI, id: string)
28352838 return BalanceISODateTime ( year , month , day , hour , minute , second , millisecond , microsecond , nanosecond ) ;
28362839}
28372840
2838- export function GetIANATimeZoneNextTransition ( epochNanoseconds : JSBI , id : string ) {
2839- const uppercap = JSBI . add ( SystemUTCEpochNanoSeconds ( ) , JSBI . multiply ( DAY_NANOS , JSBI . BigInt ( 366 ) ) ) ;
2840- let leftNanos = epochNanoseconds ;
2841+ function maxJSBI ( one : JSBI , two : JSBI ) {
2842+ return JSBI . lessThan ( one , two ) ? two : one ;
2843+ }
2844+
2845+ /**
2846+ * Our best guess at how far in advance new rules will be put into the TZDB for
2847+ * future offset transitions. We'll pick 10 years but can always revise it if
2848+ * we find that countries are being unusually proactive in their announcing
2849+ * of offset changes.
2850+ */
2851+ function afterLatestPossibleTzdbRuleChange ( ) {
2852+ return JSBI . add ( SystemUTCEpochNanoSeconds ( ) , ABOUT_TEN_YEARS_NANOS ) ;
2853+ }
2854+
2855+ export function GetIANATimeZoneNextTransition ( epochNanoseconds : JSBI , id : string ) : JSBI | null {
2856+ // Decide how far in the future after `epochNanoseconds` we'll look for an
2857+ // offset change. There are two cases:
2858+ // 1. If it's a past date (or a date in the near future) then it's possible
2859+ // that the time zone may have newly added DST in the next few years. So
2860+ // we'll have to look from the provided time until a few years after the
2861+ // current system time. (Changes to DST policy are usually announced a few
2862+ // years in the future.) Note that the first DST anywhere started in 1847,
2863+ // so we'll start checks in 1847 instead of wasting cycles on years where
2864+ // there will never be transitions.
2865+ // 2. If it's a future date beyond the next few years, then we'll just assume
2866+ // that the latest DST policy in TZDB will still be in effect. In this
2867+ // case, we only need to look one year in the future to see if there are
2868+ // any DST transitions. We actually only need to look 9-10 months because
2869+ // DST has two transitions per year, but we'll use a year just to be safe.
2870+ const oneYearLater = JSBI . add ( epochNanoseconds , ABOUT_ONE_YEAR_NANOS ) ;
2871+ const uppercap = maxJSBI ( afterLatestPossibleTzdbRuleChange ( ) , oneYearLater ) ;
2872+ // The first transition (in any timezone) recorded in the TZDB was in 1847, so
2873+ // start there if an earlier date is supplied.
2874+ let leftNanos = maxJSBI ( BEFORE_FIRST_OFFSET_TRANSITION , epochNanoseconds ) ;
28412875 const leftOffsetNs = GetIANATimeZoneOffsetNanoseconds ( leftNanos , id ) ;
28422876 let rightNanos = leftNanos ;
28432877 let rightOffsetNs = leftOffsetNs ;
28442878 while ( leftOffsetNs === rightOffsetNs && JSBI . lessThan ( JSBI . BigInt ( leftNanos ) , uppercap ) ) {
2845- rightNanos = JSBI . add ( leftNanos , JSBI . multiply ( DAY_NANOS , JSBI . BigInt ( 2 * 7 ) ) ) ;
2879+ rightNanos = JSBI . add ( leftNanos , TWO_WEEKS_NANOS ) ;
28462880 rightOffsetNs = GetIANATimeZoneOffsetNanoseconds ( rightNanos , id ) ;
28472881 if ( leftOffsetNs === rightOffsetNs ) {
28482882 leftNanos = rightNanos ;
@@ -2859,20 +2893,50 @@ export function GetIANATimeZoneNextTransition(epochNanoseconds: JSBI, id: string
28592893 return result ;
28602894}
28612895
2862- export function GetIANATimeZonePreviousTransition ( epochNanoseconds : JSBI , id : string ) {
2863- const lowercap = BEFORE_FIRST_DST ; // 1847-01-01T00:00:00Z
2896+ export function GetIANATimeZonePreviousTransition ( epochNanoseconds : JSBI , id : string ) : JSBI | null {
2897+ // If a time zone uses DST (at the time of `epochNanoseconds`), then we only
2898+ // have to look back one year to find a transition. But if it doesn't use DST,
2899+ // then we need to look all the way back to 1847 (the earliest rule in the
2900+ // TZDB) to see if it had other offset transitions in the past. Looping back
2901+ // from a far-future date to 1847 is very slow (minutes of 100% CPU!), and is
2902+ // also unnecessary because DST rules aren't put into the TZDB more than a few
2903+ // years in the future because the political changes in time zones happen with
2904+ // only a few years' warning. Therefore, if a far-future date is provided,
2905+ // then we'll run the check in two parts:
2906+ // 1. First, we'll look back for up to one year to see if the latest TZDB
2907+ // rules have DST.
2908+ // 2. If not, then we'll "fast-reverse" back to a few years later than the
2909+ // current system time, and then look back to 1847. This reduces the
2910+ // worst-case loop from 273K years to 175 years, for a ~1500x improvement
2911+ // in worst-case perf.
2912+ const afterLatestRule = afterLatestPossibleTzdbRuleChange ( ) ;
2913+ const isFarFuture = JSBI . greaterThan ( epochNanoseconds , afterLatestRule ) ;
2914+ const lowercap = isFarFuture ? JSBI . subtract ( epochNanoseconds , ABOUT_ONE_YEAR_NANOS ) : BEFORE_FIRST_OFFSET_TRANSITION ;
28642915 let rightNanos = JSBI . subtract ( epochNanoseconds , ONE ) ;
28652916 const rightOffsetNs = GetIANATimeZoneOffsetNanoseconds ( rightNanos , id ) ;
28662917 let leftNanos = rightNanos ;
28672918 let leftOffsetNs = rightOffsetNs ;
28682919 while ( rightOffsetNs === leftOffsetNs && JSBI . greaterThan ( rightNanos , lowercap ) ) {
2869- leftNanos = JSBI . subtract ( rightNanos , JSBI . multiply ( DAY_NANOS , JSBI . BigInt ( 2 * 7 ) ) ) ;
2920+ leftNanos = JSBI . subtract ( rightNanos , TWO_WEEKS_NANOS ) ;
28702921 leftOffsetNs = GetIANATimeZoneOffsetNanoseconds ( leftNanos , id ) ;
28712922 if ( rightOffsetNs === leftOffsetNs ) {
28722923 rightNanos = leftNanos ;
28732924 }
28742925 }
2875- if ( rightOffsetNs === leftOffsetNs ) return null ;
2926+ if ( rightOffsetNs === leftOffsetNs ) {
2927+ if ( isFarFuture ) {
2928+ // There was no DST after looking back one year, which means that the most
2929+ // recent TZDB rules don't have any recurring transitions. To check for
2930+ // transitions in older rules, back up to a few years after the current
2931+ // date and then look all the way back to 1847. Note that we move back one
2932+ // day from the latest possible rule so that when the recursion runs it
2933+ // won't consider the new time to be "far future" because the system clock
2934+ // has advanced in the meantime.
2935+ const newTimeToCheck = JSBI . subtract ( afterLatestRule , DAY_NANOS ) ;
2936+ return GetIANATimeZonePreviousTransition ( newTimeToCheck , id ) ;
2937+ }
2938+ return null ;
2939+ }
28762940 const result = bisect (
28772941 ( epochNs : JSBI ) => GetIANATimeZoneOffsetNanoseconds ( epochNs , id ) ,
28782942 leftNanos ,
0 commit comments