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 }