/// <summary> /// Initializes a new instance of the <see cref="ZonedDateTime"/> struct in the specified time zone /// from a given local time and offset. The offset is validated to be correct as part of initialization. /// In most cases a local time can only map to a single instant anyway, but the offset is included here for cases /// where the local time is ambiguous, usually due to daylight saving transitions. /// </summary> /// <param name="localDateTime">The local date and time.</param> /// <param name="zone">The time zone.</param> /// <param name="offset">The offset between UTC and local time at the desired instant.</param> /// <exception cref="ArgumentException"><paramref name="offset"/> is not a valid offset at the given /// local date and time.</exception> public ZonedDateTime(LocalDateTime localDateTime, DateTimeZone zone, Offset offset) { this.zone = Preconditions.CheckNotNull(zone, nameof(zone)); Instant candidateInstant = localDateTime.ToLocalInstant().Minus(offset); Offset correctOffset = zone.GetUtcOffset(candidateInstant); // Not using Preconditions, to avoid building the string unnecessarily. if (correctOffset != offset) { throw new ArgumentException($"Offset {offset} is invalid for local date and time {localDateTime} in time zone {zone.Id}", nameof(offset)); } offsetDateTime = new OffsetDateTime(localDateTime, offset); }
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))); } }
/// <summary> /// Returns complete information about how the given <see cref="LocalDateTime" /> is mapped in this time zone. /// </summary> /// <remarks> /// <para> /// Mapping a local date/time to a time zone can give an unambiguous, ambiguous or impossible result, depending on /// time zone transitions. Use the return value of this method to handle these cases in an appropriate way for /// your use case. /// </para> /// <para> /// As an alternative, consider <see cref="ResolveLocal(LocalDateTime, ZoneLocalMappingResolver)"/>, which uses a caller-provided strategy to /// convert the <see cref="ZoneLocalMapping"/> returned here to a <see cref="ZonedDateTime"/>. /// </para> /// </remarks> /// <param name="localDateTime">The local date and time to map in this time zone.</param> /// <returns>A mapping of the given local date and time to zero, one or two zoned date/time values.</returns> public virtual ZoneLocalMapping MapLocal(LocalDateTime localDateTime) { 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)) { ZoneInterval?earlier = GetEarlierMatchingInterval(interval, localInstant); if (earlier != null) { return(new ZoneLocalMapping(this, localDateTime, earlier, interval, 2)); } ZoneInterval?later = GetLaterMatchingInterval(interval, localInstant); if (later != null) { return(new ZoneLocalMapping(this, localDateTime, interval, later, 2)); } return(new ZoneLocalMapping(this, localDateTime, interval, interval, 1)); } 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 ZoneLocalMapping(this, localDateTime, earlier, earlier, 1)); } ZoneInterval?later = GetLaterMatchingInterval(interval, localInstant); if (later != null) { return(new ZoneLocalMapping(this, localDateTime, later, later, 1)); } return(new ZoneLocalMapping(this, localDateTime, GetIntervalBeforeGap(localInstant), GetIntervalAfterGap(localInstant), 0)); } }
/// <summary> /// Returns the period between a start and an end date/time, using only the given units. /// </summary> /// <remarks> /// If <paramref name="end"/> is before <paramref name="start" />, each property in the returned period /// will be negative. If the given set of units cannot exactly reach the end point (e.g. finding /// the difference between 1am and 3:15am in hours) the result will be such that adding it to <paramref name="start"/> /// will give a value between <paramref name="start"/> and <paramref name="end"/>. In other words, /// any rounding is "towards start"; this is true whether the resulting period is negative or positive. /// </remarks> /// <param name="start">Start date/time</param> /// <param name="end">End date/time</param> /// <param name="units">Units to use for calculations</param> /// <exception cref="ArgumentException"><paramref name="units"/> is empty or contained unknown values.</exception> /// <exception cref="ArgumentException"><paramref name="start"/> and <paramref name="end"/> use different calendars.</exception> /// <returns>The period between the given date/times, using the given units.</returns> public static Period Between(LocalDateTime start, LocalDateTime end, PeriodUnits units) { Preconditions.CheckArgument(units != 0, nameof(units), "Units must not be empty"); Preconditions.CheckArgument((units & ~PeriodUnits.AllUnits) == 0, nameof(units), "Units contains an unknown value: {0}", units); CalendarSystem calendar = start.Calendar; Preconditions.CheckArgument(calendar.Equals(end.Calendar), nameof(end), "start and end must use the same calendar system"); if (start == end) { return Zero; } // Adjust for situations like "days between 5th January 10am and 7th Janary 5am" which should be one // day, because if we actually reach 7th January with date fields, we've overshot. // The date adjustment will always be valid, because it's just moving it towards start. // We need this for all date-based period fields. We could potentially optimize by not doing this // in cases where we've only got time fields... LocalDate endDate = end.Date; if (start < end) { if (start.TimeOfDay > end.TimeOfDay) { endDate = endDate.PlusDays(-1); } } else if (start > end && start.TimeOfDay < end.TimeOfDay) { endDate = endDate.PlusDays(1); } // Optimization for single field switch (units) { case PeriodUnits.Years: return FromYears(DatePeriodFields.YearsField.UnitsBetween(start.Date, endDate)); case PeriodUnits.Months: return FromMonths(DatePeriodFields.MonthsField.UnitsBetween(start.Date, endDate)); case PeriodUnits.Weeks: return FromWeeks(DatePeriodFields.WeeksField.UnitsBetween(start.Date, endDate)); case PeriodUnits.Days: return FromDays(DaysBetween(start.Date, endDate)); case PeriodUnits.Hours: return FromHours(TimePeriodField.Hours.UnitsBetween(start, end)); case PeriodUnits.Minutes: return FromMinutes(TimePeriodField.Minutes.UnitsBetween(start, end)); case PeriodUnits.Seconds: return FromSeconds(TimePeriodField.Seconds.UnitsBetween(start, end)); case PeriodUnits.Milliseconds: return FromMilliseconds(TimePeriodField.Milliseconds.UnitsBetween(start, end)); case PeriodUnits.Ticks: return FromTicks(TimePeriodField.Ticks.UnitsBetween(start, end)); case PeriodUnits.Nanoseconds: return FromNanoseconds(TimePeriodField.Nanoseconds.UnitsBetween(start, end)); } // Multiple fields LocalDateTime remaining = start; int years = 0, months = 0, weeks = 0, days = 0; if ((units & PeriodUnits.AllDateUnits) != 0) { LocalDate remainingDate = DateComponentsBetween( start.Date, endDate, units, out years, out months, out weeks, out days); remaining = new LocalDateTime(remainingDate, start.TimeOfDay); } if ((units & PeriodUnits.AllTimeUnits) == 0) { return new Period(years, months, weeks, days); } // The remainder of the computation is with fixed-length units, so we can do it all with // Duration instead of Local* values. We don't know for sure that this is small though - we *could* // be trying to find the difference between 9998 BC and 9999 CE in nanoseconds... // Where we can optimize, do everything with long arithmetic (as we do for Between(LocalTime, LocalTime)). // Otherwise (rare case), use duration arithmetic. long hours, minutes, seconds, milliseconds, ticks, nanoseconds; var duration = end.ToLocalInstant().TimeSinceLocalEpoch - remaining.ToLocalInstant().TimeSinceLocalEpoch; if (duration.IsInt64Representable) { TimeComponentsBetween(duration.ToInt64Nanoseconds(), units, out hours, out minutes, out seconds, out milliseconds, out ticks, out nanoseconds); } else { hours = UnitsBetween(PeriodUnits.Hours, TimePeriodField.Hours); minutes = UnitsBetween(PeriodUnits.Minutes, TimePeriodField.Minutes); seconds = UnitsBetween(PeriodUnits.Seconds, TimePeriodField.Seconds); milliseconds = UnitsBetween(PeriodUnits.Milliseconds, TimePeriodField.Milliseconds); ticks = UnitsBetween(PeriodUnits.Ticks, TimePeriodField.Ticks); nanoseconds = UnitsBetween(PeriodUnits.Ticks, TimePeriodField.Nanoseconds); } return new Period(years, months, weeks, days, hours, minutes, seconds, milliseconds, ticks, nanoseconds); long UnitsBetween(PeriodUnits mask, TimePeriodField timeField) { if ((mask & units) == 0) { return 0; } long value = timeField.GetUnitsInDuration(duration); duration -= timeField.ToDuration(value); return value; } }