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 }