// Copyright 2009 The Noda Time Authors. All rights reserved. // Use of this source code is governed by the Apache License 2.0, // as found in the LICENSE.txt file. using NodaTime.Annotations; using NodaTime.TimeZones; using NodaTime.Utility; using System; using System.Collections.Generic; using static System.FormattableString; namespace NodaTime { /// /// Represents a time zone - a mapping between UTC and local time. A time zone maps UTC instants to local times /// - or, equivalently, to the offset from UTC at any particular instant. /// /// /// /// The mapping is unambiguous in the "UTC to local" direction, but /// the reverse is not true: when the offset changes, usually due to a Daylight Saving transition, /// the change either creates a gap (a period of local time which never occurs in the time zone) /// or an ambiguity (a period of local time which occurs twice in the time zone). Mapping back from /// local time to an instant requires consideration of how these problematic times will be handled. /// /// /// Noda Time provides various options when mapping local time to a specific instant: /// /// /// will throw an exception if the mapping from local time is either ambiguous /// or impossible, i.e. if there is anything other than one instant which maps to the given local time. /// /// /// will never throw an exception due to ambiguous or skipped times, /// resolving to the earlier option of ambiguous matches, or to a value that's forward-shifted by the duration /// of the gap for skipped times. /// /// /// will apply a to the result of /// a mapping. /// /// /// will return a /// with complete information about whether the given local time occurs zero times, once or twice. This is the most /// fine-grained approach, which is the fiddliest to use but puts the caller in the most control. /// /// /// /// /// Noda Time has two built-in sources of time zone data available: a copy of the /// tz database (also known as the IANA Time Zone database, or zoneinfo /// or Olson database), and the ability to convert .NET's own format into a "native" Noda /// Time zone. Which of these is most appropriate for you to use will very much depend on your exact needs. The /// zoneinfo database is widely used outside Windows, and has more historical data than the Windows-provided /// information, but if you need to interoperate with other Windows systems by specifying time zone IDs, you may /// wish to stick to the Windows time zones. /// /// /// To obtain a for a given timezone ID, use one of the methods on /// (and see for access to the built-in /// providers). The UTC timezone is also available via the property on this class. /// /// /// To obtain a representing the system default time zone, you can either call /// on a provider to obtain the that /// the provider considers matches the system default time zone, or you can construct a /// via , which returns a /// that wraps the system local . The latter will always /// succeed, but has access only to that information available via the .NET time zone; the former may contain more /// complete data, but may (in uncommon cases) fail to find a matching . /// Note that BclDateTimeZone may not be available in all versions of Noda Time 1.x and 2.x; see the class /// documentation for more details. /// /// /// Note that Noda Time does not require that instances be singletons. /// Comparing two time zones for equality is not straightforward: if you care about whether two /// zones act the same way within a particular portion of time, use . /// Additional guarantees are provided by and . /// /// /// /// All time zone implementations within Noda Time are immutable and thread-safe. /// See the thread safety section of the user guide for more information. /// It is expected that third party implementations will be immutable and thread-safe as well: /// code within Noda Time assumes that it can hand out time zones to any thread without any concerns. If you /// implement a non-thread-safe time zone, you will need to use it extremely carefully. We'd recommend that you /// avoid this if possible. /// [Immutable] public abstract class DateTimeZone : IZoneIntervalMap { /// /// The ID of the UTC (Coordinated Universal Time) time zone. This ID is always valid, whatever provider is /// used. If the provider has its own mapping for UTC, that will be returned by , but otherwise /// the value of the property will be returned. /// internal const string UtcId = "UTC"; /// /// Gets the UTC (Coordinated Universal Time) time zone. /// /// /// This is a single instance which is not provider-specific; it is guaranteed to have the ID "UTC", and to /// compare equal to an instance returned by calling with an offset of zero, but it may /// or may not compare equal to an instance returned by e.g. DateTimeZoneProviders.Tzdb["UTC"]. /// /// A UTC . public static DateTimeZone Utc { get; } = new FixedDateTimeZone(Offset.Zero); private const int FixedZoneCacheGranularitySeconds = NodaConstants.SecondsPerMinute * 30; private const int FixedZoneCacheMinimumSeconds = -FixedZoneCacheGranularitySeconds * 12 * 2; // From UTC-12 private const int FixedZoneCacheSize = (12 + 15) * 2 + 1; // To UTC+15 inclusive private static readonly DateTimeZone[] FixedZoneCache = BuildFixedZoneCache(); /// /// Returns a fixed time zone with the given offset. /// /// /// /// The returned time zone will have an ID of "UTC" if the offset is zero, or "UTC+/-Offset" /// otherwise. In the former case, the returned instance will be equal to . /// /// /// Note also that this method is not required to return the same instance for /// successive requests for the same offset; however, all instances returned for a given offset will compare /// as equal. /// /// /// The offset for the returned time zone /// A fixed time zone with the given offset. public static DateTimeZone ForOffset(Offset offset) { int seconds = offset.Seconds; if (seconds % FixedZoneCacheGranularitySeconds != 0) { return new FixedDateTimeZone(offset); } int index = (seconds - FixedZoneCacheMinimumSeconds) / FixedZoneCacheGranularitySeconds; if (index < 0 || index >= FixedZoneCacheSize) { return new FixedDateTimeZone(offset); } return FixedZoneCache[index]; } /// /// Initializes a new instance of the class. /// /// The unique id of this time zone. /// Set to true if this time zone has no transitions. /// Minimum offset applied within this zone /// Maximum offset applied within this zone protected DateTimeZone(string id, bool isFixed, Offset minOffset, Offset maxOffset) { this.Id = Preconditions.CheckNotNull(id, nameof(id)); this.IsFixed = isFixed; this.MinOffset = minOffset; this.MaxOffset = maxOffset; } /// /// Get the provider's ID for the time zone. /// /// /// /// This identifies the time zone within the current time zone provider; a different provider may /// provide a different time zone with the same ID, or may not provide a time zone with that ID at all. /// /// /// The provider's ID for the time zone. public string Id { get; } /// /// Indicates whether the time zone is fixed, i.e. contains no transitions. /// /// /// This is used as an optimization. If the time zone has no transitions but returns false /// for this then the behavior will be correct but the system will have to do extra work. However /// if the time zone has transitions and this returns true then the transitions will never /// be examined. /// /// true if the time zone is fixed; false otherwise. internal bool IsFixed { get; } /// /// Gets the least (most negative) offset within this time zone, over all time. /// /// The least (most negative) offset within this time zone, over all time. public Offset MinOffset { get; } /// /// Gets the greatest (most positive) offset within this time zone, over all time. /// /// The greatest (most positive) offset within this time zone, over all time. public Offset MaxOffset { get; } #region Core abstract/virtual methods /// /// Returns the offset from UTC, where a positive duration indicates that local time is /// later than UTC. In other words, local time = UTC + offset. /// /// /// This is mostly a convenience method for calling GetZoneInterval(instant).WallOffset, /// although it can also be overridden for more efficiency. /// /// The instant for which to calculate the offset. /// /// The offset from UTC at the specified instant. /// public virtual Offset GetUtcOffset(Instant instant) => GetZoneInterval(instant).WallOffset; // Note for CA2119: // IZoneIntervalMap is primarily used while *building* time zones, but is also implemented by DateTimeZone for simplicity // of caching. (BclDateTimeZone always returns a cached date time zone having created an IZoneIntervalMap.) // While a regular user can create their own DateTimeZone implementation, that would never end up being cached // because CachedDateTimeZone.ForZone is internal anyway. // In other words: we don't believe this can be exploited. /// /// Gets the zone interval for the given instant; the range of time around the instant in which the same Offset /// applies (with the same split between standard time and daylight saving time, and with the same offset). /// /// /// This will always return a valid zone interval, as time zones cover the whole of time. /// /// The to query. /// The defined . /// #pragma warning disable CA2119 // Seal methods that satisfy private interfaces - see note above public abstract ZoneInterval GetZoneInterval(Instant instant); #pragma warning restore CA2119 /// /// Returns complete information about how the given is mapped in this time zone. /// /// /// /// 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. /// /// /// As an alternative, consider , which uses a caller-provided strategy to /// convert the returned here to a . /// /// /// The local date and time to map in this time zone. /// A mapping of the given local date and time to zero, one or two zoned date/time values. 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); } } #endregion #region Conversion between local dates/times and ZonedDateTime /// /// Returns the earliest valid with the given local date. /// /// /// 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. /// /// The local date to map in this time zone. /// The entire day was skipped due to a very large time zone transition. /// (This is extremely rare.) /// The representing the earliest time in the given date, in this time zone. 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."); } } /// /// Maps the given to the corresponding , following /// the given to handle ambiguity and skipped times. /// /// /// /// This is a convenience method for calling and passing the result to the resolver. /// Common options for resolvers are provided in the static class. /// /// /// See and for alternative ways to map a local time to a /// specific instant. /// /// /// The local date and time to map in this time zone. /// The resolver to apply to the mapping. /// The result of resolving the mapping. public ZonedDateTime ResolveLocal(LocalDateTime localDateTime, ZoneLocalMappingResolver resolver) { Preconditions.CheckNotNull(resolver, nameof(resolver)); return resolver(MapLocal(localDateTime)); } /// /// Maps the given to the corresponding , if and only if /// that mapping is unambiguous in this time zone. Otherwise, or /// is thrown, depending on whether the mapping is ambiguous or the local /// date/time is skipped entirely. /// /// /// See and for alternative ways to map a local time to a /// specific instant. /// /// The local date and time to map into this time zone. /// The given local date/time is skipped in this time zone. /// The given local date/time is ambiguous in this time zone. /// The unambiguous matching if it exists. public ZonedDateTime AtStrictly(LocalDateTime localDateTime) => ResolveLocal(localDateTime, Resolvers.StrictResolver); /// /// Maps the given to the corresponding in a lenient /// manner: ambiguous values map to the earlier of the alternatives, and "skipped" values are shifted forward /// by the duration of the "gap". /// /// /// See and for alternative ways to map a local time to a /// specific instant. /// Note: The behavior of this method was changed in version 2.0 to fit the most commonly seen real-world /// usage pattern. Previous versions returned the later instance of ambiguous values, and returned the start of /// the zone interval after the gap for skipped value. The previous functionality can still be used if desired, /// by using , passing in a resolver /// created from and . /// /// The local date/time to map. /// The unambiguous mapping if there is one, the earlier result if the mapping is ambiguous, /// or the forward-shifted value if the given local date/time is skipped. public ZonedDateTime AtLeniently(LocalDateTime localDateTime) => ResolveLocal(localDateTime, Resolvers.LenientResolver); #endregion /// /// Returns the interval before this one, if it contains the given local instant, or null otherwise. /// private ZoneInterval? GetEarlierMatchingInterval(ZoneInterval interval, LocalInstant localInstant) { // Micro-optimization to avoid fetching interval.Start multiple times. Seems // to give a performance improvement on x86 at least... // If the zone interval extends to the start of time, the next check will definitely evaluate to false. Instant intervalStart = interval.RawStart; // This allows for a maxOffset of up to +1 day, and the "truncate towards beginning of time" // nature of the Days property. if (localInstant.DaysSinceEpoch <= intervalStart.DaysSinceEpoch + 1) { // We *could* do a more accurate check here based on the actual maxOffset, but it's probably // not worth it. ZoneInterval candidate = GetZoneInterval(intervalStart - Duration.Epsilon); if (candidate.Contains(localInstant)) { return candidate; } } return null; } /// /// Returns the next interval after this one, if it contains the given local instant, or null otherwise. /// private ZoneInterval? GetLaterMatchingInterval(ZoneInterval interval, LocalInstant localInstant) { // Micro-optimization to avoid fetching interval.End multiple times. Seems // to give a performance improvement on x86 at least... // If the zone interval extends to the end of time, the next check will // definitely evaluate to false. Instant intervalEnd = interval.RawEnd; // Crude but cheap first check to see whether there *might* be a later interval. // This allows for a minOffset of up to -1 day, and the "truncate towards beginning of time" // nature of the Days property. if (localInstant.DaysSinceEpoch >= intervalEnd.DaysSinceEpoch - 1) { // We *could* do a more accurate check here based on the actual maxOffset, but it's probably // not worth it. ZoneInterval candidate = GetZoneInterval(intervalEnd); if (candidate.Contains(localInstant)) { return candidate; } } return null; } private ZoneInterval GetIntervalBeforeGap(LocalInstant localInstant) { Instant guess = localInstant.MinusZeroOffset(); ZoneInterval guessInterval = GetZoneInterval(guess); // If the local interval occurs before the zone interval we're looking at starts, // we need to find the earlier one; otherwise this interval must come after the gap, and // it's therefore the one we want. if (localInstant.Minus(guessInterval.WallOffset) < guessInterval.RawStart) { return GetZoneInterval(guessInterval.Start - Duration.Epsilon); } else { return guessInterval; } } private ZoneInterval GetIntervalAfterGap(LocalInstant localInstant) { Instant guess = localInstant.MinusZeroOffset(); ZoneInterval guessInterval = GetZoneInterval(guess); // If the local interval occurs before the zone interval we're looking at starts, // it's the one we're looking for. Otherwise, we need to find the next interval. if (localInstant.Minus(guessInterval.WallOffset) < guessInterval.RawStart) { return guessInterval; } else { // Will definitely be valid - there can't be a gap after an infinite interval. return GetZoneInterval(guessInterval.End); } } #region Object overrides /// /// Returns the ID of this time zone. /// /// /// The ID of this time zone. /// /// 2 public override string ToString() => Id; #endregion /// /// Creates a fixed time zone for offsets -12 to +15 at every half hour, /// fixing the 0 offset as DateTimeZone.Utc. /// private static DateTimeZone[] BuildFixedZoneCache() { DateTimeZone[] ret = new DateTimeZone[FixedZoneCacheSize]; for (int i = 0; i < FixedZoneCacheSize; i++) { int offsetSeconds = i * FixedZoneCacheGranularitySeconds + FixedZoneCacheMinimumSeconds; ret[i] = new FixedDateTimeZone(Offset.FromSeconds(offsetSeconds)); } ret[-FixedZoneCacheMinimumSeconds / FixedZoneCacheGranularitySeconds] = Utc; return ret; } /// /// Returns all the zone intervals which occur for any instant in the interval [, ). /// /// /// This method is simply a convenience method for calling without /// explicitly constructing the interval beforehand. /// /// /// Inclusive start point of the interval for which to retrieve zone intervals. /// Exclusive end point of the interval for which to retrieve zone intervals. /// is earlier than . /// A sequence of zone intervals covering the given interval. /// public IEnumerable GetZoneIntervals(Instant start, Instant end) => // The constructor performs all the validation we need. GetZoneIntervals(new Interval(start, end)); /// /// Returns all the zone intervals which occur for any instant in the given interval. /// /// /// The zone intervals are returned in chronological order. /// This method is equivalent to calling for every /// instant in the interval and then collapsing to a set of distinct zone intervals. /// The first and last zone intervals are likely to also cover instants outside the given interval; /// the zone intervals returned are not truncated to match the start and end points. /// /// /// Interval to find zone intervals for. This is allowed to be unbounded (i.e. /// infinite in both directions). /// A sequence of zone intervals covering the given interval. /// public IEnumerable GetZoneIntervals(Interval interval) { var current = interval.HasStart ? interval.Start : Instant.MinValue; var end = interval.RawEnd; while (current < end) { var zoneInterval = GetZoneInterval(current); yield return zoneInterval; // If this is the end of time, this will just fail on the next comparison. current = zoneInterval.RawEnd; } } /// /// Returns the zone intervals within the given interval, potentially coalescing some of the /// original intervals according to options. /// /// /// /// This is equivalent to , but may coalesce some intervals. /// For example, if the is specified, /// and two consecutive zone intervals have the same offset but different names, a single zone interval /// will be returned instead of two separate ones. When zone intervals are coalesced, all aspects of /// the first zone interval are used except its end instant, which is taken from the second zone interval. /// /// /// As the options are only used to determine which intervals to coalesce, the /// option does not affect /// the intervals returned. /// /// /// Interval to find zone intervals for. This is allowed to be unbounded (i.e. /// infinite in both directions). /// /// public IEnumerable GetZoneIntervals(Interval interval, ZoneEqualityComparer.Options options) { if ((options & ~ZoneEqualityComparer.Options.StrictestMatch) != 0) { throw new ArgumentOutOfRangeException(nameof(options), Invariant($"The value {options} is not defined within ZoneEqualityComparer.Options")); } var zoneIntervalEqualityComparer = new ZoneEqualityComparer.ZoneIntervalEqualityComparer(options, interval); var originalIntervals = GetZoneIntervals(interval); return zoneIntervalEqualityComparer.CoalesceIntervals(originalIntervals); } } }