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