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.ZoneRegion;
13 
14 import hunt.collection.HashMap;
15 import hunt.collection.Map;
16 import hunt.Exceptions;
17 import hunt.stream.Common;
18 import hunt.stream.DataInput;
19 import hunt.stream.DataOutput;
20 
21 import hunt.text.Common;
22 
23 import hunt.time.Exceptions;
24 import hunt.time.zone.ZoneRules;
25 import hunt.time.zone.ZoneRulesException;
26 import hunt.time.zone.ZoneRulesProvider;
27 import hunt.time.ZoneId;
28 import hunt.time.ZoneOffset;
29 import hunt.time.Ser;
30 import hunt.time.util.Common;
31 
32 import hunt.util.Common;
33 // import hunt.serialization.JsonSerializer;
34 
35 import std.string;
36 
37 /**
38  * A geographical region where the same time-zone rules apply.
39  * !(p)
40  * Time-zone information is categorized as a set of rules defining when and
41  * how the offset from UTC/Greenwich changes. These rules are accessed using
42  * identifiers based on geographical regions, such as countries or states.
43  * The most common region classification is the Time Zone Database (TZDB),
44  * which defines regions such as 'Europe/Paris' and 'Asia/Tokyo'.
45  * !(p)
46  * The region identifier, modeled by this class, is distinct from the
47  * underlying rules, modeled by {@link ZoneRules}.
48  * The rules are defined by governments and change frequently.
49  * By contrast, the region identifier is well-defined and long-lived.
50  * This separation also allows rules to be shared between regions if appropriate.
51  *
52  * @implSpec
53  * This class is immutable and thread-safe.
54  *
55  * @since 1.8
56  */
57 final class ZoneRegion : ZoneId { //  , Serializable
58 
59     /**
60      * The time-zone ID, not null.
61      */
62     private  string id;
63     /**
64      * The time-zone rules, null if zone ID was loaded leniently.
65      */
66     private  ZoneRules rules;
67 
68     /**
69      * Checks that the given string is a legal ZondId name.
70      *
71      * @param zoneId  the time-zone ID, not null
72      * @throws DateTimeException if the ID format is invalid
73      */
74     private static void checkName(string zoneId) {
75         auto n = zoneId.length;
76         if (n < 2) {
77            throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " ~ zoneId);
78         }
79         for (int i = 0; i < n; i++) {
80             char c = zoneId[i];
81             if (c >= 'a' && c <= 'z') continue;
82             if (c >= 'A' && c <= 'Z') continue;
83             if (c == '/' && i != 0) continue;
84             if (c >= '0' && c <= '9' && i != 0) continue;
85             if (c == '~' && i != 0) continue;
86             if (c == '.' && i != 0) continue;
87             if (c == '_' && i != 0) continue;
88             if (c == '+' && i != 0) continue;
89             if (c == '-' && i != 0) continue;
90             throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " ~ zoneId);
91         }
92     }
93 
94     //-------------------------------------------------------------------------
95     /**
96      * Constructor.
97      *
98      * @param id  the time-zone ID, not null
99      * @param rules  the rules, null for lazy lookup
100      */
101     this(string id, ZoneRules rules) {
102         this.id = id;
103         this.rules = rules;
104     }
105 
106     //-----------------------------------------------------------------------
107     override
108     public string getId() {
109         return id;
110     }
111 
112     override
113     public ZoneRules getRules() {
114         // additional query for group provider when null allows for possibility
115         // that the provider was updated after the ZoneId was created
116         return (rules !is null ? rules : ZoneRulesProvider.getRules(id, false));
117     }
118 
119     //-----------------------------------------------------------------------
120     /**
121      * Writes the object using a
122      * <a href="{@docRoot}/serialized-form.html#hunt.time.Ser">dedicated serialized form</a>.
123      * @serialData
124      * !(pre)
125      *  _out.writeByte(7);  // identifies a ZoneId (not ZoneOffset)
126      *  _out.writeUTF(zoneId);
127      * </pre>
128      *
129      * @return the instance of {@code Ser}, not null
130      */
131     private Object writeReplace() {
132         return new Ser(Ser.ZONE_REGION_TYPE, this);
133     }
134 
135     /**
136      * Defend against malicious streams.
137      *
138      * @param s the stream to read
139      * @throws InvalidObjectException always
140      */
141      ///@gxc
142     // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {
143     //     throw new InvalidObjectException("Deserialization via serialization delegate");
144     // }
145 
146     // override
147     // void write(DataOutput _out) /*throws IOException*/ {
148     //     _out.writeByte(Ser.ZONE_REGION_TYPE);
149     //     writeExternal(_out);
150     // }
151 
152     // void writeExternal(DataOutput _out) /*throws IOException*/ {
153     //     _out.writeUTF(id);
154     // }
155 
156     // static ZoneId readExternal(DataInput _in) /*throws IOException*/ {
157     //     string id = _in.readUTF();
158     //     return ZoneId.of(id, false);
159     // }
160 
161     
162     /**
163      * Gets the system default time-zone.
164      * !(p)
165      * This queries {@link TimeZone#getDefault()} to find the default time-zone
166      * and converts it to a {@code ZoneId}. If the system default time-zone is changed,
167      * then the result of this method will also change.
168      *
169      * @return the zone ID, not null
170      * @throws DateTimeException if the converted zone ID has an invalid format
171      * @throws ZoneRulesException if the converted zone region ID cannot be found
172      */
173     public static ZoneId systemDefault() {
174         // return TimeZone.getDefault().toZoneId();
175         import hunt.system.TimeZone;
176         return ZoneRegion.of(getSystemTimeZoneId(),true);
177     }
178 
179     //-----------------------------------------------------------------------
180     /**
181      * Obtains an instance of {@code ZoneId} using its ID using a map
182      * of aliases to supplement the standard zone IDs.
183      * !(p)
184      * Many users of time-zones use short abbreviations, such as PST for
185      * 'Pacific Standard Time' and PDT for 'Pacific Daylight Time'.
186      * These abbreviations are not unique, and so cannot be used as IDs.
187      * This method allows a map of string to time-zone to be setup and reused
188      * within an application.
189      *
190      * @param zoneId  the time-zone ID, not null
191      * @param aliasMap  a map of alias zone IDs (typically abbreviations) to real zone IDs, not null
192      * @return the zone ID, not null
193      * @throws DateTimeException if the zone ID has an invalid format
194      * @throws ZoneRulesException if the zone ID is a region ID that cannot be found
195      */
196     public static ZoneId of(string zoneId, Map!(string, string) aliasMap) {
197         assert(zoneId, "zoneId");
198         assert(aliasMap, "aliasMap");
199         string id = aliasMap.get(zoneId) is null ? aliasMap.get(zoneId) : zoneId;
200         return of(id);
201     }
202 
203     /**
204      * Obtains an instance of {@code ZoneId} from an ID ensuring that the
205      * ID is valid and available for use.
206      * !(p)
207      * This method parses the ID producing a {@code ZoneId} or {@code ZoneOffset}.
208      * A {@code ZoneOffset} is returned if the ID is 'Z', or starts with '+' or '-'.
209      * The result will always be a valid ID for which {@link ZoneRules} can be obtained.
210      * !(p)
211      * Parsing matches the zone ID step by step as follows.
212      * !(ul)
213      * !(li)If the zone ID equals 'Z', the result is {@code ZoneOffset.UTC}.
214      * !(li)If the zone ID consists of a single letter, the zone ID is invalid
215      *  and {@code DateTimeException} is thrown.
216      * !(li)If the zone ID starts with '+' or '-', the ID is parsed as a
217      *  {@code ZoneOffset} using {@link ZoneOffset#of(string)}.
218      * !(li)If the zone ID equals 'GMT', 'UTC' or 'UT' then the result is a {@code ZoneId}
219      *  with the same ID and rules equivalent to {@code ZoneOffset.UTC}.
220      * !(li)If the zone ID starts with 'UTC+', 'UTC-', 'GMT+', 'GMT-', 'UT+' or 'UT-'
221      *  then the ID is a prefixed offset-based ID. The ID is split _in two, with
222      *  a two or three letter prefix and a suffix starting with the sign.
223      *  The suffix is parsed as a {@link ZoneOffset#of(string) ZoneOffset}.
224      *  The result will be a {@code ZoneId} with the specified UTC/GMT/UT prefix
225      *  and the normalized offset ID as per {@link ZoneOffset#getId()}.
226      *  The rules of the returned {@code ZoneId} will be equivalent to the
227      *  parsed {@code ZoneOffset}.
228      * !(li)All other IDs are parsed as region-based zone IDs. Region IDs must
229      *  match the regular expression !(code)[A-Za-z][A-Za-z0-9~/._+-]+</code>
230      *  otherwise a {@code DateTimeException} is thrown. If the zone ID is not
231      *  _in the configured set of IDs, {@code ZoneRulesException} is thrown.
232      *  The detailed format of the region ID depends on the group supplying the data.
233      *  The default set of data is supplied by the IANA Time Zone Database (TZDB).
234      *  This has region IDs of the form '{area}/{city}', such as 'Europe/Paris' or 'America/New_York'.
235      *  This is compatible with most IDs from {@link java.util.TimeZone}.
236      * </ul>
237      *
238      * @param zoneId  the time-zone ID, not null
239      * @return the zone ID, not null
240      * @throws DateTimeException if the zone ID has an invalid format
241      * @throws ZoneRulesException if the zone ID is a region ID that cannot be found
242      */
243     public static ZoneId of(string zoneId) {
244         return of(zoneId, true);
245     }
246 
247     /**
248      * Parses the ID, taking a flag to indicate whether {@code ZoneRulesException}
249      * should be thrown or not, used _in deserialization.
250      *
251      * @param zoneId  the time-zone ID, not null
252      * @param checkAvailable  whether to check if the zone ID is available
253      * @return the zone ID, not null
254      * @throws DateTimeException if the ID format is invalid
255      * @throws ZoneRulesException if checking availability and the ID cannot be found
256      */
257     static ZoneId of(string zoneId, bool checkAvailable) {
258         assert(zoneId, "zoneId");
259         if (zoneId.length <= 1 || zoneId.startsWith("+") || zoneId.startsWith("-")) {
260             return ZoneOffset.of(zoneId);
261         } else if (zoneId.startsWith("UTC") || zoneId.startsWith("GMT")) {
262             return ofWithPrefix(zoneId, 3, checkAvailable);
263         } else if (zoneId.startsWith("UT")) {
264             return ofWithPrefix(zoneId, 2, checkAvailable);
265         }
266         return ZoneRegion.ofId(zoneId, checkAvailable);
267     }
268 
269     /**
270      * Obtains an instance of {@code ZoneId} from an identifier.
271      *
272      * @param zoneId  the time-zone ID, not null
273      * @param checkAvailable  whether to check if the zone ID is available
274      * @return the zone ID, not null
275      * @throws DateTimeException if the ID format is invalid
276      * @throws ZoneRulesException if checking availability and the ID cannot be found
277      */
278     static ZoneRegion ofId(string zoneId, bool checkAvailable) {
279         assert(zoneId, "zoneId");
280         checkName(zoneId);
281         ZoneRules rules = null;
282         try {
283             // always attempt load for better behavior after deserialization
284             rules = ZoneRulesProvider.getRules(zoneId, true);
285         } catch (ZoneRulesException ex) {
286             if (checkAvailable) {
287                 throw ex;
288             }
289         }
290         return new ZoneRegion(zoneId, rules);
291     }
292 
293     /**
294      * Obtains an instance of {@code ZoneId} wrapping an offset.
295      * !(p)
296      * If the prefix is "GMT", "UTC", or "UT" a {@code ZoneId}
297      * with the prefix and the non-zero offset is returned.
298      * If the prefix is empty {@code ""} the {@code ZoneOffset} is returned.
299      *
300      * @param prefix  the time-zone ID, not null
301      * @param offset  the offset, not null
302      * @return the zone ID, not null
303      * @throws IllegalArgumentException if the prefix is not one of
304      *     "GMT", "UTC", or "UT", or ""
305      */
306     public static ZoneId ofOffset(string prefix, ZoneOffset offset) {
307         assert(prefix, "prefix");
308         assert(offset, "offset");
309         if (prefix.length == 0) {
310             return offset;
311         }
312 
313         if (!(prefix == "GMT") && !(prefix == "UTC") && !(prefix == "UT")) {
314              throw new IllegalArgumentException("prefix should be GMT, UTC or UT, is: " ~ prefix);
315         }
316 
317         if (offset.getTotalSeconds() != 0) {
318             prefix = prefix ~ (offset.getId());
319         }
320         return new ZoneRegion(prefix, offset.getRules());
321     }
322 
323     
324     /**
325      * Parse once a prefix is established.
326      *
327      * @param zoneId  the time-zone ID, not null
328      * @param prefixLength  the length of the prefix, 2 or 3
329      * @return the zone ID, not null
330      * @throws DateTimeException if the zone ID has an invalid format
331      */
332     private static ZoneId ofWithPrefix(string zoneId, int prefixLength, bool checkAvailable) {
333         string prefix = zoneId.substring(0, prefixLength);
334         if (zoneId.length == prefixLength) {
335             return ofOffset(prefix, ZoneOffset.UTC);
336         }
337         if (zoneId[prefixLength] != '+' && zoneId[prefixLength] != '-') {
338             return ZoneRegion.ofId(zoneId, checkAvailable);  // drop through to ZoneRulesProvider
339         }
340         try {
341             ZoneOffset offset = ZoneOffset.of(zoneId.substring(prefixLength));
342             if (offset == ZoneOffset.UTC) {
343                 return ofOffset(prefix, offset);
344             }
345             return ofOffset(prefix, offset);
346         } catch (DateTimeException ex) {
347             throw new DateTimeException("Invalid ID for offset-based ZoneId: " ~ zoneId, ex);
348         }
349     }
350 
351         // mixin SerializationMember!(typeof(this));
352 }