Lean  $LEAN_TAG$
TimeZoneOffsetProvider.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.Linq;
19 using System.Runtime.CompilerServices;
20 using NodaTime;
21 using NodaTime.TimeZones;
22 
23 namespace QuantConnect
24 {
25  /// <summary>
26  /// Represents the discontinuties in a single time zone and provides offsets to UTC.
27  /// This type assumes that times will be asked in a forward marching manner.
28  /// This type is not thread safe.
29  /// </summary>
31  {
32  private static readonly long DateTimeMaxValueTicks = DateTime.MaxValue.Ticks;
33 
34  private long _nextDiscontinuity;
35  private long _currentOffsetTicks;
36  private readonly DateTimeZone _timeZone;
37  private readonly Queue<long> _discontinuities;
38 
39  /// <summary>
40  /// Gets the time zone this instances provides offsets for
41  /// </summary>
42  public DateTimeZone TimeZone
43  {
44  get { return _timeZone; }
45  }
46 
47  /// <summary>
48  /// Initializes a new instance of the <see cref="TimeZoneOffsetProvider"/> class
49  /// </summary>
50  /// <param name="timeZone">The time zone to provide offsets for</param>
51  /// <param name="utcStartTime">The start of the range of offsets.
52  /// Careful here, it will determine the current discontinuity offset value. When requested to convert a date we only look forward for new discontinuities
53  /// but we suppose the current offset is correct for the requested date if in the past.</param>
54  /// <param name="utcEndTime">The end of the range of offsets</param>
55  public TimeZoneOffsetProvider(DateTimeZone timeZone, DateTime utcStartTime, DateTime utcEndTime)
56  {
57  _timeZone = timeZone;
58 
59  // pad the end so we get the correct zone interval
60  utcEndTime += TimeSpan.FromDays(2*365);
61 
62  var start = DateTimeZone.Utc.AtLeniently(LocalDateTime.FromDateTime(utcStartTime));
63  var end = DateTimeZone.Utc.AtLeniently(LocalDateTime.FromDateTime(utcEndTime));
64  var zoneIntervals = _timeZone.GetZoneIntervals(start.ToInstant(), end.ToInstant()).ToList();
65 
66  // In NodaTime v3.0.5, ZoneInterval throws if `ZoneInterval.HasStart` is false and `ZoneInterval.Start` is called.
67  // short circuit time zones with no discontinuities
68  if (zoneIntervals.Count == 1 && zoneIntervals[0].HasStart && zoneIntervals[0].Start == Instant.MinValue && zoneIntervals[0].End == Instant.MaxValue)
69  {
70  // end of discontinuities
71  _discontinuities = new Queue<long>();
72  _nextDiscontinuity = DateTime.MaxValue.Ticks;
73  _currentOffsetTicks = _timeZone.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks;
74  }
75  else
76  {
77  // get the offset just before the next discontinuity to initialize
78  _discontinuities = new Queue<long>(zoneIntervals.Select(GetDateTimeUtcTicks));
79  _nextDiscontinuity = _discontinuities.Dequeue();
80  _currentOffsetTicks = _timeZone.GetUtcOffset(Instant.FromDateTimeUtc(new DateTime(_nextDiscontinuity - 1, DateTimeKind.Utc))).Ticks;
81  }
82  }
83 
84  /// <summary>
85  /// Gets the offset in ticks from this time zone to UTC, such that UTC time + offset = local time
86  /// </summary>
87  /// <param name="utcTime">The time in UTC to get an offset to local</param>
88  /// <returns>The offset in ticks between UTC and the local time zone</returns>
89  [MethodImpl(MethodImplOptions.AggressiveInlining)]
90  public long GetOffsetTicks(DateTime utcTime)
91  {
92  // keep advancing our discontinuity until the requested time, don't recompute if already at max value
93  while (utcTime.Ticks >= _nextDiscontinuity && _nextDiscontinuity != DateTimeMaxValueTicks)
94  {
95  // grab the next discontinuity
96  _nextDiscontinuity = _discontinuities.Count == 0
97  ? DateTime.MaxValue.Ticks
98  : _discontinuities.Dequeue();
99 
100  // get the offset just before the next discontinuity
101  var offset = _timeZone.GetUtcOffset(Instant.FromDateTimeUtc(new DateTime(_nextDiscontinuity - 1, DateTimeKind.Utc)));
102  _currentOffsetTicks = offset.Ticks;
103  }
104 
105  return _currentOffsetTicks;
106  }
107 
108  /// <summary>
109  /// Converts the specified local time to UTC. This function will advance this offset provider
110  /// </summary>
111  /// <param name="localTime">The local time to be converted to UTC</param>
112  /// <returns>The specified time in UTC</returns>
113  [MethodImpl(MethodImplOptions.AggressiveInlining)]
114  public DateTime ConvertToUtc(DateTime localTime)
115  {
116  // it's important to walk forward to the next time zone discontinuity
117  // to ensure a deterministic read. We continue reading with the current
118  // offset until the converted value is beyond the next discontinuity, at
119  // which time we advance the offset again.
120  var currentEndTimeTicks = localTime.Ticks;
121  var currentEndTimeUtc = new DateTime(currentEndTimeTicks - _currentOffsetTicks);
122  var offsetTicks = GetOffsetTicks(currentEndTimeUtc);
123  var emitTimeUtcTicks = currentEndTimeTicks - offsetTicks;
124  while (emitTimeUtcTicks > _nextDiscontinuity)
125  {
126  // advance to the next discontinuity to get the new offset
127  offsetTicks = GetOffsetTicks(new DateTime(_nextDiscontinuity));
128  emitTimeUtcTicks = currentEndTimeTicks - offsetTicks;
129  }
130 
131  return new DateTime(emitTimeUtcTicks);
132  }
133 
134  /// <summary>
135  /// Gets this offset provider's next discontinuity
136  /// </summary>
137  /// <returns>The next discontinuity in UTC ticks</returns>
138  public long GetNextDiscontinuity()
139  {
140  return _nextDiscontinuity;
141  }
142 
143  /// <summary>
144  /// Converts the specified <paramref name="utcTime"/> using the offset resolved from
145  /// a call to <see cref="GetOffsetTicks"/>
146  /// </summary>
147  /// <param name="utcTime">The time to convert from utc</param>
148  /// <returns>The same instant in time represented in the <see cref="TimeZone"/></returns>
149  [MethodImpl(MethodImplOptions.AggressiveInlining)]
150  public virtual DateTime ConvertFromUtc(DateTime utcTime)
151  {
152  return new DateTime(utcTime.Ticks + GetOffsetTicks(utcTime));
153  }
154 
155  /// <summary>
156  /// Gets the zone interval's start time in DateTimeKind.Utc ticks
157  /// </summary>
158  private static long GetDateTimeUtcTicks(ZoneInterval zoneInterval)
159  {
160  // can't convert these values directly to date times, so just shortcut these here
161  // we set the min value to one since the logic in the ctor will decrement this value to
162  // determine the last instant BEFORE the discontinuity
163  if (!zoneInterval.HasStart || zoneInterval.Start == Instant.MinValue) return 1;
164  if (zoneInterval.HasStart && zoneInterval.Start == Instant.MaxValue) return DateTime.MaxValue.Ticks;
165  if (zoneInterval.HasStart) return zoneInterval.Start.ToDateTimeUtc().Ticks;
166 
167  return 1;
168  }
169  }
170 }