14
14
15
15
package com .google .api .client .util ;
16
16
17
+ import com .google .common .base .Strings ;
17
18
import java .io .Serializable ;
18
19
import java .util .Arrays ;
19
20
import java .util .Calendar ;
20
21
import java .util .Date ;
21
22
import java .util .GregorianCalendar ;
23
+ import java .util .Objects ;
22
24
import java .util .TimeZone ;
25
+ import java .util .concurrent .TimeUnit ;
23
26
import java .util .regex .Matcher ;
24
27
import java .util .regex .Pattern ;
25
28
@@ -39,12 +42,12 @@ public final class DateTime implements Serializable {
39
42
private static final TimeZone GMT = TimeZone .getTimeZone ("GMT" );
40
43
41
44
/** Regular expression for parsing RFC3339 date/times. */
42
- private static final Pattern RFC3339_PATTERN =
43
- Pattern . compile (
44
- "^( \\ d{4})- (\\ d{2})- (\\ d{2})" // yyyy-MM-dd
45
- + "([Tt]( \\ d{2}): (\\ d{2}):(\\ d{2})( \\ . \\ d+)?)?" // 'T' HH:mm:ss.milliseconds
46
- + "([Zz]|([+-])( \\ d{2}):( \\ d{2}))?" ); // 'Z' or time zone shift HH:mm following '+' or
47
- // '-'
45
+ private static final String RFC3339_REGEX =
46
+ "( \\ d{4})-( \\ d{2})-( \\ d{2})" // yyyy-MM-dd
47
+ + "([Tt]( \\ d{2}): (\\ d{2}): (\\ d{2})( \\ . \\ d{1,9})?)? " // 'T'HH:mm:ss.nanoseconds
48
+ + "([Zz]|([+-]) (\\ d{2}):(\\ d{2}))?" ; // 'Z' or time zone shift HH:mm following '+' or '-'
49
+
50
+ private static final Pattern RFC3339_PATTERN = Pattern . compile ( RFC3339_REGEX );
48
51
49
52
/**
50
53
* Date/time value expressed as the number of ms since the Unix epoch.
@@ -260,6 +263,8 @@ public int hashCode() {
260
263
* NumberFormatException}. Also, in accordance with the RFC3339 standard, any number of
261
264
* milliseconds digits is now allowed.
262
265
*
266
+ * <p>Any time information beyond millisecond precision is truncated.
267
+ *
263
268
* <p>For the date-only case, the time zone is ignored and the hourOfDay, minute, second, and
264
269
* millisecond parameters are set to zero.
265
270
*
@@ -269,6 +274,98 @@ public int hashCode() {
269
274
* time zone shift but no time.
270
275
*/
271
276
public static DateTime parseRfc3339 (String str ) throws NumberFormatException {
277
+ return parseRfc3339WithNanoSeconds (str ).toDateTime ();
278
+ }
279
+
280
+ /**
281
+ * Parses an RFC3339 timestamp to a pair of seconds and nanoseconds since Unix Epoch.
282
+ *
283
+ * @param str Date/time string in RFC3339 format
284
+ * @throws IllegalArgumentException if {@code str} doesn't match the RFC3339 standard format; an
285
+ * exception is thrown if {@code str} doesn't match {@code RFC3339_REGEX} or if it contains a
286
+ * time zone shift but no time.
287
+ */
288
+ public static SecondsAndNanos parseRfc3339ToSecondsAndNanos (String str )
289
+ throws IllegalArgumentException {
290
+ return parseRfc3339WithNanoSeconds (str ).toSecondsAndNanos ();
291
+ }
292
+
293
+ /** A timestamp represented as the number of seconds and nanoseconds since Epoch. */
294
+ public static final class SecondsAndNanos implements Serializable {
295
+ private final long seconds ;
296
+ private final int nanos ;
297
+
298
+ public static SecondsAndNanos ofSecondsAndNanos (long seconds , int nanos ) {
299
+ return new SecondsAndNanos (seconds , nanos );
300
+ }
301
+
302
+ private SecondsAndNanos (long seconds , int nanos ) {
303
+ this .seconds = seconds ;
304
+ this .nanos = nanos ;
305
+ }
306
+
307
+ public long getSeconds () {
308
+ return seconds ;
309
+ }
310
+
311
+ public int getNanos () {
312
+ return nanos ;
313
+ }
314
+
315
+ @ Override
316
+ public boolean equals (Object o ) {
317
+ if (this == o ) {
318
+ return true ;
319
+ }
320
+ if (o == null || getClass () != o .getClass ()) {
321
+ return false ;
322
+ }
323
+ SecondsAndNanos that = (SecondsAndNanos ) o ;
324
+ return seconds == that .seconds && nanos == that .nanos ;
325
+ }
326
+
327
+ @ Override
328
+ public int hashCode () {
329
+ return Objects .hash (seconds , nanos );
330
+ }
331
+
332
+ @ Override
333
+ public String toString () {
334
+ return String .format ("Seconds: %d, Nanos: %d" , seconds , nanos );
335
+ }
336
+ }
337
+
338
+ /** Result of parsing a Rfc3339 string. */
339
+ private static class Rfc3339ParseResult implements Serializable {
340
+ private final long seconds ;
341
+ private final int nanos ;
342
+ private final boolean timeGiven ;
343
+ private final Integer tzShift ;
344
+
345
+ private Rfc3339ParseResult (long seconds , int nanos , boolean timeGiven , Integer tzShift ) {
346
+ this .seconds = seconds ;
347
+ this .nanos = nanos ;
348
+ this .timeGiven = timeGiven ;
349
+ this .tzShift = tzShift ;
350
+ }
351
+
352
+ /**
353
+ * Convert this {@link Rfc3339ParseResult} to a {@link DateTime} with millisecond precision. Any
354
+ * fraction of a millisecond will be truncated.
355
+ */
356
+ private DateTime toDateTime () {
357
+ long seconds = TimeUnit .SECONDS .toMillis (this .seconds );
358
+ long nanos = TimeUnit .NANOSECONDS .toMillis (this .nanos );
359
+ return new DateTime (!timeGiven , seconds + nanos , tzShift );
360
+ }
361
+
362
+ private SecondsAndNanos toSecondsAndNanos () {
363
+ return new SecondsAndNanos (seconds , nanos );
364
+ }
365
+ }
366
+
367
+ private static Rfc3339ParseResult parseRfc3339WithNanoSeconds (String str )
368
+ throws NumberFormatException {
272
369
Matcher matcher = RFC3339_PATTERN .matcher (str );
273
370
if (!matcher .matches ()) {
274
371
throw new NumberFormatException ("Invalid date/time format: " + str );
@@ -283,7 +380,7 @@ public static DateTime parseRfc3339(String str) throws NumberFormatException {
283
380
int hourOfDay = 0 ;
284
381
int minute = 0 ;
285
382
int second = 0 ;
286
- int milliseconds = 0 ;
383
+ int nanoseconds = 0 ;
287
384
Integer tzShiftInteger = null ;
288
385
289
386
if (isTzShiftGiven && !isTimeGiven ) {
@@ -297,34 +394,32 @@ public static DateTime parseRfc3339(String str) throws NumberFormatException {
297
394
hourOfDay = Integer .parseInt (matcher .group (5 )); // HH
298
395
minute = Integer .parseInt (matcher .group (6 )); // mm
299
396
second = Integer .parseInt (matcher .group (7 )); // ss
300
- if (matcher .group (8 ) != null ) { // contains .milliseconds?
301
- milliseconds = Integer .parseInt (matcher .group (8 ).substring (1 )); // milliseconds
302
- // The number of digits after the dot may not be 3. Need to renormalize.
303
- int fractionDigits = matcher .group (8 ).substring (1 ).length () - 3 ;
304
- milliseconds = (int ) ((float ) milliseconds / Math .pow (10 , fractionDigits ));
397
+ if (matcher .group (8 ) != null ) { // contains .nanoseconds?
398
+ String fraction = Strings .padEnd (matcher .group (8 ).substring (1 ), 9 , '0' );
399
+ nanoseconds = Integer .parseInt (fraction );
305
400
}
306
401
}
307
402
Calendar dateTime = new GregorianCalendar (GMT );
308
403
dateTime .set (year , month , day , hourOfDay , minute , second );
309
- dateTime .set (Calendar .MILLISECOND , milliseconds );
310
404
long value = dateTime .getTimeInMillis ();
311
405
312
406
if (isTimeGiven && isTzShiftGiven ) {
313
- int tzShift ;
314
- if (Character .toUpperCase (tzShiftRegexGroup .charAt (0 )) == 'Z' ) {
315
- tzShift = 0 ;
316
- } else {
317
- tzShift =
407
+ if (Character .toUpperCase (tzShiftRegexGroup .charAt (0 )) != 'Z' ) {
408
+ int tzShift =
318
409
Integer .parseInt (matcher .group (11 )) * 60 // time zone shift HH
319
410
+ Integer .parseInt (matcher .group (12 )); // time zone shift mm
320
411
if (matcher .group (10 ).charAt (0 ) == '-' ) { // time zone shift + or -
321
412
tzShift = -tzShift ;
322
413
}
323
414
value -= tzShift * 60000L ; // e.g. if 1 hour ahead of UTC, subtract an hour to get UTC time
415
+ tzShiftInteger = tzShift ;
416
+ } else {
417
+ tzShiftInteger = 0 ;
324
418
}
325
- tzShiftInteger = tzShift ;
326
419
}
327
- return new DateTime (!isTimeGiven , value , tzShiftInteger );
420
+ // convert to seconds and nanoseconds
421
+ long secondsSinceEpoch = value / 1000L ;
422
+ return new Rfc3339ParseResult (secondsSinceEpoch , nanoseconds , isTimeGiven , tzShiftInteger );
328
423
}
329
424
330
425
/** Appends a zero-padded number to a string builder. */
0 commit comments