/// <summary> /// Returns the earliest valid <see cref="ZonedDateTime"/> with the given local date. /// </summary> /// <remarks> /// If midnight exists unambiguously on the given date, it is returned. /// If the given date has an ambiguous start time (e.g. the clocks go back from 1am to midnight) /// then the earlier ZonedDateTime is returned. If the given date has no midnight (e.g. the clocks /// go forward from midnight to 1am) then the earliest valid value is returned; this will be the instant /// of the transition. /// </remarks> /// <param name="date">The local date to map in this time zone.</param> /// <exception cref="SkippedTimeException">The entire day was skipped due to a very large time zone transition. /// (This is extremely rare.)</exception> /// <returns>The <see cref="ZonedDateTime"/> representing the earliest time in the given date, in this time zone.</returns> public ZonedDateTime AtStartOfDay(LocalDate date) { LocalDateTime midnight = date.AtMidnight(); var mapping = MapLocal(midnight); switch (mapping.Count) { // Midnight doesn't exist. Maybe we just skip to 1am (or whatever), or maybe the whole day is missed. case 0: var interval = mapping.LateInterval; // Safe to use Start, as it can't extend to the start of time. var offsetDateTime = new OffsetDateTime(interval.Start, interval.WallOffset, date.Calendar); // It's possible that the entire day is skipped. For example, Samoa skipped December 30th 2011. // We know the two values are in the same calendar here, so we just need to check the YearMonthDay. if (offsetDateTime.YearMonthDay != date.YearMonthDay) { throw new SkippedTimeException(midnight, this); } return(new ZonedDateTime(offsetDateTime, this)); // Unambiguous or occurs twice, we can just use the offset from the earlier interval. case 1: case 2: return(new ZonedDateTime(midnight.WithOffset(mapping.EarlyInterval.WallOffset), this)); default: throw new InvalidOperationException("This won't happen."); } }
internal ZonedDateTime ResolveLocal(LocalDateTime localDateTime, [Trusted][NotNull] AmbiguousTimeResolver ambiguousResolver, [Trusted][NotNull] SkippedTimeResolver skippedResolver) { Preconditions.DebugCheckNotNull(ambiguousResolver, nameof(ambiguousResolver)); Preconditions.DebugCheckNotNull(skippedResolver, nameof(skippedResolver)); LocalInstant localInstant = localDateTime.ToLocalInstant(); Instant firstGuess = localInstant.MinusZeroOffset(); ZoneInterval interval = GetZoneInterval(firstGuess); // Most of the time we'll go into here... the local instant and the instant // are close enough that we've found the right instant. if (interval.Contains(localInstant)) { ZonedDateTime guessZoned = new ZonedDateTime(localDateTime.WithOffset(interval.WallOffset), this); ZoneInterval earlier = GetEarlierMatchingInterval(interval, localInstant); if (earlier != null) { ZonedDateTime earlierZoned = new ZonedDateTime(localDateTime.WithOffset(earlier.WallOffset), this); return(ambiguousResolver(earlierZoned, guessZoned)); } ZoneInterval later = GetLaterMatchingInterval(interval, localInstant); if (later != null) { ZonedDateTime laterZoned = new ZonedDateTime(localDateTime.WithOffset(later.WallOffset), this); return(ambiguousResolver(guessZoned, laterZoned)); } return(guessZoned); } else { // Our first guess was wrong. Either we need to change interval by one (either direction) // or we're in a gap. ZoneInterval earlier = GetEarlierMatchingInterval(interval, localInstant); if (earlier != null) { return(new ZonedDateTime(localDateTime.WithOffset(earlier.WallOffset), this)); } ZoneInterval later = GetLaterMatchingInterval(interval, localInstant); if (later != null) { return(new ZonedDateTime(localDateTime.WithOffset(later.WallOffset), this)); } return(skippedResolver(localDateTime, this, GetIntervalBeforeGap(localInstant), GetIntervalAfterGap(localInstant))); } }