1 /* 2 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 /* 27 * This file is available under and governed by the GNU General Public 28 * License version 2 only, as published by the Free Software Foundation. 29 * However, the following notice accompanied the original version of this 30 * file: 31 * 32 * Copyright (c) 2007-2012, Stephen Colebourne & Michael Nascimento Santos 33 * 34 * All rights reserved. 35 * 36 * Redistribution and use in source and binary forms, with or without 37 * modification, are permitted provided that the following conditions are met: 38 * 39 * * Redistributions of source code must retain the above copyright notice, 40 * this list of conditions and the following disclaimer. 41 * 42 * * Redistributions in binary form must reproduce the above copyright notice, 43 * this list of conditions and the following disclaimer in the documentation 44 * and/or other materials provided with the distribution. 45 * 46 * * Neither the name of JSR-310 nor the names of its contributors 47 * may be used to endorse or promote products derived from this software 48 * without specific prior written permission. 49 * 50 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 51 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 52 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 53 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 54 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 55 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 56 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 57 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 58 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 59 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 60 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 61 */ 62 package build.tools.tzdb; 63 64 import java.util.Objects; 65 import java.util.concurrent.ConcurrentHashMap; 66 import java.util.concurrent.ConcurrentMap; 67 68 /** 69 * A time-zone offset from Greenwich/UTC, such as {@code +02:00}. 70 * <p> 71 * A time-zone offset is the period of time that a time-zone differs from Greenwich/UTC. 72 * This is usually a fixed number of hours and minutes. 73 * 74 * @since 1.8 75 */ 76 final class ZoneOffset implements Comparable<ZoneOffset> { 77 78 /** Cache of time-zone offset by offset in seconds. */ 79 private static final ConcurrentMap<Integer, ZoneOffset> SECONDS_CACHE = new ConcurrentHashMap<>(16, 0.75f, 4); 80 /** Cache of time-zone offset by ID. */ 81 private static final ConcurrentMap<String, ZoneOffset> ID_CACHE = new ConcurrentHashMap<>(16, 0.75f, 4); 82 83 /** 84 * The number of seconds per hour. 85 */ 86 private static final int SECONDS_PER_HOUR = 60 * 60; 87 /** 88 * The number of seconds per minute. 89 */ 90 private static final int SECONDS_PER_MINUTE = 60; 91 /** 92 * The number of minutes per hour. 93 */ 94 private static final int MINUTES_PER_HOUR = 60; 95 /** 96 * The abs maximum seconds. 97 */ 98 private static final int MAX_SECONDS = 18 * SECONDS_PER_HOUR; 99 /** 100 * Serialization version. 101 */ 102 private static final long serialVersionUID = 2357656521762053153L; 103 104 /** 105 * The time-zone offset for UTC, with an ID of 'Z'. 106 */ 107 public static final ZoneOffset UTC = ZoneOffset.ofTotalSeconds(0); 108 /** 109 * Constant for the maximum supported offset. 110 */ 111 public static final ZoneOffset MIN = ZoneOffset.ofTotalSeconds(-MAX_SECONDS); 112 /** 113 * Constant for the maximum supported offset. 114 */ 115 public static final ZoneOffset MAX = ZoneOffset.ofTotalSeconds(MAX_SECONDS); 116 117 /** 118 * The total offset in seconds. 119 */ 120 private final int totalSeconds; 121 /** 122 * The string form of the time-zone offset. 123 */ 124 private final transient String id; 125 126 //----------------------------------------------------------------------- 127 /** 128 * Obtains an instance of {@code ZoneOffset} using the ID. 129 * <p> 130 * This method parses the string ID of a {@code ZoneOffset} to 131 * return an instance. The parsing accepts all the formats generated by 132 * {@link #getId()}, plus some additional formats: 133 * <p><ul> 134 * <li>{@code Z} - for UTC 135 * <li>{@code +h} 136 * <li>{@code +hh} 137 * <li>{@code +hh:mm} 138 * <li>{@code -hh:mm} 139 * <li>{@code +hhmm} 140 * <li>{@code -hhmm} 141 * <li>{@code +hh:mm:ss} 142 * <li>{@code -hh:mm:ss} 143 * <li>{@code +hhmmss} 144 * <li>{@code -hhmmss} 145 * </ul><p> 146 * Note that ± means either the plus or minus symbol. 147 * <p> 148 * The ID of the returned offset will be normalized to one of the formats 149 * described by {@link #getId()}. 150 * <p> 151 * The maximum supported range is from +18:00 to -18:00 inclusive. 152 * 153 * @param offsetId the offset ID, not null 154 * @return the zone-offset, not null 155 * @throws DateTimeException if the offset ID is invalid 156 */ 157 @SuppressWarnings("fallthrough") 158 public static ZoneOffset of(String offsetId) { 159 Objects.requireNonNull(offsetId, "offsetId"); 160 // "Z" is always in the cache 161 ZoneOffset offset = ID_CACHE.get(offsetId); 162 if (offset != null) { 163 return offset; 164 } 165 166 // parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss 167 final int hours, minutes, seconds; 168 switch (offsetId.length()) { 169 case 2: 170 offsetId = offsetId.charAt(0) + "0" + offsetId.charAt(1); // fallthru 171 case 3: 172 hours = parseNumber(offsetId, 1, false); 173 minutes = 0; 174 seconds = 0; 175 break; 176 case 5: 177 hours = parseNumber(offsetId, 1, false); 178 minutes = parseNumber(offsetId, 3, false); 179 seconds = 0; 180 break; 181 case 6: 182 hours = parseNumber(offsetId, 1, false); 183 minutes = parseNumber(offsetId, 4, true); 184 seconds = 0; 185 break; 186 case 7: 187 hours = parseNumber(offsetId, 1, false); 188 minutes = parseNumber(offsetId, 3, false); 189 seconds = parseNumber(offsetId, 5, false); 190 break; 191 case 9: 192 hours = parseNumber(offsetId, 1, false); 193 minutes = parseNumber(offsetId, 4, true); 194 seconds = parseNumber(offsetId, 7, true); 195 break; 196 default: 197 throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid"); 198 } 199 char first = offsetId.charAt(0); 200 if (first != '+' && first != '-') { 201 throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Plus/minus not found when expected"); 202 } 203 if (first == '-') { 204 return ofHoursMinutesSeconds(-hours, -minutes, -seconds); 205 } else { 206 return ofHoursMinutesSeconds(hours, minutes, seconds); 207 } 208 } 209 210 /** 211 * Parse a two digit zero-prefixed number. 212 * 213 * @param offsetId the offset ID, not null 214 * @param pos the position to parse, valid 215 * @param precededByColon should this number be prefixed by a precededByColon 216 * @return the parsed number, from 0 to 99 217 */ 218 private static int parseNumber(CharSequence offsetId, int pos, boolean precededByColon) { 219 if (precededByColon && offsetId.charAt(pos - 1) != ':') { 220 throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Colon not found when expected"); 221 } 222 char ch1 = offsetId.charAt(pos); 223 char ch2 = offsetId.charAt(pos + 1); 224 if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') { 225 throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Non numeric characters found"); 226 } 227 return (ch1 - 48) * 10 + (ch2 - 48); 228 } 229 230 //----------------------------------------------------------------------- 231 /** 232 * Obtains an instance of {@code ZoneOffset} using an offset in hours. 233 * 234 * @param hours the time-zone offset in hours, from -18 to +18 235 * @return the zone-offset, not null 236 * @throws DateTimeException if the offset is not in the required range 237 */ 238 public static ZoneOffset ofHours(int hours) { 239 return ofHoursMinutesSeconds(hours, 0, 0); 240 } 241 242 /** 243 * Obtains an instance of {@code ZoneOffset} using an offset in 244 * hours and minutes. 245 * <p> 246 * The sign of the hours and minutes components must match. 247 * Thus, if the hours is negative, the minutes must be negative or zero. 248 * If the hours is zero, the minutes may be positive, negative or zero. 249 * 250 * @param hours the time-zone offset in hours, from -18 to +18 251 * @param minutes the time-zone offset in minutes, from 0 to ±59, sign matches hours 252 * @return the zone-offset, not null 253 * @throws DateTimeException if the offset is not in the required range 254 */ 255 public static ZoneOffset ofHoursMinutes(int hours, int minutes) { 256 return ofHoursMinutesSeconds(hours, minutes, 0); 257 } 258 259 /** 260 * Obtains an instance of {@code ZoneOffset} using an offset in 261 * hours, minutes and seconds. 262 * <p> 263 * The sign of the hours, minutes and seconds components must match. 264 * Thus, if the hours is negative, the minutes and seconds must be negative or zero. 265 * 266 * @param hours the time-zone offset in hours, from -18 to +18 267 * @param minutes the time-zone offset in minutes, from 0 to ±59, sign matches hours and seconds 268 * @param seconds the time-zone offset in seconds, from 0 to ±59, sign matches hours and minutes 269 * @return the zone-offset, not null 270 * @throws DateTimeException if the offset is not in the required range 271 */ 272 public static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds) { 273 validate(hours, minutes, seconds); 274 int totalSeconds = totalSeconds(hours, minutes, seconds); 275 return ofTotalSeconds(totalSeconds); 276 } 277 278 /** 279 * Validates the offset fields. 280 * 281 * @param hours the time-zone offset in hours, from -18 to +18 282 * @param minutes the time-zone offset in minutes, from 0 to ±59 283 * @param seconds the time-zone offset in seconds, from 0 to ±59 284 * @throws DateTimeException if the offset is not in the required range 285 */ 286 private static void validate(int hours, int minutes, int seconds) { 287 if (hours < -18 || hours > 18) { 288 throw new DateTimeException("Zone offset hours not in valid range: value " + hours + 289 " is not in the range -18 to 18"); 290 } 291 if (hours > 0) { 292 if (minutes < 0 || seconds < 0) { 293 throw new DateTimeException("Zone offset minutes and seconds must be positive because hours is positive"); 294 } 295 } else if (hours < 0) { 296 if (minutes > 0 || seconds > 0) { 297 throw new DateTimeException("Zone offset minutes and seconds must be negative because hours is negative"); 298 } 299 } else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0)) { 300 throw new DateTimeException("Zone offset minutes and seconds must have the same sign"); 301 } 302 if (Math.abs(minutes) > 59) { 303 throw new DateTimeException("Zone offset minutes not in valid range: abs(value) " + 304 Math.abs(minutes) + " is not in the range 0 to 59"); 305 } 306 if (Math.abs(seconds) > 59) { 307 throw new DateTimeException("Zone offset seconds not in valid range: abs(value) " + 308 Math.abs(seconds) + " is not in the range 0 to 59"); 309 } 310 if (Math.abs(hours) == 18 && (Math.abs(minutes) > 0 || Math.abs(seconds) > 0)) { 311 throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00"); 312 } 313 } 314 315 /** 316 * Calculates the total offset in seconds. 317 * 318 * @param hours the time-zone offset in hours, from -18 to +18 319 * @param minutes the time-zone offset in minutes, from 0 to ±59, sign matches hours and seconds 320 * @param seconds the time-zone offset in seconds, from 0 to ±59, sign matches hours and minutes 321 * @return the total in seconds 322 */ 323 private static int totalSeconds(int hours, int minutes, int seconds) { 324 return hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds; 325 } 326 327 //----------------------------------------------------------------------- 328 /** 329 * Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds 330 * <p> 331 * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800. 332 * 333 * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 334 * @return the ZoneOffset, not null 335 * @throws DateTimeException if the offset is not in the required range 336 */ 337 public static ZoneOffset ofTotalSeconds(int totalSeconds) { 338 if (Math.abs(totalSeconds) > MAX_SECONDS) { 339 throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00"); 340 } 341 if (totalSeconds % (15 * SECONDS_PER_MINUTE) == 0) { 342 Integer totalSecs = totalSeconds; 343 ZoneOffset result = SECONDS_CACHE.get(totalSecs); 344 if (result == null) { 345 result = new ZoneOffset(totalSeconds); 346 SECONDS_CACHE.putIfAbsent(totalSecs, result); 347 result = SECONDS_CACHE.get(totalSecs); 348 ID_CACHE.putIfAbsent(result.getId(), result); 349 } 350 return result; 351 } else { 352 return new ZoneOffset(totalSeconds); 353 } 354 } 355 356 /** 357 * Constructor. 358 * 359 * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 360 */ 361 private ZoneOffset(int totalSeconds) { 362 super(); 363 this.totalSeconds = totalSeconds; 364 id = buildId(totalSeconds); 365 } 366 367 private static String buildId(int totalSeconds) { 368 if (totalSeconds == 0) { 369 return "Z"; 370 } else { 371 int absTotalSeconds = Math.abs(totalSeconds); 372 StringBuilder buf = new StringBuilder(); 373 int absHours = absTotalSeconds / SECONDS_PER_HOUR; 374 int absMinutes = (absTotalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; 375 buf.append(totalSeconds < 0 ? "-" : "+") 376 .append(absHours < 10 ? "0" : "").append(absHours) 377 .append(absMinutes < 10 ? ":0" : ":").append(absMinutes); 378 int absSeconds = absTotalSeconds % SECONDS_PER_MINUTE; 379 if (absSeconds != 0) { 380 buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds); 381 } 382 return buf.toString(); 383 } 384 } 385 386 /** 387 * Gets the total zone offset in seconds. 388 * <p> 389 * This is the primary way to access the offset amount. 390 * It returns the total of the hours, minutes and seconds fields as a 391 * single offset that can be added to a time. 392 * 393 * @return the total zone offset amount in seconds 394 */ 395 public int getTotalSeconds() { 396 return totalSeconds; 397 } 398 399 /** 400 * Gets the normalized zone offset ID. 401 * <p> 402 * The ID is minor variation to the standard ISO-8601 formatted string 403 * for the offset. There are three formats: 404 * <p><ul> 405 * <li>{@code Z} - for UTC (ISO-8601) 406 * <li>{@code +hh:mm} or {@code -hh:mm} - if the seconds are zero (ISO-8601) 407 * <li>{@code +hh:mm:ss} or {@code -hh:mm:ss} - if the seconds are non-zero (not ISO-8601) 408 * </ul><p> 409 * 410 * @return the zone offset ID, not null 411 */ 412 public String getId() { 413 return id; 414 } 415 416 /** 417 * Compares this offset to another offset in descending order. 418 * <p> 419 * The offsets are compared in the order that they occur for the same time 420 * of day around the world. Thus, an offset of {@code +10:00} comes before an 421 * offset of {@code +09:00} and so on down to {@code -18:00}. 422 * <p> 423 * The comparison is "consistent with equals", as defined by {@link Comparable}. 424 * 425 * @param other the other date to compare to, not null 426 * @return the comparator value, negative if less, postive if greater 427 * @throws NullPointerException if {@code other} is null 428 */ 429 @Override 430 public int compareTo(ZoneOffset other) { 431 return other.totalSeconds - totalSeconds; 432 } 433 434 /** 435 * Checks if this offset is equal to another offset. 436 * <p> 437 * The comparison is based on the amount of the offset in seconds. 438 * This is equivalent to a comparison by ID. 439 * 440 * @param obj the object to check, null returns false 441 * @return true if this is equal to the other offset 442 */ 443 @Override 444 public boolean equals(Object obj) { 445 if (this == obj) { 446 return true; 447 } 448 if (obj instanceof ZoneOffset) { 449 return totalSeconds == ((ZoneOffset) obj).totalSeconds; 450 } 451 return false; 452 } 453 454 /** 455 * A hash code for this offset. 456 * 457 * @return a suitable hash code 458 */ 459 @Override 460 public int hashCode() { 461 return totalSeconds; 462 } 463 464 /** 465 * Outputs this offset as a {@code String}, using the normalized ID. 466 * 467 * @return a string representation of this offset, not null 468 */ 469 @Override 470 public String toString() { 471 return id; 472 } 473 474 }