1 // Protocol Buffers - Google's data interchange format 2 // Copyright 2008 Google Inc. All rights reserved. 3 // https://developers.google.com/protocol-buffers/ 4 // 5 // Redistribution and use in source and binary forms, with or without 6 // modification, are permitted provided that the following conditions are 7 // met: 8 // 9 // * Redistributions of source code must retain the above copyright 10 // notice, this list of conditions and the following disclaimer. 11 // * Redistributions in binary form must reproduce the above 12 // copyright notice, this list of conditions and the following disclaimer 13 // in the documentation and/or other materials provided with the 14 // distribution. 15 // * Neither the name of Google Inc. nor the names of its 16 // contributors may be used to endorse or promote products derived from 17 // this software without specific prior written permission. 18 // 19 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 31 package com.google.protobuf.util; 32 33 import static com.google.common.math.IntMath.checkedAdd; 34 import static com.google.common.math.IntMath.checkedSubtract; 35 import static com.google.common.math.LongMath.checkedAdd; 36 import static com.google.common.math.LongMath.checkedMultiply; 37 import static com.google.common.math.LongMath.checkedSubtract; 38 39 import com.google.errorprone.annotations.CanIgnoreReturnValue; 40 import com.google.protobuf.Duration; 41 import com.google.protobuf.Timestamp; 42 import java.text.ParseException; 43 import java.text.SimpleDateFormat; 44 import java.util.Comparator; 45 import java.util.Date; 46 import java.util.GregorianCalendar; 47 import java.util.Locale; 48 import java.util.TimeZone; 49 50 /** 51 * Utilities to help create/manipulate {@code protobuf/timestamp.proto}. All operations throw an 52 * {@link IllegalArgumentException} if the input(s) are not {@linkplain #isValid(Timestamp) valid}. 53 */ 54 public final class Timestamps { 55 56 // Timestamp for "0001-01-01T00:00:00Z" 57 static final long TIMESTAMP_SECONDS_MIN = -62135596800L; 58 59 // Timestamp for "9999-12-31T23:59:59Z" 60 static final long TIMESTAMP_SECONDS_MAX = 253402300799L; 61 62 static final long NANOS_PER_SECOND = 1000000000; 63 static final long NANOS_PER_MILLISECOND = 1000000; 64 static final long NANOS_PER_MICROSECOND = 1000; 65 static final long MILLIS_PER_SECOND = 1000; 66 static final long MICROS_PER_SECOND = 1000000; 67 68 /** A constant holding the minimum valid {@link Timestamp}, {@code 0001-01-01T00:00:00Z}. */ 69 public static final Timestamp MIN_VALUE = 70 Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MIN).setNanos(0).build(); 71 72 /** 73 * A constant holding the maximum valid {@link Timestamp}, {@code 9999-12-31T23:59:59.999999999Z}. 74 */ 75 public static final Timestamp MAX_VALUE = 76 Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MAX).setNanos(999999999).build(); 77 78 /** 79 * A constant holding the {@link Timestamp} of epoch time, {@code 1970-01-01T00:00:00.000000000Z}. 80 */ 81 public static final Timestamp EPOCH = Timestamp.newBuilder().setSeconds(0).setNanos(0).build(); 82 83 private static final ThreadLocal<SimpleDateFormat> timestampFormat = 84 new ThreadLocal<SimpleDateFormat>() { 85 @Override 86 protected SimpleDateFormat initialValue() { 87 return createTimestampFormat(); 88 } 89 }; 90 createTimestampFormat()91 private static SimpleDateFormat createTimestampFormat() { 92 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); 93 GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 94 // We use Proleptic Gregorian Calendar (i.e., Gregorian calendar extends 95 // backwards to year one) for timestamp formating. 96 calendar.setGregorianChange(new Date(Long.MIN_VALUE)); 97 sdf.setCalendar(calendar); 98 return sdf; 99 } 100 Timestamps()101 private Timestamps() {} 102 103 private static final Comparator<Timestamp> COMPARATOR = 104 new Comparator<Timestamp>() { 105 @Override 106 public int compare(Timestamp t1, Timestamp t2) { 107 checkValid(t1); 108 checkValid(t2); 109 int secDiff = Long.compare(t1.getSeconds(), t2.getSeconds()); 110 return (secDiff != 0) ? secDiff : Integer.compare(t1.getNanos(), t2.getNanos()); 111 } 112 }; 113 114 /** 115 * Returns a {@link Comparator} for {@link Timestamp Timestamps} which sorts in increasing 116 * chronological order. Nulls and invalid {@link Timestamp Timestamps} are not allowed (see 117 * {@link #isValid}). 118 */ comparator()119 public static Comparator<Timestamp> comparator() { 120 return COMPARATOR; 121 } 122 123 /** 124 * Compares two timestamps. The value returned is identical to what would be returned by: {@code 125 * Timestamps.comparator().compare(x, y)}. 126 * 127 * @return the value {@code 0} if {@code x == y}; a value less than {@code 0} if {@code x < y}; 128 * and a value greater than {@code 0} if {@code x > y} 129 */ compare(Timestamp x, Timestamp y)130 public static int compare(Timestamp x, Timestamp y) { 131 return COMPARATOR.compare(x, y); 132 } 133 134 /** 135 * Returns true if the given {@link Timestamp} is valid. The {@code seconds} value must be in the 136 * range [-62,135,596,800, +253,402,300,799] (i.e., between 0001-01-01T00:00:00Z and 137 * 9999-12-31T23:59:59Z). The {@code nanos} value must be in the range [0, +999,999,999]. 138 * 139 * <p><b>Note:</b> Negative second values with fractional seconds must still have non-negative 140 * nanos values that count forward in time. 141 */ isValid(Timestamp timestamp)142 public static boolean isValid(Timestamp timestamp) { 143 return isValid(timestamp.getSeconds(), timestamp.getNanos()); 144 } 145 146 /** 147 * Returns true if the given number of seconds and nanos is a valid {@link Timestamp}. The {@code 148 * seconds} value must be in the range [-62,135,596,800, +253,402,300,799] (i.e., between 149 * 0001-01-01T00:00:00Z and 9999-12-31T23:59:59Z). The {@code nanos} value must be in the range 150 * [0, +999,999,999]. 151 * 152 * <p><b>Note:</b> Negative second values with fractional seconds must still have non-negative 153 * nanos values that count forward in time. 154 */ 155 @SuppressWarnings("GoodTime") // this is a legacy conversion API isValid(long seconds, int nanos)156 public static boolean isValid(long seconds, int nanos) { 157 if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) { 158 return false; 159 } 160 if (nanos < 0 || nanos >= NANOS_PER_SECOND) { 161 return false; 162 } 163 return true; 164 } 165 166 /** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */ 167 @CanIgnoreReturnValue checkValid(Timestamp timestamp)168 public static Timestamp checkValid(Timestamp timestamp) { 169 long seconds = timestamp.getSeconds(); 170 int nanos = timestamp.getNanos(); 171 if (!isValid(seconds, nanos)) { 172 throw new IllegalArgumentException( 173 String.format( 174 "Timestamp is not valid. See proto definition for valid values. " 175 + "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. " 176 + "Nanos (%s) must be in range [0, +999,999,999].", 177 seconds, nanos)); 178 } 179 return timestamp; 180 } 181 182 /** 183 * Builds the given builder and throws an {@link IllegalArgumentException} if it is not valid. See 184 * {@link #checkValid(Timestamp)}. 185 * 186 * @return A valid, built {@link Timestamp}. 187 */ checkValid(Timestamp.Builder timestampBuilder)188 public static Timestamp checkValid(Timestamp.Builder timestampBuilder) { 189 return checkValid(timestampBuilder.build()); 190 } 191 192 /** 193 * Convert Timestamp to RFC 3339 date string format. The output will always be Z-normalized and 194 * uses 3, 6 or 9 fractional digits as required to represent the exact value. Note that Timestamp 195 * can only represent time from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. See 196 * https://www.ietf.org/rfc/rfc3339.txt 197 * 198 * <p>Example of generated format: "1972-01-01T10:00:20.021Z" 199 * 200 * @return The string representation of the given timestamp. 201 * @throws IllegalArgumentException if the given timestamp is not in the valid range. 202 */ toString(Timestamp timestamp)203 public static String toString(Timestamp timestamp) { 204 checkValid(timestamp); 205 206 long seconds = timestamp.getSeconds(); 207 int nanos = timestamp.getNanos(); 208 209 StringBuilder result = new StringBuilder(); 210 // Format the seconds part. 211 Date date = new Date(seconds * MILLIS_PER_SECOND); 212 result.append(timestampFormat.get().format(date)); 213 // Format the nanos part. 214 if (nanos != 0) { 215 result.append("."); 216 result.append(formatNanos(nanos)); 217 } 218 result.append("Z"); 219 return result.toString(); 220 } 221 222 /** 223 * Parse from RFC 3339 date string to Timestamp. This method accepts all outputs of {@link 224 * #toString(Timestamp)} and it also accepts any fractional digits (or none) and any offset as 225 * long as they fit into nano-seconds precision. 226 * 227 * <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00" 228 * 229 * @return A Timestamp parsed from the string. 230 * @throws ParseException if parsing fails. 231 */ parse(String value)232 public static Timestamp parse(String value) throws ParseException { 233 int dayOffset = value.indexOf('T'); 234 if (dayOffset == -1) { 235 throw new ParseException("Failed to parse timestamp: invalid timestamp \"" + value + "\"", 0); 236 } 237 int timezoneOffsetPosition = value.indexOf('Z', dayOffset); 238 if (timezoneOffsetPosition == -1) { 239 timezoneOffsetPosition = value.indexOf('+', dayOffset); 240 } 241 if (timezoneOffsetPosition == -1) { 242 timezoneOffsetPosition = value.indexOf('-', dayOffset); 243 } 244 if (timezoneOffsetPosition == -1) { 245 throw new ParseException("Failed to parse timestamp: missing valid timezone offset.", 0); 246 } 247 // Parse seconds and nanos. 248 String timeValue = value.substring(0, timezoneOffsetPosition); 249 String secondValue = timeValue; 250 String nanoValue = ""; 251 int pointPosition = timeValue.indexOf('.'); 252 if (pointPosition != -1) { 253 secondValue = timeValue.substring(0, pointPosition); 254 nanoValue = timeValue.substring(pointPosition + 1); 255 } 256 Date date = timestampFormat.get().parse(secondValue); 257 long seconds = date.getTime() / MILLIS_PER_SECOND; 258 int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); 259 // Parse timezone offsets. 260 if (value.charAt(timezoneOffsetPosition) == 'Z') { 261 if (value.length() != timezoneOffsetPosition + 1) { 262 throw new ParseException( 263 "Failed to parse timestamp: invalid trailing data \"" 264 + value.substring(timezoneOffsetPosition) 265 + "\"", 266 0); 267 } 268 } else { 269 String offsetValue = value.substring(timezoneOffsetPosition + 1); 270 long offset = parseTimezoneOffset(offsetValue); 271 if (value.charAt(timezoneOffsetPosition) == '+') { 272 seconds -= offset; 273 } else { 274 seconds += offset; 275 } 276 } 277 try { 278 return normalizedTimestamp(seconds, nanos); 279 } catch (IllegalArgumentException e) { 280 throw new ParseException("Failed to parse timestamp: timestamp is out of range.", 0); 281 } 282 } 283 284 /** Create a Timestamp from the number of seconds elapsed from the epoch. */ 285 @SuppressWarnings("GoodTime") // this is a legacy conversion API fromSeconds(long seconds)286 public static Timestamp fromSeconds(long seconds) { 287 return normalizedTimestamp(seconds, 0); 288 } 289 290 /** 291 * Convert a Timestamp to the number of seconds elapsed from the epoch. 292 * 293 * <p>The result will be rounded down to the nearest second. E.g., if the timestamp represents 294 * "1969-12-31T23:59:59.999999999Z", it will be rounded to -1 second. 295 */ 296 @SuppressWarnings("GoodTime") // this is a legacy conversion API toSeconds(Timestamp timestamp)297 public static long toSeconds(Timestamp timestamp) { 298 return checkValid(timestamp).getSeconds(); 299 } 300 301 /** Create a Timestamp from the number of milliseconds elapsed from the epoch. */ 302 @SuppressWarnings("GoodTime") // this is a legacy conversion API fromMillis(long milliseconds)303 public static Timestamp fromMillis(long milliseconds) { 304 return normalizedTimestamp( 305 milliseconds / MILLIS_PER_SECOND, 306 (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND)); 307 } 308 309 /** 310 * Convert a Timestamp to the number of milliseconds elapsed from the epoch. 311 * 312 * <p>The result will be rounded down to the nearest millisecond. E.g., if the timestamp 313 * represents "1969-12-31T23:59:59.999999999Z", it will be rounded to -1 millisecond. 314 */ 315 @SuppressWarnings("GoodTime") // this is a legacy conversion API toMillis(Timestamp timestamp)316 public static long toMillis(Timestamp timestamp) { 317 checkValid(timestamp); 318 return checkedAdd( 319 checkedMultiply(timestamp.getSeconds(), MILLIS_PER_SECOND), 320 timestamp.getNanos() / NANOS_PER_MILLISECOND); 321 } 322 323 /** Create a Timestamp from the number of microseconds elapsed from the epoch. */ 324 @SuppressWarnings("GoodTime") // this is a legacy conversion API fromMicros(long microseconds)325 public static Timestamp fromMicros(long microseconds) { 326 return normalizedTimestamp( 327 microseconds / MICROS_PER_SECOND, 328 (int) (microseconds % MICROS_PER_SECOND * NANOS_PER_MICROSECOND)); 329 } 330 331 /** 332 * Convert a Timestamp to the number of microseconds elapsed from the epoch. 333 * 334 * <p>The result will be rounded down to the nearest microsecond. E.g., if the timestamp 335 * represents "1969-12-31T23:59:59.999999999Z", it will be rounded to -1 microsecond. 336 */ 337 @SuppressWarnings("GoodTime") // this is a legacy conversion API toMicros(Timestamp timestamp)338 public static long toMicros(Timestamp timestamp) { 339 checkValid(timestamp); 340 return checkedAdd( 341 checkedMultiply(timestamp.getSeconds(), MICROS_PER_SECOND), 342 timestamp.getNanos() / NANOS_PER_MICROSECOND); 343 } 344 345 /** Create a Timestamp from the number of nanoseconds elapsed from the epoch. */ 346 @SuppressWarnings("GoodTime") // this is a legacy conversion API fromNanos(long nanoseconds)347 public static Timestamp fromNanos(long nanoseconds) { 348 return normalizedTimestamp( 349 nanoseconds / NANOS_PER_SECOND, (int) (nanoseconds % NANOS_PER_SECOND)); 350 } 351 352 /** Convert a Timestamp to the number of nanoseconds elapsed from the epoch. */ 353 @SuppressWarnings("GoodTime") // this is a legacy conversion API toNanos(Timestamp timestamp)354 public static long toNanos(Timestamp timestamp) { 355 checkValid(timestamp); 356 return checkedAdd( 357 checkedMultiply(timestamp.getSeconds(), NANOS_PER_SECOND), timestamp.getNanos()); 358 } 359 360 /** Calculate the difference between two timestamps. */ between(Timestamp from, Timestamp to)361 public static Duration between(Timestamp from, Timestamp to) { 362 checkValid(from); 363 checkValid(to); 364 return Durations.normalizedDuration( 365 checkedSubtract(to.getSeconds(), from.getSeconds()), 366 checkedSubtract(to.getNanos(), from.getNanos())); 367 } 368 369 /** Add a duration to a timestamp. */ add(Timestamp start, Duration length)370 public static Timestamp add(Timestamp start, Duration length) { 371 checkValid(start); 372 Durations.checkValid(length); 373 return normalizedTimestamp( 374 checkedAdd(start.getSeconds(), length.getSeconds()), 375 checkedAdd(start.getNanos(), length.getNanos())); 376 } 377 378 /** Subtract a duration from a timestamp. */ subtract(Timestamp start, Duration length)379 public static Timestamp subtract(Timestamp start, Duration length) { 380 checkValid(start); 381 Durations.checkValid(length); 382 return normalizedTimestamp( 383 checkedSubtract(start.getSeconds(), length.getSeconds()), 384 checkedSubtract(start.getNanos(), length.getNanos())); 385 } 386 normalizedTimestamp(long seconds, int nanos)387 static Timestamp normalizedTimestamp(long seconds, int nanos) { 388 if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { 389 seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); 390 nanos = (int) (nanos % NANOS_PER_SECOND); 391 } 392 if (nanos < 0) { 393 nanos = 394 (int) 395 (nanos + NANOS_PER_SECOND); // no overflow since nanos is negative (and we're adding) 396 seconds = checkedSubtract(seconds, 1); 397 } 398 Timestamp timestamp = Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build(); 399 return checkValid(timestamp); 400 } 401 parseTimezoneOffset(String value)402 private static long parseTimezoneOffset(String value) throws ParseException { 403 int pos = value.indexOf(':'); 404 if (pos == -1) { 405 throw new ParseException("Invalid offset value: " + value, 0); 406 } 407 String hours = value.substring(0, pos); 408 String minutes = value.substring(pos + 1); 409 return (Long.parseLong(hours) * 60 + Long.parseLong(minutes)) * 60; 410 } 411 parseNanos(String value)412 static int parseNanos(String value) throws ParseException { 413 int result = 0; 414 for (int i = 0; i < 9; ++i) { 415 result = result * 10; 416 if (i < value.length()) { 417 if (value.charAt(i) < '0' || value.charAt(i) > '9') { 418 throw new ParseException("Invalid nanoseconds.", 0); 419 } 420 result += value.charAt(i) - '0'; 421 } 422 } 423 return result; 424 } 425 426 /** Format the nano part of a timestamp or a duration. */ formatNanos(int nanos)427 static String formatNanos(int nanos) { 428 // Determine whether to use 3, 6, or 9 digits for the nano part. 429 if (nanos % NANOS_PER_MILLISECOND == 0) { 430 return String.format(Locale.ENGLISH, "%1$03d", nanos / NANOS_PER_MILLISECOND); 431 } else if (nanos % NANOS_PER_MICROSECOND == 0) { 432 return String.format(Locale.ENGLISH, "%1$06d", nanos / NANOS_PER_MICROSECOND); 433 } else { 434 return String.format(Locale.ENGLISH, "%1$09d", nanos); 435 } 436 } 437 } 438