/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using NodaTime; using NodaTime.TimeZones; namespace QuantConnect { /// /// Represents the discontinuties in a single time zone and provides offsets to UTC. /// This type assumes that times will be asked in a forward marching manner. /// This type is not thread safe. /// public class TimeZoneOffsetProvider { private static readonly long DateTimeMaxValueTicks = DateTime.MaxValue.Ticks; private long _nextDiscontinuity; private long _currentOffsetTicks; private readonly DateTimeZone _timeZone; private readonly Queue _discontinuities; /// /// Gets the time zone this instances provides offsets for /// public DateTimeZone TimeZone { get { return _timeZone; } } /// /// Initializes a new instance of the class /// /// The time zone to provide offsets for /// The start of the range of offsets. /// Careful here, it will determine the current discontinuity offset value. When requested to convert a date we only look forward for new discontinuities /// but we suppose the current offset is correct for the requested date if in the past. /// The end of the range of offsets public TimeZoneOffsetProvider(DateTimeZone timeZone, DateTime utcStartTime, DateTime utcEndTime) { _timeZone = timeZone; // pad the end so we get the correct zone interval utcEndTime += TimeSpan.FromDays(2*365); var start = DateTimeZone.Utc.AtLeniently(LocalDateTime.FromDateTime(utcStartTime)); var end = DateTimeZone.Utc.AtLeniently(LocalDateTime.FromDateTime(utcEndTime)); var zoneIntervals = _timeZone.GetZoneIntervals(start.ToInstant(), end.ToInstant()).ToList(); // In NodaTime v3.0.5, ZoneInterval throws if `ZoneInterval.HasStart` is false and `ZoneInterval.Start` is called. // short circuit time zones with no discontinuities if (zoneIntervals.Count == 1 && zoneIntervals[0].HasStart && zoneIntervals[0].Start == Instant.MinValue && zoneIntervals[0].End == Instant.MaxValue) { // end of discontinuities _discontinuities = new Queue(); _nextDiscontinuity = DateTime.MaxValue.Ticks; _currentOffsetTicks = _timeZone.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks; } else { // get the offset just before the next discontinuity to initialize _discontinuities = new Queue(zoneIntervals.Select(GetDateTimeUtcTicks)); _nextDiscontinuity = _discontinuities.Dequeue(); _currentOffsetTicks = _timeZone.GetUtcOffset(Instant.FromDateTimeUtc(new DateTime(_nextDiscontinuity - 1, DateTimeKind.Utc))).Ticks; } } /// /// Gets the offset in ticks from this time zone to UTC, such that UTC time + offset = local time /// /// The time in UTC to get an offset to local /// The offset in ticks between UTC and the local time zone [MethodImpl(MethodImplOptions.AggressiveInlining)] public long GetOffsetTicks(DateTime utcTime) { // keep advancing our discontinuity until the requested time, don't recompute if already at max value while (utcTime.Ticks >= _nextDiscontinuity && _nextDiscontinuity != DateTimeMaxValueTicks) { // grab the next discontinuity _nextDiscontinuity = _discontinuities.Count == 0 ? DateTime.MaxValue.Ticks : _discontinuities.Dequeue(); // get the offset just before the next discontinuity var offset = _timeZone.GetUtcOffset(Instant.FromDateTimeUtc(new DateTime(_nextDiscontinuity - 1, DateTimeKind.Utc))); _currentOffsetTicks = offset.Ticks; } return _currentOffsetTicks; } /// /// Converts the specified local time to UTC. This function will advance this offset provider /// /// The local time to be converted to UTC /// The specified time in UTC [MethodImpl(MethodImplOptions.AggressiveInlining)] public DateTime ConvertToUtc(DateTime localTime) { // it's important to walk forward to the next time zone discontinuity // to ensure a deterministic read. We continue reading with the current // offset until the converted value is beyond the next discontinuity, at // which time we advance the offset again. var currentEndTimeTicks = localTime.Ticks; var currentEndTimeUtc = new DateTime(currentEndTimeTicks - _currentOffsetTicks); var offsetTicks = GetOffsetTicks(currentEndTimeUtc); var emitTimeUtcTicks = currentEndTimeTicks - offsetTicks; while (emitTimeUtcTicks > _nextDiscontinuity) { // advance to the next discontinuity to get the new offset offsetTicks = GetOffsetTicks(new DateTime(_nextDiscontinuity)); emitTimeUtcTicks = currentEndTimeTicks - offsetTicks; } return new DateTime(emitTimeUtcTicks); } /// /// Gets this offset provider's next discontinuity /// /// The next discontinuity in UTC ticks public long GetNextDiscontinuity() { return _nextDiscontinuity; } /// /// Converts the specified using the offset resolved from /// a call to /// /// The time to convert from utc /// The same instant in time represented in the [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual DateTime ConvertFromUtc(DateTime utcTime) { return new DateTime(utcTime.Ticks + GetOffsetTicks(utcTime)); } /// /// Gets the zone interval's start time in DateTimeKind.Utc ticks /// private static long GetDateTimeUtcTicks(ZoneInterval zoneInterval) { // can't convert these values directly to date times, so just shortcut these here // we set the min value to one since the logic in the ctor will decrement this value to // determine the last instant BEFORE the discontinuity if (!zoneInterval.HasStart || zoneInterval.Start == Instant.MinValue) return 1; if (zoneInterval.HasStart && zoneInterval.Start == Instant.MaxValue) return DateTime.MaxValue.Ticks; if (zoneInterval.HasStart) return zoneInterval.Start.ToDateTimeUtc().Ticks; return 1; } } }