20
20
import java .util .Date ;
21
21
import java .util .GregorianCalendar ;
22
22
import java .util .TimeZone ;
23
+ import java .util .regex .Matcher ;
24
+ import java .util .regex .Pattern ;
23
25
24
26
/**
25
27
* Immutable representation of a date with an optional time and an optional time zone based on <a
@@ -38,6 +40,12 @@ public final class DateTime implements Serializable {
38
40
39
41
private static final TimeZone GMT = TimeZone .getTimeZone ("GMT" );
40
42
43
+ /** Regular expression for parsing RFC3339 date/times. */
44
+ private static final Pattern RFC3339_PATTERN = Pattern .compile (
45
+ "^(\\ d{4})-(\\ d{2})-(\\ d{2})" // yyyy-MM-dd
46
+ + "([Tt](\\ d{2}):(\\ d{2}):(\\ d{2})(\\ .\\ d+)?)?" // 'T'HH:mm:ss.milliseconds
47
+ + "([Zz]|([+-])(\\ d{2}):(\\ d{2}))?" ); // 'Z' or time zone shift HH:mm following '+' or '-'
48
+
41
49
/**
42
50
* Date/time value expressed as the number of ms since the Unix epoch.
43
51
*
@@ -123,12 +131,22 @@ public DateTime(boolean dateOnly, long value, Integer tzShift) {
123
131
* Instantiates {@link DateTime} from an <a href='http://tools.ietf.org/html/rfc3339'>RFC 3339</a>
124
132
* date/time value.
125
133
*
134
+ * <p>
135
+ * Upgrade warning: in prior version 1.17, this method required milliseconds to be exactly 3
136
+ * digits (if included), and did not throw an exception for all types of invalid input values, but
137
+ * starting in version 1.18, the parsing done by this method has become more strict to enforce
138
+ * that only valid RFC3339 strings are entered, and if not, it throws a
139
+ * {@link NumberFormatException}. Also, in accordance with the RFC3339 standard, any number of
140
+ * milliseconds digits is now allowed.
141
+ * </p>
142
+ *
126
143
* @param value an <a href='http://tools.ietf.org/html/rfc3339'>RFC 3339</a> date/time value.
127
144
* @since 1.11
128
145
*/
129
146
public DateTime (String value ) {
130
- // TODO(rmistry): Move the implementation of parseRfc3339 into this constructor. Implementation
131
- // of parseRfc3339 can then do "return new DateTime(str);".
147
+ // Note, the following refactoring is being considered: Move the implementation of parseRfc3339
148
+ // into this constructor. Implementation of parseRfc3339 can then do
149
+ // "return new DateTime(str);".
132
150
DateTime dateTime = parseRfc3339 (value );
133
151
this .dateOnly = dateTime .dateOnly ;
134
152
this .value = dateTime .value ;
@@ -248,58 +266,75 @@ public int hashCode() {
248
266
* Parses an RFC 3339 date/time value.
249
267
*
250
268
* <p>
269
+ * Upgrade warning: in prior version 1.17, this method required milliseconds to be exactly 3
270
+ * digits (if included), and did not throw an exception for all types of invalid input values, but
271
+ * starting in version 1.18, the parsing done by this method has become more strict to enforce
272
+ * that only valid RFC3339 strings are entered, and if not, it throws a
273
+ * {@link NumberFormatException}. Also, in accordance with the RFC3339 standard, any number of
274
+ * milliseconds digits is now allowed.
275
+ * </p>
276
+ *
277
+ * <p>
251
278
* For the date-only case, the time zone is ignored and the hourOfDay, minute, second, and
252
279
* millisecond parameters are set to zero.
253
280
* </p>
281
+ *
282
+ * @param str Date/time string in RFC3339 format
283
+ * @throws NumberFormatException if {@code str} doesn't match the RFC3339 standard format; an
284
+ * exception is thrown if {@code str} doesn't match {@code RFC3339_REGEX} or if it
285
+ * contains a time zone shift but no time.
254
286
*/
255
287
public static DateTime parseRfc3339 (String str ) throws NumberFormatException {
256
- try {
257
- int year = Integer .parseInt (str .substring (0 , 4 ));
258
- int month = Integer .parseInt (str .substring (5 , 7 )) - 1 ;
259
- int day = Integer .parseInt (str .substring (8 , 10 ));
260
- int tzIndex ;
261
- int length = str .length ();
262
- boolean dateOnly = length <= 10 || Character .toUpperCase (str .charAt (10 )) != 'T' ;
263
- int hourOfDay = 0 ;
264
- int minute = 0 ;
265
- int second = 0 ;
266
- int milliseconds = 0 ;
267
- Integer tzShiftInteger = null ;
268
- if (dateOnly ) {
269
- tzIndex = Integer .MAX_VALUE ;
270
- } else {
271
- hourOfDay = Integer .parseInt (str .substring (11 , 13 ));
272
- minute = Integer .parseInt (str .substring (14 , 16 ));
273
- second = Integer .parseInt (str .substring (17 , 19 ));
274
- if (str .charAt (19 ) == '.' ) {
275
- milliseconds = Integer .parseInt (str .substring (20 , 23 ));
276
- tzIndex = 23 ;
277
- } else {
278
- tzIndex = 19 ;
279
- }
288
+ Matcher matcher = RFC3339_PATTERN .matcher (str );
289
+ if (!matcher .matches ()) {
290
+ throw new NumberFormatException ("Invalid date/time format: " + str );
291
+ }
292
+
293
+ int year = Integer .parseInt (matcher .group (1 )); // yyyy
294
+ int month = Integer .parseInt (matcher .group (2 )) - 1 ; // MM
295
+ int day = Integer .parseInt (matcher .group (3 )); // dd
296
+ boolean isTimeGiven = matcher .group (4 ) != null ; // 'T'HH:mm:ss.milliseconds
297
+ String tzShiftRegexGroup = matcher .group (9 ); // 'Z', or time zone shift HH:mm following '+'/'-'
298
+ boolean isTzShiftGiven = tzShiftRegexGroup != null ;
299
+ int hourOfDay = 0 ;
300
+ int minute = 0 ;
301
+ int second = 0 ;
302
+ int milliseconds = 0 ;
303
+ Integer tzShiftInteger = null ;
304
+
305
+ if (isTzShiftGiven && !isTimeGiven ) {
306
+ throw new NumberFormatException ("Invalid date/time format, cannot specify time zone shift" +
307
+ " without specifying time: " + str );
308
+ }
309
+
310
+ if (isTimeGiven ) {
311
+ hourOfDay = Integer .parseInt (matcher .group (5 )); // HH
312
+ minute = Integer .parseInt (matcher .group (6 )); // mm
313
+ second = Integer .parseInt (matcher .group (7 )); // ss
314
+ if (matcher .group (8 ) != null ) { // contains .milliseconds?
315
+ milliseconds = Integer .parseInt (matcher .group (8 ).substring (1 )); // milliseconds
280
316
}
281
- Calendar dateTime = new GregorianCalendar ( GMT );
282
- dateTime . set ( year , month , day , hourOfDay , minute , second );
283
- dateTime .set (Calendar . MILLISECOND , milliseconds );
284
- long value = dateTime .getTimeInMillis ( );
285
- if ( length > tzIndex ) {
286
- int tzShift ;
287
- if (Character . toUpperCase ( str . charAt ( tzIndex )) == 'Z' ) {
288
- tzShift = 0 ;
289
- } else {
290
- tzShift = Integer . parseInt ( str . substring ( tzIndex + 1 , tzIndex + 3 )) * 60
291
- + Integer . parseInt ( str . substring ( tzIndex + 4 , tzIndex + 6 ));
292
- if ( str . charAt ( tzIndex ) == '-' ) {
293
- tzShift = - tzShift ;
294
- }
295
- value -= tzShift * 60000L ;
317
+ }
318
+ Calendar dateTime = new GregorianCalendar ( GMT );
319
+ dateTime .set (year , month , day , hourOfDay , minute , second );
320
+ dateTime .set ( Calendar . MILLISECOND , milliseconds );
321
+ long value = dateTime . getTimeInMillis ();
322
+
323
+ if (isTimeGiven && isTzShiftGiven ) {
324
+ int tzShift ;
325
+ if ( Character . toUpperCase ( tzShiftRegexGroup . charAt ( 0 )) == 'Z' ) {
326
+ tzShift = 0 ;
327
+ } else {
328
+ tzShift = Integer . parseInt ( matcher . group ( 11 )) * 60 // time zone shift HH
329
+ + Integer . parseInt ( matcher . group ( 12 )); // time zone shift mm
330
+ if ( matcher . group ( 10 ). charAt ( 0 ) == '-' ) { // time zone shift + or -
331
+ tzShift = - tzShift ;
296
332
}
297
- tzShiftInteger = tzShift ;
333
+ value - = tzShift * 60000L ; // e.g. if 1 hour ahead of UTC, subtract an hour to get UTC time
298
334
}
299
- return new DateTime (dateOnly , value , tzShiftInteger );
300
- } catch (StringIndexOutOfBoundsException e ) {
301
- throw new NumberFormatException ("Invalid date/time format: " + str );
335
+ tzShiftInteger = tzShift ;
302
336
}
337
+ return new DateTime (!isTimeGiven , value , tzShiftInteger );
303
338
}
304
339
305
340
/** Appends a zero-padded number to a string builder. */
0 commit comments