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 modulehunt.time.ZoneRegion;
13 14 importhunt.collection.HashMap;
15 importhunt.collection.Map;
16 importhunt.Exceptions;
17 importhunt.stream.Common;
18 importhunt.stream.DataInput;
19 importhunt.stream.DataOutput;
20 21 importhunt.text.Common;
22 23 importhunt.time.Exceptions;
24 importhunt.time.zone.ZoneRules;
25 importhunt.time.zone.ZoneRulesException;
26 importhunt.time.zone.ZoneRulesProvider;
27 importhunt.time.ZoneId;
28 importhunt.time.ZoneOffset;
29 importhunt.time.Ser;
30 importhunt.time.util.Common;
31 32 importhunt.util.Common;
33 // import hunt.serialization.JsonSerializer;34 35 importstd.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 finalclassZoneRegion : ZoneId { // , Serializable58 59 /**
60 * The time-zone ID, not null.
61 */62 privatestringid;
63 /**
64 * The time-zone rules, null if zone ID was loaded leniently.
65 */66 privateZoneRulesrules;
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 privatestaticvoidcheckName(stringzoneId) {
75 auton = zoneId.length;
76 if (n < 2) {
77 thrownewDateTimeException("Invalid ID for region-based ZoneId, invalid format: " ~ zoneId);
78 }
79 for (inti = 0; i < n; i++) {
80 charc = 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 thrownewDateTimeException("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(stringid, ZoneRulesrules) {
102 this.id = id;
103 this.rules = rules;
104 }
105 106 //-----------------------------------------------------------------------107 override108 publicstringgetId() {
109 returnid;
110 }
111 112 override113 publicZoneRulesgetRules() {
114 // additional query for group provider when null allows for possibility115 // that the provider was updated after the ZoneId was created116 return (rules !isnull ? 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 privateObjectwriteReplace() {
132 returnnewSer(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 ///@gxc142 // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {143 // throw new InvalidObjectException("Deserialization via serialization delegate");144 // }145 146 // override147 // 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 publicstaticZoneIdsystemDefault() {
174 // return TimeZone.getDefault().toZoneId();175 importhunt.system.TimeZone;
176 returnZoneRegion.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 publicstaticZoneIdof(stringzoneId, Map!(string, string) aliasMap) {
197 assert(zoneId, "zoneId");
198 assert(aliasMap, "aliasMap");
199 stringid = aliasMap.get(zoneId) isnull ? aliasMap.get(zoneId) : zoneId;
200 returnof(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 publicstaticZoneIdof(stringzoneId) {
244 returnof(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 staticZoneIdof(stringzoneId, boolcheckAvailable) {
258 assert(zoneId, "zoneId");
259 if (zoneId.length <= 1 || zoneId.startsWith("+") || zoneId.startsWith("-")) {
260 returnZoneOffset.of(zoneId);
261 } elseif (zoneId.startsWith("UTC") || zoneId.startsWith("GMT")) {
262 returnofWithPrefix(zoneId, 3, checkAvailable);
263 } elseif (zoneId.startsWith("UT")) {
264 returnofWithPrefix(zoneId, 2, checkAvailable);
265 }
266 returnZoneRegion.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 staticZoneRegionofId(stringzoneId, boolcheckAvailable) {
279 assert(zoneId, "zoneId");
280 checkName(zoneId);
281 ZoneRulesrules = null;
282 try {
283 // always attempt load for better behavior after deserialization284 rules = ZoneRulesProvider.getRules(zoneId, true);
285 } catch (ZoneRulesExceptionex) {
286 if (checkAvailable) {
287 throwex;
288 }
289 }
290 returnnewZoneRegion(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 publicstaticZoneIdofOffset(stringprefix, ZoneOffsetoffset) {
307 assert(prefix, "prefix");
308 assert(offset, "offset");
309 if (prefix.length == 0) {
310 returnoffset;
311 }
312 313 if (!(prefix == "GMT") && !(prefix == "UTC") && !(prefix == "UT")) {
314 thrownewIllegalArgumentException("prefix should be GMT, UTC or UT, is: " ~ prefix);
315 }
316 317 if (offset.getTotalSeconds() != 0) {
318 prefix = prefix ~ (offset.getId());
319 }
320 returnnewZoneRegion(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 privatestaticZoneIdofWithPrefix(stringzoneId, intprefixLength, boolcheckAvailable) {
333 stringprefix = zoneId.substring(0, prefixLength);
334 if (zoneId.length == prefixLength) {
335 returnofOffset(prefix, ZoneOffset.UTC);
336 }
337 if (zoneId[prefixLength] != '+' && zoneId[prefixLength] != '-') {
338 returnZoneRegion.ofId(zoneId, checkAvailable); // drop through to ZoneRulesProvider339 }
340 try {
341 ZoneOffsetoffset = ZoneOffset.of(zoneId.substring(prefixLength));
342 if (offset == ZoneOffset.UTC) {
343 returnofOffset(prefix, offset);
344 }
345 returnofOffset(prefix, offset);
346 } catch (DateTimeExceptionex) {
347 thrownewDateTimeException("Invalid ID for offset-based ZoneId: " ~ zoneId, ex);
348 }
349 }
350 351 // mixin SerializationMember!(typeof(this));352 }