internal bool TryAdjustAmbiguousTime()

in src/WebJobs.Extensions/Extensions/Timers/Scheduling/CronSchedule.cs [125:206]


        internal bool TryAdjustAmbiguousTime(DateTimeOffset now, DateTimeOffset next, out DateTimeOffset? adjusted)
        {
            adjusted = null;

            TimeZoneInfo timeZone = TimeZoneInfo.Local;

            // Begin evaluating scenarios when Daylight Saving ends and time falls back. This leads to "ambiguous" times
            // as the clock repeats an hour. For example, times from 1:00 - 1:59 AM will occur twice in Pacific Standard Time
            // and are therefore considered "ambiguous" for this time zone.            
            bool isNowAmbiguous = timeZone.IsAmbiguousTime(now);
            bool isNextAmbiguous = timeZone.IsAmbiguousTime(next);

            if (!isNowAmbiguous && !isNextAmbiguous)
            {
                // There are no ambiguous times to adjust.
                return false;
            }

            // We also need to differentiate between "interval" and "point-in-time" schedules when exiting Daylight Saving Time.
            //
            // For "interval" schedules, we want to continue running through this ambiguous hour as usual. Using an "every 30 minute" schedule
            // and Pacific time offset an example, we'd want to:
            //    - Run at 01:00-7, 01:30-7, 01:00-8, 01:30-8, 02:00-8, 02:30-8, etc.
            //
            // For "point-in-time" schedules, we only want to run them once (i.e a 1:30 trigger only runs once on this day). Using an
            // "every day at 01:30" schedule and Pacific time offset as an example in 2018, we'd want to:
            //    - Run on November 3rd at 01:30-7
            //    - Run on November 4th at 01:30-7 (and not run at 1:30-8)
            //    - Run on November 5th at 01:30-8
            if (!IsInterval && isNextAmbiguous && next.Offset != now.Offset)
            {
                // "Fall Back" Scenario 1 -- Point-in-time schedule where the next time is ambiguous and the offsets have changed.
                //   This means that "now" is in DST and "next" is in Standard Time. Since we never want to run an ambiguous
                //   point-in-time schedule on Standard time, move the offset back to the Daylight offset.
                //   Example, all on the "fall back" day:
                //     - schedule:   every day at 01:30
                //     - now:        00:59-7
                //     - next:       01:30-8 (because all ambiguous "next" use Standard time when using NCronTab)
                //     - offsetDiff: (-8)-(-7) = -1
                //     - adjusted:   01:30-7
                //     In this scenario, we see that we're going from DST to Standard, but the Standard time is ambiguous. In this case,
                //     we always want to bring it back to the DST offset in the current local time zone.
                var offsetDiff = next.Offset - now.Offset;
                adjusted = TimeZoneInfo.ConvertTime(next.Add(offsetDiff), timeZone);
                Logger.DstAmbiguousTime(_logger, now, _cronSchedule.ToString(), next, TimeZoneInfo.Local.DisplayName, adjusted.Value);
            }
            else if (!IsInterval && isNextAmbiguous && next.Offset == now.Offset)
            {
                // "Fall Back" Scenario 2 -- Point-in-time where "next" is ambiguous and "now" and "next" are in Standard time. This can happen
                //   when the timer starts and we are already past the "fall back" point. For example, if the trigger starts up at
                //   01:29-8 and we have a "every day at 01:30" schedule, we have to assume that we've already run it at 01:30-7 and
                //   therefore calculate the time after this.
                //   Example:
                //     - schedule:   every day at 01:30
                //     - now:        11/4/2018 01:29-8
                //     - next:       11/4/2018 01:30-8 (because all ambiguous "next" use Standard time when using NCronTab)                
                //     - adjusted:   11/5/2018 01:30-8
                //     In this scenario, we see that we're already in Standard time, but the Standard time is ambiguous. In this case,
                //     we always assume that we've already run during DST and we don't want to run twice in one day.
                var nextDateTime = next.LocalDateTime;

                while (timeZone.IsAmbiguousTime(nextDateTime))
                {
                    nextDateTime = _cronSchedule.GetNextOccurrence(nextDateTime);
                }

                adjusted = new DateTimeOffset(nextDateTime);
                Logger.DstAmbiguousTimeSecondOccurrence(_logger, now, _cronSchedule.ToString(), next, TimeZoneInfo.Local.DisplayName, adjusted.Value);
            }
            else if (IsInterval && (isNowAmbiguous || isNextAmbiguous) && next.Offset != now.Offset)
            {
                // "Fall Back" Scenario 3 -- Interval schedule where either point is ambiguous and we're crossing the offset boundary. For example, 
                //   an "every 30 minute" interval in Pacific time:
                //   - 00:30-7 -> 01:00-8 (because all ambiguous "next" use Standard time) -> adjust back 01:00-7
                //   - 01:30-7 -> 02:00-8 -> adjust back to 01:00-8
                var offsetDiff = next.Offset - now.Offset;
                adjusted = TimeZoneInfo.ConvertTime(next.Add(offsetDiff), timeZone);
                Logger.DstAmbiguousTimeInterval(_logger, now, _cronSchedule.ToString(), next, TimeZoneInfo.Local.DisplayName, adjusted.Value);
            }

            return adjusted is not null;
        }