1 /*
2  * hunt-time: A time library for D programming language.
3  *
4  * Copyright (C) 2015-2018 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.time.zone.ZoneRules;
13 
14 import hunt.stream.DataInput;
15 import hunt.stream.DataOutput;
16 import hunt.Exceptions;
17 
18 //import hunt.io.ObjectInputStream;
19 import hunt.stream.Common;
20 // import hunt.time.Duration;
21 import hunt.time.Instant;
22 import hunt.time.LocalDate;
23 import hunt.time.LocalDateTime;
24 import hunt.time.ZoneOffset;
25 // import hunt.time.Year;
26 import hunt.collection.ArrayList;
27 import hunt.time.zone.Ser;
28 import hunt.collection.Collections;
29 import hunt.collection.List;
30 import hunt.Integer;
31 import hunt.Long;
32 import hunt.math.Helper;
33 import hunt.collection.HashMap;
34 import hunt.time.zone.ZoneOffsetTransitionRule;
35 import hunt.time.zone.ZoneOffsetTransition;
36 import hunt.text.Common;
37 import hunt.util.ArrayHelper;
38 import hunt.time.util.Common;
39 import hunt.util.ArrayHelper;
40 
41 import std.algorithm.searching;
42 import std.concurrency : initOnce;
43 
44 /**
45  * The rules defining how the zone offset varies for a single time-zone.
46  * !(p)
47  * The rules model all the historic and future transitions for a time-zone.
48  * {@link ZoneOffsetTransition} is used for known transitions, typically historic.
49  * {@link ZoneOffsetTransitionRule} is used for future transitions that are based
50  * on the result of an algorithm.
51  * !(p)
52  * The rules are loaded via {@link ZoneRulesProvider} using a {@link ZoneId}.
53  * The same rules may be shared internally between multiple zone IDs.
54  * !(p)
55  * Serializing an instance of {@code ZoneRules} will store the entire set of rules.
56  * It does not store the zone ID as it is not part of the state of this object.
57  * !(p)
58  * A rule implementation may or may not store full information about historic
59  * and future transitions, and the information stored is only as accurate as
60  * that supplied to the implementation by the rules provider.
61  * Applications should treat the data provided as representing the best information
62  * available to the implementation of this rule.
63  *
64  * @implSpec
65  * This class is immutable and thread-safe.
66  *
67  */
68 public final class ZoneRules // : Serializable
69 {
70 
71     /**
72      * The last year to have its transitions cached.
73      */
74     private enum int LAST_CACHED_YEAR = 2100;
75 
76     /**
77      * The transitions between standard offsets (epoch seconds), sorted.
78      */
79     private long[] standardTransitions;
80     /**
81      * The standard offsets.
82      */
83     private ZoneOffset[] standardOffsets;
84     /**
85      * The transitions between instants (epoch seconds), sorted.
86      */
87     private long[] savingsInstantTransitions;
88     /**
89      * The transitions between local date-times, sorted.
90      * This is a paired array, where the first entry is the start of the transition
91      * and the second entry is the end of the transition.
92      */
93     private LocalDateTime[] savingsLocalTransitions;
94     /**
95      * The wall offsets.
96      */
97     private ZoneOffset[] wallOffsets;
98     /**
99      * The last rule.
100      */
101     private ZoneOffsetTransitionRule[] lastRules;
102     /**
103      * The map of recent transitions.
104      */
105     // private  ConcurrentMap!(Integer, ZoneOffsetTransition[]) lastRulesCache =
106     //             new ConcurrentHashMap!(Integer, ZoneOffsetTransition[])();
107     private  HashMap!(Integer, ZoneOffsetTransition[]) lastRulesCache;
108     
109     /**
110      * The zero-length long array.
111      */
112     __gshared long[] EMPTY_LONG_ARRAY;
113 
114     /**
115      * The zero-length lastrules array.
116      */
117     __gshared ZoneOffsetTransitionRule[] EMPTY_LASTRULES;
118 
119     /**
120      * The zero-length ldt array.
121      */
122     __gshared LocalDateTime[] EMPTY_LDT_ARRAY;
123 
124 
125     /**
126      * Obtains an instance of a ZoneRules.
127      *
128      * @param baseStandardOffset  the standard offset to use before legal rules were set, not null
129      * @param baseWallOffset  the wall offset to use before legal rules were set, not null
130      * @param standardOffsetTransitionList  the list of changes to the standard offset, not null
131      * @param transitionList  the list of transitions, not null
132      * @param lastRules  the recurring last rules, size 16 or less, not null
133      * @return the zone rules, not null
134      */
135     public static ZoneRules of(ZoneOffset baseStandardOffset, ZoneOffset baseWallOffset,
136             List!(ZoneOffsetTransition) standardOffsetTransitionList,
137             List!(ZoneOffsetTransition) transitionList, List!(ZoneOffsetTransitionRule) lastRules)
138     {
139         assert(baseStandardOffset, "baseStandardOffset");
140         assert(baseWallOffset, "baseWallOffset");
141         assert(standardOffsetTransitionList, "standardOffsetTransitionList");
142         assert(transitionList, "transitionList");
143         assert(lastRules, "lastRules");
144         return new ZoneRules(baseStandardOffset, baseWallOffset,
145                 standardOffsetTransitionList, transitionList, lastRules);
146     }
147 
148     /**
149      * Obtains an instance of ZoneRules that has fixed zone rules.
150      *
151      * @param offset  the offset this fixed zone rules is based on, not null
152      * @return the zone rules, not null
153      * @see #isFixedOffset()
154      */
155     public static ZoneRules of(ZoneOffset offset)
156     {
157         assert(offset, "offset");
158         return new ZoneRules(offset);
159     }
160 
161     /**
162      * Creates an instance.
163      *
164      * @param baseStandardOffset  the standard offset to use before legal rules were set, not null
165      * @param baseWallOffset  the wall offset to use before legal rules were set, not null
166      * @param standardOffsetTransitionList  the list of changes to the standard offset, not null
167      * @param transitionList  the list of transitions, not null
168      * @param lastRules  the recurring last rules, size 16 or less, not null
169      */
170     this(ZoneOffset baseStandardOffset, ZoneOffset baseWallOffset,
171             List!(ZoneOffsetTransition) standardOffsetTransitionList,
172             List!(ZoneOffsetTransition) transitionList, List!(ZoneOffsetTransitionRule) lastRules)
173     {
174         // super();
175         lastRulesCache = new HashMap!(Integer, ZoneOffsetTransition[])();
176 
177         // convert standard transitions
178 
179         this.standardTransitions = new long[standardOffsetTransitionList.size()];
180 
181         this.standardOffsets = new ZoneOffset[standardOffsetTransitionList.size() + 1];
182         this.standardOffsets[0] = baseStandardOffset;
183         for (int i = 0; i < standardOffsetTransitionList.size(); i++)
184         {
185             this.standardTransitions[i] = standardOffsetTransitionList.get(i).toEpochSecond();
186             this.standardOffsets[i + 1] = standardOffsetTransitionList.get(i).getOffsetAfter();
187         }
188 
189         // convert savings transitions to locals
190         List!(LocalDateTime) localTransitionList = new ArrayList!(LocalDateTime)();
191         List!(ZoneOffset) localTransitionOffsetList = new ArrayList!(ZoneOffset)();
192         localTransitionOffsetList.add(baseWallOffset);
193         foreach (ZoneOffsetTransition trans; transitionList)
194         {
195             if (trans.isGap())
196             {
197                 localTransitionList.add(trans.getDateTimeBefore());
198                 localTransitionList.add(trans.getDateTimeAfter());
199             }
200             else
201             {
202                 localTransitionList.add(trans.getDateTimeAfter());
203                 localTransitionList.add(trans.getDateTimeBefore());
204             }
205             localTransitionOffsetList.add(trans.getOffsetAfter());
206         }
207         // this.savingsLocalTransitions = new LocalDateTime[localTransitionList.size()];
208         // foreach (data; localTransitionList)
209         //     this.savingsLocalTransitions ~= data;
210         // this.wallOffsets = new ZoneOffset[localTransitionOffsetList.size()];
211         // foreach (data; localTransitionOffsetList)
212         // {
213         //     this.wallOffsets ~= data;
214         // }
215 
216         this.savingsLocalTransitions = localTransitionList.toArray();
217         this.wallOffsets = localTransitionOffsetList.toArray();
218         // convert savings transitions to instants
219         this.savingsInstantTransitions = new long[transitionList.size()];
220         for (int i = 0; i < transitionList.size(); i++)
221         {
222             this.savingsInstantTransitions[i] = transitionList.get(i).toEpochSecond();
223         }
224 
225         // last rules
226         if (lastRules.size() > 16)
227         {
228             throw new IllegalArgumentException("Too many transition rules");
229         }
230         // this.lastRules = new ZoneOffsetTransitionRule[lastRules.size()];
231         // foreach (data; lastRules)
232         //     this.lastRules ~= data;
233         this.lastRules = lastRules.toArray();
234     }
235 
236     /**
237      * Constructor.
238      *
239      * @param standardTransitions  the standard transitions, not null
240      * @param standardOffsets  the standard offsets, not null
241      * @param savingsInstantTransitions  the standard transitions, not null
242      * @param wallOffsets  the wall offsets, not null
243      * @param lastRules  the recurring last rules, size 15 or less, not null
244      */
245     private this(long[] standardTransitions, ZoneOffset[] standardOffsets,
246             long[] savingsInstantTransitions, ZoneOffset[] wallOffsets,
247             ZoneOffsetTransitionRule[] lastRules)
248     {
249         // super();
250 
251         lastRulesCache = new HashMap!(Integer, ZoneOffsetTransition[])();
252         this.standardTransitions = standardTransitions;
253         this.standardOffsets = standardOffsets;
254         this.savingsInstantTransitions = savingsInstantTransitions;
255         this.wallOffsets = wallOffsets;
256         this.lastRules = lastRules;
257 
258         if (savingsInstantTransitions.length == 0)
259         {
260             this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
261         }
262         else
263         {
264             // convert savings transitions to locals
265             List!(LocalDateTime) localTransitionList = new ArrayList!(LocalDateTime)();
266             for (int i = 0; i < savingsInstantTransitions.length; i++)
267             {
268                 ZoneOffset before = wallOffsets[i];
269                 ZoneOffset after = wallOffsets[i + 1];
270                 ZoneOffsetTransition trans = new ZoneOffsetTransition(savingsInstantTransitions[i],
271                         before, after);
272                 if (trans.isGap())
273                 {
274                     localTransitionList.add(trans.getDateTimeBefore());
275                     localTransitionList.add(trans.getDateTimeAfter());
276                 }
277                 else
278                 {
279                     localTransitionList.add(trans.getDateTimeAfter());
280                     localTransitionList.add(trans.getDateTimeBefore());
281                 }
282             }
283             // this.savingsLocalTransitions = new LocalDateTime[localTransitionList.size()];
284             // foreach (data; localTransitionList)
285             //     this.savingsLocalTransitions ~= data;
286             this.savingsLocalTransitions = localTransitionList.toArray();
287         }
288     }
289 
290     /**
291      * Creates an instance of ZoneRules that has fixed zone rules.
292      *
293      * @param offset  the offset this fixed zone rules is based on, not null
294      * @see #isFixedOffset()
295      */
296     private this(ZoneOffset offset)
297     {
298         lastRulesCache = new HashMap!(Integer, ZoneOffsetTransition[])();
299         this.standardOffsets = new ZoneOffset[1];
300         this.standardOffsets[0] = offset;
301         this.standardTransitions = EMPTY_LONG_ARRAY;
302         this.savingsInstantTransitions = EMPTY_LONG_ARRAY;
303         this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
304         this.wallOffsets = standardOffsets;
305         this.lastRules = EMPTY_LASTRULES;
306     }
307 
308     /**
309      * Defend against malicious streams.
310      *
311      * @param s the stream to read
312      * @throws InvalidObjectException always
313      */
314     ///@gxc
315     // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {
316     //     throw new InvalidObjectException("Deserialization via serialization delegate");
317     // }
318 
319     /**
320      * Writes the object using a
321      * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.Ser">dedicated serialized form</a>.
322      * @serialData
323      * <pre style="font-size:1.0em">{@code
324      *
325      *   _out.writeByte(1);  // identifies a ZoneRules
326      *   _out.writeInt(standardTransitions.length);
327      *   foreach(long trans ; standardTransitions) {
328      *       Ser.writeEpochSec(trans, _out);
329      *   }
330      *   foreach(ZoneOffset offset ; standardOffsets) {
331      *       Ser.writeOffset(offset, _out);
332      *   }
333      *   _out.writeInt(savingsInstantTransitions.length);
334      *   foreach(long trans ; savingsInstantTransitions) {
335      *       Ser.writeEpochSec(trans, _out);
336      *   }
337      *   foreach(ZoneOffset offset ; wallOffsets) {
338      *       Ser.writeOffset(offset, _out);
339      *   }
340      *   _out.writeByte(lastRules.length);
341      *   foreach(ZoneOffsetTransitionRule rule ; lastRules) {
342      *       rule.writeExternal(_out);
343      *   }
344      * }
345      * </pre>
346      * !(p)
347      * Epoch second values used for offsets are encoded _in a variable
348      * length form to make the common cases put fewer bytes _in the stream.
349      * <pre style="font-size:1.0em">{@code
350      *
351      *  static void writeEpochSec(long epochSec, DataOutput _out) throws IOException {
352      *     if (epochSec >= -4575744000L && epochSec < 10413792000L && epochSec % 900 == 0) {  // quarter hours between 1825 and 2300
353      *         int store = cast(int) ((epochSec + 4575744000L) / 900);
354      *         _out.writeByte((store >>> 16) & 255);
355      *         _out.writeByte((store >>> 8) & 255);
356      *         _out.writeByte(store & 255);
357      *      } else {
358      *          _out.writeByte(255);
359      *          _out.writeLong(epochSec);
360      *      }
361      *  }
362      * }
363      * </pre>
364      * !(p)
365      * ZoneOffset values are encoded _in a variable length form so the
366      * common cases put fewer bytes _in the stream.
367      * <pre style="font-size:1.0em">{@code
368      *
369      *  static void writeOffset(ZoneOffset offset, DataOutput _out) throws IOException {
370      *     final int offsetSecs = offset.getTotalSeconds();
371      *     int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127;  // compress to -72 to +72
372      *     _out.writeByte(offsetByte);
373      *     if (offsetByte == 127) {
374      *         _out.writeInt(offsetSecs);
375      *     }
376      * }
377      *}
378      * </pre>
379      * @return the replacing object, not null
380      */
381     private Object writeReplace()
382     {
383         return new Ser(Ser.ZRULES, this);
384     }
385 
386     /**
387      * Writes the state to the stream.
388      *
389      * @param _out  the output stream, not null
390      * @throws IOException if an error occurs
391      */
392     void writeExternal(DataOutput _out) /*throws IOException*/
393     {
394         _out.writeInt(cast(int)(standardTransitions.length));
395         foreach (long trans; standardTransitions)
396         {
397             Ser.writeEpochSec(trans, _out);
398         }
399         foreach (ZoneOffset offset; standardOffsets)
400         {
401             Ser.writeOffset(offset, _out);
402         }
403         _out.writeInt(cast(int)(savingsInstantTransitions.length));
404         foreach (long trans; savingsInstantTransitions)
405         {
406             Ser.writeEpochSec(trans, _out);
407         }
408         foreach (ZoneOffset offset; wallOffsets)
409         {
410             Ser.writeOffset(offset, _out);
411         }
412         _out.writeByte(cast(int)(lastRules.length));
413         foreach (ZoneOffsetTransitionRule rule; lastRules)
414         {
415             rule.writeExternal(_out);
416         }
417     }
418 
419     /**
420      * Reads the state from the stream.
421      *
422      * @param _in  the input stream, not null
423      * @return the created object, not null
424      * @throws IOException if an error occurs
425      */
426     static ZoneRules readExternal(DataInput _in) /*throws IOException, ClassNotFoundException*/
427     {
428         int stdSize = _in.readInt();
429         long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY : new long[stdSize];
430         for (int i = 0; i < stdSize; i++)
431         {
432             stdTrans[i] = Ser.readEpochSec(_in);
433         }
434         ZoneOffset[] stdOffsets = new ZoneOffset[stdSize + 1];
435         for (int i = 0; i < stdOffsets.length; i++)
436         {
437             stdOffsets[i] = Ser.readOffset(_in);
438         }
439         int savSize = _in.readInt();
440         long[] savTrans = (savSize == 0) ? EMPTY_LONG_ARRAY : new long[savSize];
441         for (int i = 0; i < savSize; i++)
442         {
443             savTrans[i] = Ser.readEpochSec(_in);
444         }
445         ZoneOffset[] savOffsets = new ZoneOffset[savSize + 1];
446         for (int i = 0; i < savOffsets.length; i++)
447         {
448             savOffsets[i] = Ser.readOffset(_in);
449         }
450         int ruleSize = _in.readByte();
451         ZoneOffsetTransitionRule[] rules = (ruleSize == 0) ? EMPTY_LASTRULES
452             : new ZoneOffsetTransitionRule[ruleSize];
453         for (int i = 0; i < ruleSize; i++)
454         {
455             rules[i] = ZoneOffsetTransitionRule.readExternal(_in);
456         }
457         return new ZoneRules(stdTrans, stdOffsets, savTrans, savOffsets, rules);
458     }
459 
460     /**
461      * Checks of the zone rules are fixed, such that the offset never varies.
462      *
463      * @return true if the time-zone is fixed and the offset never changes
464      */
465     public bool isFixedOffset()
466     {
467         return savingsInstantTransitions.length == 0;
468     }
469 
470     /**
471      * Gets the offset applicable at the specified instant _in these rules.
472      * !(p)
473      * The mapping from an instant to an offset is simple, there is only
474      * one valid offset for each instant.
475      * This method returns that offset.
476      *
477      * @param instant  the instant to find the offset for, not null, but null
478      *  may be ignored if the rules have a single offset for all instants
479      * @return the offset, not null
480      */
481     public ZoneOffset getOffset(Instant instant)
482     {
483         if (savingsInstantTransitions.length == 0)
484         {
485             return standardOffsets[0];
486         }
487         long epochSec = instant.getEpochSecond();
488         // check if using last rules
489         if (lastRules.length > 0
490                 && epochSec > savingsInstantTransitions[savingsInstantTransitions.length - 1])
491         {
492             int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);
493             ZoneOffsetTransition[] transArray = findTransitionArray(year);
494             ZoneOffsetTransition trans = null;
495             for (int i = 0; i < transArray.length; i++)
496             {
497                 trans = transArray[i];
498                 if (epochSec < trans.toEpochSecond())
499                 {
500                     return trans.getOffsetBefore();
501                 }
502             }
503             return trans.getOffsetAfter();
504         }
505 
506         // using historic rules
507         import hunt.text.Common;
508 
509         int index = ArrayHelper.binarySearch(savingsInstantTransitions, epochSec);
510         if (index == -1)
511             index = -(cast(int)(savingsInstantTransitions.length)) - 1;
512         if (index < 0)
513         {
514             // switch negative insert position to start of matched range
515             index = -index - 2;
516         }
517         return wallOffsets[index + 1];
518     }
519 
520     /**
521      * Gets a suitable offset for the specified local date-time _in these rules.
522      * !(p)
523      * The mapping from a local date-time to an offset is not straightforward.
524      * There are three cases:
525      * !(ul)
526      * !(li)Normal, with one valid offset. For the vast majority of the year, the normal
527      *  case applies, where there is a single valid offset for the local date-time.</li>
528      * !(li)Gap, with zero valid offsets. This is when clocks jump forward typically
529      *  due to the spring daylight savings change from "winter" to "summer".
530      *  In a gap there are local date-time values with no valid offset.</li>
531      * !(li)Overlap, with two valid offsets. This is when clocks are set back typically
532      *  due to the autumn daylight savings change from "summer" to "winter".
533      *  In an overlap there are local date-time values with two valid offsets.</li>
534      * </ul>
535      * Thus, for any given local date-time there can be zero, one or two valid offsets.
536      * This method returns the single offset _in the Normal case, and _in the Gap or Overlap
537      * case it returns the offset before the transition.
538      * !(p)
539      * Since, _in the case of Gap and Overlap, the offset returned is a "best" value, rather
540      * than the "correct" value, it should be treated with care. Applications that care
541      * about the correct offset should use a combination of this method,
542      * {@link #getValidOffsets(LocalDateTime)} and {@link #getTransition(LocalDateTime)}.
543      *
544      * @param localDateTime  the local date-time to query, not null, but null
545      *  may be ignored if the rules have a single offset for all instants
546      * @return the best available offset for the local date-time, not null
547      */
548     public ZoneOffset getOffset(LocalDateTime localDateTime)
549     {
550         Object info = getOffsetInfo(localDateTime);
551         if (cast(ZoneOffsetTransition)(info) !is null)
552         {
553             return (cast(ZoneOffsetTransition) info).getOffsetBefore();
554         }
555         return cast(ZoneOffset) info;
556     }
557 
558     /**
559      * Gets the offset applicable at the specified local date-time _in these rules.
560      * !(p)
561      * The mapping from a local date-time to an offset is not straightforward.
562      * There are three cases:
563      * !(ul)
564      * !(li)Normal, with one valid offset. For the vast majority of the year, the normal
565      *  case applies, where there is a single valid offset for the local date-time.</li>
566      * !(li)Gap, with zero valid offsets. This is when clocks jump forward typically
567      *  due to the spring daylight savings change from "winter" to "summer".
568      *  In a gap there are local date-time values with no valid offset.</li>
569      * !(li)Overlap, with two valid offsets. This is when clocks are set back typically
570      *  due to the autumn daylight savings change from "summer" to "winter".
571      *  In an overlap there are local date-time values with two valid offsets.</li>
572      * </ul>
573      * Thus, for any given local date-time there can be zero, one or two valid offsets.
574      * This method returns that list of valid offsets, which is a list of size 0, 1 or 2.
575      * In the case where there are two offsets, the earlier offset is returned at index 0
576      * and the later offset at index 1.
577      * !(p)
578      * There are various ways to handle the conversion from a {@code LocalDateTime}.
579      * One technique, using this method, would be:
580      * !(pre)
581      *  List&lt;ZoneOffset&gt; validOffsets = rules.getOffset(localDT);
582      *  if (validOffsets.size() == 1) {
583      *    // Normal case: only one valid offset
584      *    zoneOffset = validOffsets.get(0);
585      *  } else {
586      *    // Gap or Overlap: determine what to do from transition (which will be non-null)
587      *    ZoneOffsetTransition trans = rules.getTransition(localDT);
588      *  }
589      * </pre>
590      * !(p)
591      * In theory, it is possible for there to be more than two valid offsets.
592      * This would happen if clocks to be put back more than once _in quick succession.
593      * This has never happened _in the history of time-zones and thus has no special handling.
594      * However, if it were to happen, then the list would return more than 2 entries.
595      *
596      * @param localDateTime  the local date-time to query for valid offsets, not null, but null
597      *  may be ignored if the rules have a single offset for all instants
598      * @return the list of valid offsets, may be immutable, not null
599      */
600     public List!(ZoneOffset) getValidOffsets(LocalDateTime localDateTime)
601     {
602         // should probably be optimized
603         Object info = getOffsetInfo(localDateTime);
604         if (cast(ZoneOffsetTransition)(info) !is null)
605         {
606             return (cast(ZoneOffsetTransition) info).getValidOffsets();
607         }
608         return Collections.singletonList(cast(ZoneOffset) info);
609     }
610 
611     /**
612      * Gets the offset transition applicable at the specified local date-time _in these rules.
613      * !(p)
614      * The mapping from a local date-time to an offset is not straightforward.
615      * There are three cases:
616      * !(ul)
617      * !(li)Normal, with one valid offset. For the vast majority of the year, the normal
618      *  case applies, where there is a single valid offset for the local date-time.</li>
619      * !(li)Gap, with zero valid offsets. This is when clocks jump forward typically
620      *  due to the spring daylight savings change from "winter" to "summer".
621      *  In a gap there are local date-time values with no valid offset.</li>
622      * !(li)Overlap, with two valid offsets. This is when clocks are set back typically
623      *  due to the autumn daylight savings change from "summer" to "winter".
624      *  In an overlap there are local date-time values with two valid offsets.</li>
625      * </ul>
626      * A transition is used to model the cases of a Gap or Overlap.
627      * The Normal case will return null.
628      * !(p)
629      * There are various ways to handle the conversion from a {@code LocalDateTime}.
630      * One technique, using this method, would be:
631      * !(pre)
632      *  ZoneOffsetTransition trans = rules.getTransition(localDT);
633      *  if (trans !is null) {
634      *    // Gap or Overlap: determine what to do from transition
635      *  } else {
636      *    // Normal case: only one valid offset
637      *    zoneOffset = rule.getOffset(localDT);
638      *  }
639      * </pre>
640      *
641      * @param localDateTime  the local date-time to query for offset transition, not null, but null
642      *  may be ignored if the rules have a single offset for all instants
643      * @return the offset transition, null if the local date-time is not _in transition
644      */
645     public ZoneOffsetTransition getTransition(LocalDateTime localDateTime)
646     {
647         Object info = getOffsetInfo(localDateTime);
648         return (cast(ZoneOffsetTransition)(info) !is null ? cast(ZoneOffsetTransition) info : null);
649     }
650 
651     private Object getOffsetInfo(LocalDateTime dt)
652     {
653         if (savingsInstantTransitions.length == 0)
654         {
655             return standardOffsets[0];
656         }
657         // check if using last rules
658         if (lastRules.length > 0
659                 && dt.isAfter(savingsLocalTransitions[savingsLocalTransitions.length - 1]))
660         {
661             ZoneOffsetTransition[] transArray = findTransitionArray(dt.getYear());
662             Object info = null;
663             foreach (ZoneOffsetTransition trans; transArray)
664             {
665                 info = findOffsetInfo(dt, trans);
666                 if (cast(ZoneOffsetTransition)(info) !is null || (info == trans.getOffsetBefore()))
667                 {
668                     return info;
669                 }
670             }
671             return info;
672         }
673 
674         // using historic rules
675         int index = ArrayHelper.binarySearch(savingsLocalTransitions, dt);
676         if (index == -1)
677         {
678             // before first transition
679             return wallOffsets[0];
680         }
681         if (index < 0)
682         {
683             // switch negative insert position to start of matched range
684             index = -index - 2;
685         }
686         else if (index < savingsLocalTransitions.length - 1
687                 && (savingsLocalTransitions[index] == savingsLocalTransitions[index + 1]))
688         {
689             // handle overlap immediately following gap
690             index++;
691         }
692         if ((index & 1) == 0)
693         {
694             // gap or overlap
695             LocalDateTime dtBefore = savingsLocalTransitions[index];
696             LocalDateTime dtAfter = savingsLocalTransitions[index + 1];
697             ZoneOffset offsetBefore = wallOffsets[index / 2];
698             ZoneOffset offsetAfter = wallOffsets[index / 2 + 1];
699             if (offsetAfter.getTotalSeconds() > offsetBefore.getTotalSeconds())
700             {
701                 // gap
702                 return new ZoneOffsetTransition(dtBefore, offsetBefore, offsetAfter);
703             }
704             else
705             {
706                 // overlap
707                 return new ZoneOffsetTransition(dtAfter, offsetBefore, offsetAfter);
708             }
709         }
710         else
711         {
712             // normal (neither gap or overlap)
713             return wallOffsets[index / 2 + 1];
714         }
715     }
716 
717     /**
718      * Finds the offset info for a local date-time and transition.
719      *
720      * @param dt  the date-time, not null
721      * @param trans  the transition, not null
722      * @return the offset info, not null
723      */
724     private Object findOffsetInfo(LocalDateTime dt, ZoneOffsetTransition trans)
725     {
726         LocalDateTime localTransition = trans.getDateTimeBefore();
727         if (trans.isGap())
728         {
729             if (dt.isBefore(localTransition))
730             {
731                 return trans.getOffsetBefore();
732             }
733             if (dt.isBefore(trans.getDateTimeAfter()))
734             {
735                 return trans;
736             }
737             else
738             {
739                 return trans.getOffsetAfter();
740             }
741         }
742         else
743         {
744             if (dt.isBefore(localTransition) == false)
745             {
746                 return trans.getOffsetAfter();
747             }
748             if (dt.isBefore(trans.getDateTimeAfter()))
749             {
750                 return trans.getOffsetBefore();
751             }
752             else
753             {
754                 return trans;
755             }
756         }
757     }
758 
759     /**
760      * Finds the appropriate transition array for the given year.
761      *
762      * @param year  the year, not null
763      * @return the transition array, not null
764      */
765     private ZoneOffsetTransition[] findTransitionArray(int year)
766     {
767         Integer yearObj = new Integer(year); // should use Year class, but this saves a class load
768         if (lastRulesCache.containsKey(yearObj))
769         {
770             return lastRulesCache.get(yearObj);
771         }
772         ZoneOffsetTransitionRule[] ruleArray = lastRules;
773         ZoneOffsetTransition[] transArray = new ZoneOffsetTransition[ruleArray.length];
774         for (int i = 0; i < ruleArray.length; i++)
775         {
776             transArray[i] = ruleArray[i].createTransition(year);
777         }
778         if (year < LAST_CACHED_YEAR)
779         {
780             lastRulesCache.putIfAbsent(yearObj, transArray);
781         }
782         return transArray;
783     }
784 
785     /**
786      * Gets the standard offset for the specified instant _in this zone.
787      * !(p)
788      * This provides access to historic information on how the standard offset
789      * has changed over time.
790      * The standard offset is the offset before any daylight saving time is applied.
791      * This is typically the offset applicable during winter.
792      *
793      * @param instant  the instant to find the offset information for, not null, but null
794      *  may be ignored if the rules have a single offset for all instants
795      * @return the standard offset, not null
796      */
797     public ZoneOffset getStandardOffset(Instant instant)
798     {
799         if (savingsInstantTransitions.length == 0)
800         {
801             return standardOffsets[0];
802         }
803         long epochSec = instant.getEpochSecond();
804         int index = ArrayHelper.binarySearch(standardTransitions, epochSec);
805         if (index < 0)
806         {
807             // switch negative insert position to start of matched range
808             index = -index - 2;
809         }
810         return standardOffsets[index + 1];
811     }
812 
813     /**
814      * Gets the amount of daylight savings _in use for the specified instant _in this zone.
815      * !(p)
816      * This provides access to historic information on how the amount of daylight
817      * savings has changed over time.
818      * This is the difference between the standard offset and the actual offset.
819      * Typically the amount is zero during winter and one hour during summer.
820      * Time-zones are second-based, so the nanosecond part of the duration will be zero.
821      * !(p)
822      * This default implementation calculates the duration from the
823      * {@link #getOffset(hunt.time.Instant) actual} and
824      * {@link #getStandardOffset(hunt.time.Instant) standard} offsets.
825      *
826      * @param instant  the instant to find the daylight savings for, not null, but null
827      *  may be ignored if the rules have a single offset for all instants
828      * @return the difference between the standard and actual offset, not null
829      */
830     // public Duration getDaylightSavings(Instant instant)
831     // {
832     //     if (savingsInstantTransitions.length == 0)
833     //     {
834     //         return Duration.ZERO;
835     //     }
836     //     ZoneOffset standardOffset = getStandardOffset(instant);
837     //     ZoneOffset actualOffset = getOffset(instant);
838     //     return Duration.ofSeconds(actualOffset.getTotalSeconds() - standardOffset.getTotalSeconds());
839     // }
840 
841     /**
842      * Checks if the specified instant is _in daylight savings.
843      * !(p)
844      * This checks if the standard offset and the actual offset are the same
845      * for the specified instant.
846      * If they are not, it is assumed that daylight savings is _in operation.
847      * !(p)
848      * This default implementation compares the {@link #getOffset(hunt.time.Instant) actual}
849      * and {@link #getStandardOffset(hunt.time.Instant) standard} offsets.
850      *
851      * @param instant  the instant to find the offset information for, not null, but null
852      *  may be ignored if the rules have a single offset for all instants
853      * @return the standard offset, not null
854      */
855     public bool isDaylightSavings(Instant instant)
856     {
857         return ((getStandardOffset(instant) == getOffset(instant)) == false);
858     }
859 
860     /**
861      * Checks if the offset date-time is valid for these rules.
862      * !(p)
863      * To be valid, the local date-time must not be _in a gap and the offset
864      * must match one of the valid offsets.
865      * !(p)
866      * This default implementation checks if {@link #getValidOffsets(hunt.time.LocalDateTime)}
867      * contains the specified offset.
868      *
869      * @param localDateTime  the date-time to check, not null, but null
870      *  may be ignored if the rules have a single offset for all instants
871      * @param offset  the offset to check, null returns false
872      * @return true if the offset date-time is valid for these rules
873      */
874     public bool isValidOffset(LocalDateTime localDateTime, ZoneOffset offset)
875     {
876         return getValidOffsets(localDateTime).contains(offset);
877     }
878 
879     /**
880      * Gets the next transition after the specified instant.
881      * !(p)
882      * This returns details of the next transition after the specified instant.
883      * For example, if the instant represents a point where "Summer" daylight savings time
884      * applies, then the method will return the transition to the next "Winter" time.
885      *
886      * @param instant  the instant to get the next transition after, not null, but null
887      *  may be ignored if the rules have a single offset for all instants
888      * @return the next transition after the specified instant, null if this is after the last transition
889      */
890     public ZoneOffsetTransition nextTransition(Instant instant)
891     {
892         if (savingsInstantTransitions.length == 0)
893         {
894             return null;
895         }
896         long epochSec = instant.getEpochSecond();
897         // check if using last rules
898         if (epochSec >= savingsInstantTransitions[savingsInstantTransitions.length - 1])
899         {
900             if (lastRules.length == 0)
901             {
902                 return null;
903             }
904             // search year the instant is _in
905             int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);
906             ZoneOffsetTransition[] transArray = findTransitionArray(year);
907             foreach (ZoneOffsetTransition trans; transArray)
908             {
909                 if (epochSec < trans.toEpochSecond())
910                 {
911                     return trans;
912                 }
913             }
914             // use first from following year
915             if (year < 999_999_999/* Year.MAX_VALUE */)
916             {
917                 transArray = findTransitionArray(year + 1);
918                 return transArray[0];
919             }
920             return null;
921         }
922 
923         // using historic rules
924         int index = ArrayHelper.binarySearch(savingsInstantTransitions, epochSec);
925         if (index < 0)
926         {
927             index = -index - 1; // switched value is the next transition
928         }
929         else
930         {
931             index += 1; // exact match, so need to add one to get the next
932         }
933         return new ZoneOffsetTransition(savingsInstantTransitions[index],
934                 wallOffsets[index], wallOffsets[index + 1]);
935     }
936 
937     /**
938      * Gets the previous transition before the specified instant.
939      * !(p)
940      * This returns details of the previous transition before the specified instant.
941      * For example, if the instant represents a point where "summer" daylight saving time
942      * applies, then the method will return the transition from the previous "winter" time.
943      *
944      * @param instant  the instant to get the previous transition after, not null, but null
945      *  may be ignored if the rules have a single offset for all instants
946      * @return the previous transition before the specified instant, null if this is before the first transition
947      */
948     public ZoneOffsetTransition previousTransition(Instant instant)
949     {
950         if (savingsInstantTransitions.length == 0)
951         {
952             return null;
953         }
954         long epochSec = instant.getEpochSecond();
955         if (instant.getNano() > 0 && epochSec < Long.MAX_VALUE)
956         {
957             epochSec += 1; // allow rest of method to only use seconds
958         }
959 
960         // check if using last rules
961         long lastHistoric = savingsInstantTransitions[savingsInstantTransitions.length - 1];
962         if (lastRules.length > 0 && epochSec > lastHistoric)
963         {
964             // search year the instant is _in
965             ZoneOffset lastHistoricOffset = wallOffsets[wallOffsets.length - 1];
966             int year = findYear(epochSec, lastHistoricOffset);
967             ZoneOffsetTransition[] transArray = findTransitionArray(year);
968             for (int i = cast(int)(transArray.length) - 1; i >= 0; i--)
969             {
970                 if (epochSec > transArray[i].toEpochSecond())
971                 {
972                     return transArray[i];
973                 }
974             }
975             // use last from preceding year
976             int lastHistoricYear = findYear(lastHistoric, lastHistoricOffset);
977             if (--year > lastHistoricYear)
978             {
979                 transArray = findTransitionArray(year);
980                 return transArray[transArray.length - 1];
981             }
982             // drop through
983         }
984 
985         // using historic rules
986         int index = ArrayHelper.binarySearch(savingsInstantTransitions, epochSec);
987         if (index < 0)
988         {
989             index = -index - 1;
990         }
991         if (index <= 0)
992         {
993             return null;
994         }
995         return new ZoneOffsetTransition(savingsInstantTransitions[index - 1],
996                 wallOffsets[index - 1], wallOffsets[index]);
997     }
998 
999     private int findYear(long epochSecond, ZoneOffset offset)
1000     {
1001         // inline for performance
1002         long localSecond = epochSecond + offset.getTotalSeconds();
1003         long localEpochDay = MathHelper.floorDiv(localSecond, 86400);
1004         return LocalDate.ofEpochDay(localEpochDay).getYear();
1005     }
1006 
1007     /**
1008      * Gets the complete list of fully defined transitions.
1009      * !(p)
1010      * The complete set of transitions for this rules instance is defined by this method
1011      * and {@link #getTransitionRules()}. This method returns those transitions that have
1012      * been fully defined. These are typically historical, but may be _in the future.
1013      * !(p)
1014      * The list will be empty for fixed offset rules and for any time-zone where there has
1015      * only ever been a single offset. The list will also be empty if the transition rules are unknown.
1016      *
1017      * @return an immutable list of fully defined transitions, not null
1018      */
1019     public List!(ZoneOffsetTransition) getTransitions()
1020     {
1021         List!(ZoneOffsetTransition) list = new ArrayList!(ZoneOffsetTransition)();
1022         for (int i = 0; i < savingsInstantTransitions.length; i++)
1023         {
1024             list.add(new ZoneOffsetTransition(savingsInstantTransitions[i],
1025                     wallOffsets[i], wallOffsets[i + 1]));
1026         }
1027         return  /* Collections.unmodifiableList */ (list);
1028     }
1029 
1030     /**
1031      * Gets the list of transition rules for years beyond those defined _in the transition list.
1032      * !(p)
1033      * The complete set of transitions for this rules instance is defined by this method
1034      * and {@link #getTransitions()}. This method returns instances of {@link ZoneOffsetTransitionRule}
1035      * that define an algorithm for when transitions will occur.
1036      * !(p)
1037      * For any given {@code ZoneRules}, this list contains the transition rules for years
1038      * beyond those years that have been fully defined. These rules typically refer to future
1039      * daylight saving time rule changes.
1040      * !(p)
1041      * If the zone defines daylight savings into the future, then the list will normally
1042      * be of size two and hold information about entering and exiting daylight savings.
1043      * If the zone does not have daylight savings, or information about future changes
1044      * is uncertain, then the list will be empty.
1045      * !(p)
1046      * The list will be empty for fixed offset rules and for any time-zone where there is no
1047      * daylight saving time. The list will also be empty if the transition rules are unknown.
1048      *
1049      * @return an immutable list of transition rules, not null
1050      */
1051     public List!(ZoneOffsetTransitionRule) getTransitionRules()
1052     {
1053         auto l = new ArrayList!ZoneOffsetTransitionRule();
1054         foreach (item; lastRules)
1055         {
1056             l.add(item);
1057         }
1058         return l;
1059     }
1060 
1061     /**
1062      * Checks if this set of rules equals another.
1063      * !(p)
1064      * Two rule sets are equal if they will always result _in the same output
1065      * for any given input instant or local date-time.
1066      * Rules from two different groups may return false even if they are _in fact the same.
1067      * !(p)
1068      * This definition should result _in implementations comparing their entire state.
1069      *
1070      * @param otherRules  the other rules, null returns false
1071      * @return true if this rules is the same as that specified
1072      */
1073     override public bool opEquals(Object otherRules)
1074     {
1075         if (this == otherRules)
1076         {
1077             return true;
1078         }
1079         import std.algorithm : equal;
1080 
1081         if (cast(ZoneRules)(otherRules) !is null)
1082         {
1083             ZoneRules other = cast(ZoneRules) otherRules;
1084             return equal(standardTransitions, other.standardTransitions)
1085                 && equal(standardOffsets, other.standardOffsets)
1086                 && equal(savingsInstantTransitions,
1087                         other.savingsInstantTransitions) && equal(wallOffsets,
1088                         other.wallOffsets) && equal(lastRules, other.lastRules);
1089         }
1090         return false;
1091     }
1092 
1093     /**
1094      * Returns a suitable hash code given the definition of {@code #equals}.
1095      *
1096      * @return the hash code
1097      */
1098     override public size_t toHash() @trusted nothrow
1099     {
1100         return hashOf(standardTransitions) ^ hashOf(standardOffsets) ^ hashOf(
1101                 savingsInstantTransitions) ^ hashOf(wallOffsets) ^ hashOf(lastRules);
1102     }
1103 
1104     /**
1105      * Returns a string describing this object.
1106      *
1107      * @return a string for debugging, not null
1108      */
1109     override public string toString()
1110     {
1111         return "ZoneRules[currentStandardOffset="
1112             ~ standardOffsets[standardOffsets.length - 1].toString ~ "]";
1113     }
1114 
1115 }