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.ZoneId;
13 
14 import hunt.stream.DataOutput;
15 import hunt.Exceptions;
16 
17 import std.conv;
18 import hunt.stream.Common;
19 // import hunt.time.format.DateTimeFormatterBuilder;
20 import hunt.time.format.TextStyle;
21 import hunt.time.temporal.TemporalAccessor;
22 import hunt.time.temporal.TemporalField;
23 import hunt.time.temporal.TemporalQueries;
24 import hunt.time.temporal.TemporalQuery;
25 import hunt.time.temporal.ValueRange;
26 import hunt.time.temporal.ChronoField;
27 import hunt.time.Exceptions;
28 import hunt.time.zone.ZoneRules;
29 import hunt.time.zone.ZoneRulesException;
30 // import hunt.time.zone.ZoneRulesProvider;
31 import hunt.collection.HashSet;
32 // import hunt.time.util.Locale;
33 import hunt.collection;
34 import hunt.time.ZoneOffset;
35 import hunt.time.Ser;
36 import hunt.util.StringBuilder;
37 // import hunt.time.ZoneRegion;
38 import std.algorithm.searching;
39 import hunt.text.Common;
40 import hunt.time.Exceptions;
41 import hunt.Assert;
42 import hunt.time.Instant;
43 import hunt.time.util.QueryHelper;
44 import hunt.time.util.Common;
45 
46 import hunt.util.Common;
47 // import hunt.serialization.JsonSerializer;
48 
49 import std.concurrency : initOnce;
50 
51 /**
52  * A time-zone ID, such as {@code Europe/Paris}.
53  * !(p)
54  * A {@code ZoneId} is used to identify the rules used to convert between
55  * an {@link Instant} and a {@link LocalDateTime}.
56  * There are two distinct types of ID:
57  * !(ul)
58  * !(li)Fixed offsets - a fully resolved offset from UTC/Greenwich, that uses
59  *  the same offset for all local date-times
60  * !(li)Geographical regions - an area where a specific set of rules for finding
61  *  the offset from UTC/Greenwich apply
62  * </ul>
63  * Most fixed offsets are represented by {@link ZoneOffset}.
64  * Calling {@link #normalized()} on any {@code ZoneId} will ensure that a
65  * fixed offset ID will be represented as a {@code ZoneOffset}.
66  * !(p)
67  * The actual rules, describing when and how the offset changes, are defined by {@link ZoneRules}.
68  * This class is simply an ID used to obtain the underlying rules.
69  * This approach is taken because rules are defined by governments and change
70  * frequently, whereas the ID is stable.
71  * !(p)
72  * The distinction has other effects. Serializing the {@code ZoneId} will only send
73  * the ID, whereas serializing the rules sends the entire data set.
74  * Similarly, a comparison of two IDs only examines the ID, whereas
75  * a comparison of two rules examines the entire data set.
76  *
77  * !(h3)Time-zone IDs</h3>
78  * The ID is unique within the system.
79  * There are three types of ID.
80  * !(p)
81  * The simplest type of ID is that from {@code ZoneOffset}.
82  * This consists of 'Z' and IDs starting with '+' or '-'.
83  * !(p)
84  * The next type of ID are offset-style IDs with some form of prefix,
85  * such as 'GMT+2' or 'UTC+01:00'.
86  * The recognised prefixes are 'UTC', 'GMT' and 'UT'.
87  * The offset is the suffix and will be normalized during creation.
88  * These IDs can be normalized to a {@code ZoneOffset} using {@code normalized()}.
89  * !(p)
90  * The third type of ID are region-based IDs. A region-based ID must be of
91  * two or more characters, and not start with 'UTC', 'GMT', 'UT' '+' or '-'.
92  * Region-based IDs are defined by configuration, see {@link ZoneRulesProvider}.
93  * The configuration focuses on providing the lookup from the ID to the
94  * underlying {@code ZoneRules}.
95  * !(p)
96  * Time-zone rules are defined by governments and change frequently.
97  * There are a number of organizations, known here as groups, that monitor
98  * time-zone changes and collate them.
99  * The default group is the IANA Time Zone Database (TZDB).
100  * Other organizations include IATA (the airline industry body) and Microsoft.
101  * !(p)
102  * Each group defines its own format for the region ID it provides.
103  * The TZDB group defines IDs such as 'Europe/London' or 'America/New_York'.
104  * TZDB IDs take precedence over other groups.
105  * !(p)
106  * It is strongly recommended that the group name is included _in all IDs supplied by
107  * groups other than TZDB to avoid conflicts. For example, IATA airline time-zone
108  * region IDs are typically the same as the three letter airport code.
109  * However, the airport of Utrecht has the code 'UTC', which is obviously a conflict.
110  * The recommended format for region IDs from groups other than TZDB is 'group~region'.
111  * Thus if IATA data were defined, Utrecht airport would be 'IATA~UTC'.
112  *
113  * !(h3)Serialization</h3>
114  * This class can be serialized and stores the string zone ID _in the external form.
115  * The {@code ZoneOffset} subclass uses a dedicated format that only stores the
116  * offset from UTC/Greenwich.
117  * !(p)
118  * A {@code ZoneId} can be deserialized _in a Java Runtime where the ID is unknown.
119  * For example, if a server-side Java Runtime has been updated with a new zone ID, but
120  * the client-side Java Runtime has not been updated. In this case, the {@code ZoneId}
121  * object will exist, and can be queried using {@code getId}, {@code equals},
122  * {@code hashCode}, {@code toString}, {@code getDisplayName} and {@code normalized}.
123  * However, any call to {@code getRules} will fail with {@code ZoneRulesException}.
124  * This approach is designed to allow a {@link ZonedDateTime} to be loaded and
125  * queried, but not modified, on a Java Runtime with incomplete time-zone information.
126  *
127  * !(p)
128  * This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
129  * class; use of identity-sensitive operations (including reference equality
130  * ({@code ==}), identity hash code, or synchronization) on instances of
131  * {@code ZoneId} may have unpredictable results and should be avoided.
132  * The {@code equals} method should be used for comparisons.
133  *
134  * @implSpec
135  * This abstract class has two implementations, both of which are immutable and thread-safe.
136  * One implementation models region-based IDs, the other is {@code ZoneOffset} modelling
137  * offset-based IDs. This difference is visible _in serialization.
138  *
139  * @since 1.8
140  */
141 abstract class ZoneId : Serializable {
142 
143     /**
144      * A map of zone overrides to enable the short time-zone names to be used.
145      * !(p)
146      * Use of short zone IDs has been deprecated _in {@code java.util.TimeZone}.
147      * This map allows the IDs to continue to be used via the
148      * {@link #of(string, Map)} factory method.
149      * !(p)
150      * This map contains a mapping of the IDs that is _in line with TZDB 2005r and
151      * later, where 'EST', 'MST' and 'HST' map to IDs which do not include daylight
152      * savings.
153      * !(p)
154      * This maps as follows:
155      * !(ul)
156      * !(li)EST - -05:00</li>
157      * !(li)HST - -10:00</li>
158      * !(li)MST - -07:00</li>
159      * !(li)ACT - Australia/Darwin</li>
160      * !(li)AET - Australia/Sydney</li>
161      * !(li)AGT - America/Argentina/Buenos_Aires</li>
162      * !(li)ART - Africa/Cairo</li>
163      * !(li)AST - America/Anchorage</li>
164      * !(li)BET - America/Sao_Paulo</li>
165      * !(li)BST - Asia/Dhaka</li>
166      * !(li)CAT - Africa/Harare</li>
167      * !(li)CNT - America/St_Johns</li>
168      * !(li)CST - America/Chicago</li>
169      * !(li)CTT - Asia/Shanghai</li>
170      * !(li)EAT - Africa/Addis_Ababa</li>
171      * !(li)ECT - Europe/Paris</li>
172      * !(li)IET - America/Indiana/Indianapolis</li>
173      * !(li)IST - Asia/Kolkata</li>
174      * !(li)JST - Asia/Tokyo</li>
175      * !(li)MIT - Pacific/Apia</li>
176      * !(li)NET - Asia/Yerevan</li>
177      * !(li)NST - Pacific/Auckland</li>
178      * !(li)PLT - Asia/Karachi</li>
179      * !(li)PNT - America/Phoenix</li>
180      * !(li)PRT - America/Puerto_Rico</li>
181      * !(li)PST - America/Los_Angeles</li>
182      * !(li)SST - Pacific/Guadalcanal</li>
183      * !(li)VST - Asia/Ho_Chi_Minh</li>
184      * </ul>
185      * The map is unmodifiable.
186      */
187     
188     
189 
190     static Map!(string, string) SHORT_IDS()
191     {
192         __gshared Map!(string, string) inst;
193         
194         return initOnce!inst({
195             HashMap!(string, string) _SHORT_IDS = new HashMap!(string, string);
196             _SHORT_IDS.put("ACT", "Australia/Darwin");
197             _SHORT_IDS.put("AET", "Australia/Sydney");
198             _SHORT_IDS.put("AGT", "America/Argentina/Buenos_Aires");
199             _SHORT_IDS.put("ART", "Africa/Cairo");
200             _SHORT_IDS.put("AST", "America/Anchorage");
201             _SHORT_IDS.put("BET", "America/Sao_Paulo");
202             _SHORT_IDS.put("BST", "Asia/Dhaka");
203             _SHORT_IDS.put("CAT", "Africa/Harare");
204             _SHORT_IDS.put("CNT", "America/St_Johns");
205             _SHORT_IDS.put("CST", "America/Chicago");
206             _SHORT_IDS.put("CTT", "Asia/Shanghai");
207             _SHORT_IDS.put("EAT", "Africa/Addis_Ababa");
208             _SHORT_IDS.put("ECT", "Europe/Paris");
209             _SHORT_IDS.put("IET", "America/Indiana/Indianapolis");
210             _SHORT_IDS.put("IST", "Asia/Kolkata");
211             _SHORT_IDS.put("JST", "Asia/Tokyo");
212             _SHORT_IDS.put("MIT", "Pacific/Apia");
213             _SHORT_IDS.put("NET", "Asia/Yerevan");
214             _SHORT_IDS.put("NST", "Pacific/Auckland");
215             _SHORT_IDS.put("PLT", "Asia/Karachi");
216             _SHORT_IDS.put("PNT", "America/Phoenix");
217             _SHORT_IDS.put("PRT", "America/Puerto_Rico");
218             _SHORT_IDS.put("PST", "America/Los_Angeles");
219             _SHORT_IDS.put("SST", "Pacific/Guadalcanal");
220             _SHORT_IDS.put("VST", "Asia/Ho_Chi_Minh");
221             _SHORT_IDS.put("EST", "-05:00");
222             _SHORT_IDS.put("MST", "-07:00");
223             _SHORT_IDS.put("HST", "-10:00");
224             return _SHORT_IDS;
225         }());
226     }
227 
228     //-----------------------------------------------------------------------
229  
230     deprecated("Using ZoneRegion.systemDefault instead.")
231     static ZoneId systemDefault() {
232         throw new Exception("Using ZoneRegion.systemDefault instead.");
233     }
234 
235     /**
236      * Gets the set of available zone IDs.
237      * !(p)
238      * This set includes the string form of all available region-based IDs.
239      * Offset-based zone IDs are not included _in the returned set.
240      * The ID can be passed to {@link #of(string)} to create a {@code ZoneId}.
241      * !(p)
242      * The set of zone IDs can increase over time, although _in a typical application
243      * the set of IDs is fixed. Each call to this method is thread-safe.
244      *
245      * @return a modifiable copy of the set of zone IDs, not null
246      */
247     // static Set!(string) getAvailableZoneIds() {
248     //     return new HashSet!(string)(ZoneRulesProvider.getAvailableZoneIds());
249     // }
250 
251     
252     deprecated("Using ZoneRegion.of instead.")
253     static ZoneId of(string zoneId, Map!(string, string) aliasMap) {
254         throw new Exception("Using ZoneRegion.of instead.");
255     }
256 
257    
258     deprecated("Using ZoneRegion.of instead.")
259     static ZoneId of(string zoneId) {
260         throw new Exception("Using ZoneRegion.of instead.");
261     }
262 
263     /**
264      * Obtains an instance of {@code ZoneId} wrapping an offset.
265      * !(p)
266      * If the prefix is "GMT", "UTC", or "UT" a {@code ZoneId}
267      * with the prefix and the non-zero offset is returned.
268      * If the prefix is empty {@code ""} the {@code ZoneOffset} is returned.
269      *
270      * @param prefix  the time-zone ID, not null
271      * @param offset  the offset, not null
272      * @return the zone ID, not null
273      * @throws IllegalArgumentException if the prefix is not one of
274      *     "GMT", "UTC", or "UT", or ""
275      */
276 
277     deprecated("Using ZoneRegion.ofOffset instead.")
278     static ZoneId ofOffset(string prefix, ZoneOffset offset) {
279         throw new Exception("Using ZoneRegion.ofOffset instead.");
280         // assert(prefix, "prefix");
281         // assert(offset, "offset");
282         // if (prefix.length == 0) {
283         //     return offset;
284         // }
285 
286         // if (!(prefix == "GMT") && !(prefix == "UTC") && !(prefix == "UT")) {
287         //      throw new IllegalArgumentException("prefix should be GMT, UTC or UT, is: " ~ prefix);
288         // }
289 
290         // if (offset.getTotalSeconds() != 0) {
291         //     prefix = prefix ~ (offset.getId());
292         // }
293         // return new ZoneRegion(prefix, offset.getRules());
294     }
295 
296     /**
297      * Parses the ID, taking a flag to indicate whether {@code ZoneRulesException}
298      * should be thrown or not, used _in deserialization.
299      *
300      * @param zoneId  the time-zone ID, not null
301      * @param checkAvailable  whether to check if the zone ID is available
302      * @return the zone ID, not null
303      * @throws DateTimeException if the ID format is invalid
304      * @throws ZoneRulesException if checking availability and the ID cannot be found
305      */
306     deprecated("Using ZoneRegion.of instead.")
307     static ZoneId of(string zoneId, bool checkAvailable) {
308         throw new Exception("Using ZoneRegion.of instead.");
309         // assert(zoneId, "zoneId");
310         // if (zoneId.length <= 1 || zoneId.startsWith("+") || zoneId.startsWith("-")) {
311         //     return ZoneOffset.of(zoneId);
312         // } else if (zoneId.startsWith("UTC") || zoneId.startsWith("GMT")) {
313         //     return ofWithPrefix(zoneId, 3, checkAvailable);
314         // } else if (zoneId.startsWith("UT")) {
315         //     return ofWithPrefix(zoneId, 2, checkAvailable);
316         // }
317         // return ZoneRegion.ofId(zoneId, checkAvailable);
318     }
319 
320     /**
321      * Parse once a prefix is established.
322      *
323      * @param zoneId  the time-zone ID, not null
324      * @param prefixLength  the length of the prefix, 2 or 3
325      * @return the zone ID, not null
326      * @throws DateTimeException if the zone ID has an invalid format
327      */
328     // private static ZoneId ofWithPrefix(string zoneId, int prefixLength, bool checkAvailable) {
329     //     string prefix = zoneId.substring(0, prefixLength);
330     //     if (zoneId.length == prefixLength) {
331     //         return ofOffset(prefix, ZoneOffset.UTC);
332     //     }
333     //     if (zoneId[prefixLength] != '+' && zoneId[prefixLength] != '-') {
334     //         return ZoneRegion.ofId(zoneId, checkAvailable);  // drop through to ZoneRulesProvider
335     //     }
336     //     try {
337     //         ZoneOffset offset = ZoneOffset.of(zoneId.substring(prefixLength));
338     //         if (offset == ZoneOffset.UTC) {
339     //             return ofOffset(prefix, offset);
340     //         }
341     //         return ofOffset(prefix, offset);
342     //     } catch (DateTimeException ex) {
343     //         throw new DateTimeException("Invalid ID for offset-based ZoneId: " ~ zoneId, ex);
344     //     }
345     // }
346 
347     //-----------------------------------------------------------------------
348     /**
349      * Obtains an instance of {@code ZoneId} from a temporal object.
350      * !(p)
351      * This obtains a zone based on the specified temporal.
352      * A {@code TemporalAccessor} represents an arbitrary set of date and time information,
353      * which this factory converts to an instance of {@code ZoneId}.
354      * !(p)
355      * A {@code TemporalAccessor} represents some form of date and time information.
356      * This factory converts the arbitrary temporal object to an instance of {@code ZoneId}.
357      * !(p)
358      * The conversion will try to obtain the zone _in a way that favours region-based
359      * zones over offset-based zones using {@link TemporalQueries#zone()}.
360      * !(p)
361      * This method matches the signature of the functional interface {@link TemporalQuery}
362      * allowing it to be used as a query via method reference, {@code ZoneId::from}.
363      *
364      * @param temporal  the temporal object to convert, not null
365      * @return the zone ID, not null
366      * @throws DateTimeException if unable to convert to a {@code ZoneId}
367      */
368     static ZoneId from(TemporalAccessor temporal) {
369         ZoneId obj =QueryHelper.query!ZoneId(temporal,TemporalQueries.zone());
370         if (obj is null) {
371             throw new DateTimeException("Unable to obtain ZoneId from TemporalAccessor: " ~
372                     typeid(temporal).name ~ " of type " ~ typeid(temporal).stringof);
373         }
374         return obj;
375     }
376 
377     //-----------------------------------------------------------------------
378     /**
379      * Constructor only accessible within the package.
380      */
381     this() {
382         // if (typeid(this).stringof != ZoneOffset.stringof && typeof(this).stringof != ZoneRegion.stringof) {
383             // throw new AssertionError("Invalid subclass");
384         // }
385     }
386 
387     //-----------------------------------------------------------------------
388     /**
389      * Gets the unique time-zone ID.
390      * !(p)
391      * This ID uniquely defines this object.
392      * The format of an offset based ID is defined by {@link ZoneOffset#getId()}.
393      *
394      * @return the time-zone unique ID, not null
395      */
396     abstract string getId();
397 
398     //-----------------------------------------------------------------------
399     /**
400      * Gets the textual representation of the zone, such as 'British Time' or
401      * '+02:00'.
402      * !(p)
403      * This returns the textual name used to identify the time-zone ID,
404      * suitable for presentation to the user.
405      * The parameters control the style of the returned text and the locale.
406      * !(p)
407      * If no textual mapping is found then the {@link #getId() full ID} is returned.
408      *
409      * @param style  the length of the text required, not null
410      * @param locale  the locale to use, not null
411      * @return the text value of the zone, not null
412      */
413     // string getDisplayName(TextStyle style, Locale locale) {
414     //     return new DateTimeFormatterBuilder().appendZoneText(style).toFormatter(locale).format(toTemporal());
415     // }
416 
417     /**
418      * Converts this zone to a {@code TemporalAccessor}.
419      * !(p)
420      * A {@code ZoneId} can be fully represented as a {@code TemporalAccessor}.
421      * However, the interface is not implemented by this class as most of the
422      * methods on the interface have no meaning to {@code ZoneId}.
423      * !(p)
424      * The returned temporal has no supported fields, with the query method
425      * supporting the return of the zone using {@link TemporalQueries#zoneId()}.
426      *
427      * @return a temporal equivalent to this zone, not null
428      */
429     private TemporalAccessor toTemporal() {
430         return new AnonymousClass3();
431     }
432 
433     //-----------------------------------------------------------------------
434     /**
435      * Gets the time-zone rules for this ID allowing calculations to be performed.
436      * !(p)
437      * The rules provide the functionality associated with a time-zone,
438      * such as finding the offset for a given instant or local date-time.
439      * !(p)
440      * A time-zone can be invalid if it is deserialized _in a Java Runtime which
441      * does not have the same rules loaded as the Java Runtime that stored it.
442      * In this case, calling this method will throw a {@code ZoneRulesException}.
443      * !(p)
444      * The rules are supplied by {@link ZoneRulesProvider}. An advanced provider may
445      * support dynamic updates to the rules without restarting the Java Runtime.
446      * If so, then the result of this method may change over time.
447      * Each individual call will be still remain thread-safe.
448      * !(p)
449      * {@link ZoneOffset} will always return a set of rules where the offset never changes.
450      *
451      * @return the rules, not null
452      * @throws ZoneRulesException if no rules are available for this ID
453      */
454     abstract ZoneRules getRules();
455 
456     /**
457      * Normalizes the time-zone ID, returning a {@code ZoneOffset} where possible.
458      * !(p)
459      * The returns a normalized {@code ZoneId} that can be used _in place of this ID.
460      * The result will have {@code ZoneRules} equivalent to those returned by this object,
461      * however the ID returned by {@code getId()} may be different.
462      * !(p)
463      * The normalization checks if the rules of this {@code ZoneId} have a fixed offset.
464      * If they do, then the {@code ZoneOffset} equal to that offset is returned.
465      * Otherwise {@code this} is returned.
466      *
467      * @return the time-zone unique ID, not null
468      */
469     ZoneId normalized() {
470         try {
471             ZoneRules rules = getRules();
472             if (rules.isFixedOffset()) {
473                 return rules.getOffset(Instant.EPOCH);
474             }
475         } catch (ZoneRulesException ex) {
476             // invalid ZoneRegion is not important to this method
477         }
478         return this;
479     }
480 
481     //-----------------------------------------------------------------------
482     /**
483      * Checks if this time-zone ID is equal to another time-zone ID.
484      * !(p)
485      * The comparison is based on the ID.
486      *
487      * @param obj  the object to check, null returns false
488      * @return true if this is equal to the other time-zone ID
489      */
490     override
491     bool opEquals(Object obj) {
492         if (this is obj) {
493            return true;
494         }
495         if (cast(ZoneId)(obj) !is null) {
496             ZoneId other = cast(ZoneId) obj;
497             return getId() == (other.getId());
498         }
499         return false;
500     }
501 
502     /**
503      * A hash code for this time-zone ID.
504      *
505      * @return a suitable hash code
506      */
507     override
508     size_t toHash() @trusted nothrow {
509         try
510         {
511             return hashOf(getId());
512         }
513         catch(Exception e){}
514         return int.init;
515     }
516 
517     //-----------------------------------------------------------------------
518     /**
519      * Defend against malicious streams.
520      *
521      * @param s the stream to read
522      * @throws InvalidObjectException always
523      */
524      ///@gxc
525     // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {
526     //     throw new InvalidObjectException("Deserialization via serialization delegate");
527     // }
528 
529     /**
530      * Outputs this zone as a {@code string}, using the ID.
531      *
532      * @return a string representation of this time-zone ID, not null
533      */
534     override
535     string toString() {
536         return getId();
537     }
538 
539     //-----------------------------------------------------------------------
540     /**
541      * Writes the object using a
542      * <a href="{@docRoot}/serialized-form.html#hunt.time.Ser">dedicated serialized form</a>.
543      * @serialData
544      * !(pre)
545      *  _out.writeByte(7);  // identifies a ZoneId (not ZoneOffset)
546      *  _out.writeUTF(getId());
547      * </pre>
548      * !(p)
549      * When read back _in, the {@code ZoneId} will be created as though using
550      * {@link #of(string)}, but without any exception _in the case where the
551      * ID has a valid format, but is not _in the known set of region-based IDs.
552      *
553      * @return the instance of {@code Ser}, not null
554      */
555     // this is here for serialization Javadoc
556     // private Object writeReplace() {
557     //     return new Ser(Ser.ZONE_REGION_TYPE, this);
558     // }
559 
560     // abstract void write(DataOutput _out) /*throws IOException*/;
561     
562     // mixin SerializationMember!(typeof(this));
563 
564 }