1 /*
2 * Copyright 2001-2013 Stephen Colebourne
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.joda.time.format;
17
18 import java.util.Arrays;
19 import java.util.Locale;
20
21 import org.joda.time.Chronology;
22 import org.joda.time.DateTimeField;
23 import org.joda.time.DateTimeFieldType;
24 import org.joda.time.DateTimeUtils;
25 import org.joda.time.DateTimeZone;
26 import org.joda.time.DurationField;
27 import org.joda.time.DurationFieldType;
28 import org.joda.time.IllegalFieldValueException;
29 import org.joda.time.IllegalInstantException;
30
31 /**
32 * DateTimeParserBucket is an advanced class, intended mainly for parser
33 * implementations. It can also be used during normal parsing operations to
34 * capture more information about the parse.
35 * <p>
36 * This class allows fields to be saved in any order, but be physically set in
37 * a consistent order. This is useful for parsing against formats that allow
38 * field values to contradict each other.
39 * <p>
40 * Field values are applied in an order where the "larger" fields are set
41 * first, making their value less likely to stick. A field is larger than
42 * another when it's range duration is longer. If both ranges are the same,
43 * then the larger field has the longer duration. If it cannot be determined
44 * which field is larger, then the fields are set in the order they were saved.
45 * <p>
46 * For example, these fields were saved in this order: dayOfWeek, monthOfYear,
47 * dayOfMonth, dayOfYear. When computeMillis is called, the fields are set in
48 * this order: monthOfYear, dayOfYear, dayOfMonth, dayOfWeek.
49 * <p>
50 * DateTimeParserBucket is mutable and not thread-safe.
51 *
52 * @author Brian S O'Neill
53 * @author Fredrik Borgh
54 * @since 1.0
55 */
56 public class DateTimeParserBucket {
57
58 /** The chronology to use for parsing. */
59 private final Chronology iChrono;
60 private final long iMillis;
61
62 /** The parsed zone, initialised to formatter zone. */
63 private DateTimeZone iZone;
64 /** The parsed offset. */
65 private Integer iOffset;
66 /** The locale to use for parsing. */
67 private Locale iLocale;
68 /** Used for parsing two-digit years. */
69 private Integer iPivotYear;
70 /** Used for parsing month/day without year. */
71 private int iDefaultYear;
72
73 private SavedField[] iSavedFields = new SavedField[8];
74 private int iSavedFieldsCount;
75 private boolean iSavedFieldsShared;
76
77 private Object iSavedState;
78
79 /**
80 * Constructs a bucket.
81 *
82 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time
83 * @param chrono the chronology to use
84 * @param locale the locale to use
85 * @deprecated Use longer constructor
86 */
87 @Deprecated
88 public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale) {
89 this(instantLocal, chrono, locale, null, 2000);
90 }
91
92 /**
93 * Constructs a bucket, with the option of specifying the pivot year for
94 * two-digit year parsing.
95 *
96 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time
97 * @param chrono the chronology to use
98 * @param locale the locale to use
99 * @param pivotYear the pivot year to use when parsing two-digit years
100 * @since 1.1
101 * @deprecated Use longer constructor
102 */
103 @Deprecated
104 public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale, Integer pivotYear) {
105 this(instantLocal, chrono, locale, pivotYear, 2000);
106 }
107
108 /**
109 * Constructs a bucket, with the option of specifying the pivot year for
110 * two-digit year parsing.
111 *
112 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time
113 * @param chrono the chronology to use
114 * @param locale the locale to use
115 * @param pivotYear the pivot year to use when parsing two-digit years
116 * @since 2.0
117 */
118 public DateTimeParserBucket(long instantLocal, Chronology chrono,
119 Locale locale, Integer pivotYear, int defaultYear) {
120 super();
121 chrono = DateTimeUtils.getChronology(chrono);
122 iMillis = instantLocal;
123 iZone = chrono.getZone();
124 iChrono = chrono.withUTC();
125 iLocale = (locale == null ? Locale.getDefault() : locale);
126 iPivotYear = pivotYear;
127 iDefaultYear = defaultYear;
128 }
129
130 //-----------------------------------------------------------------------
131 /**
132 * Gets the chronology of the bucket, which will be a local (UTC) chronology.
133 */
134 public Chronology getChronology() {
135 return iChrono;
136 }
137
138 //-----------------------------------------------------------------------
139 /**
140 * Returns the locale to be used during parsing.
141 *
142 * @return the locale to use
143 */
144 public Locale getLocale() {
145 return iLocale;
146 }
147
148 //-----------------------------------------------------------------------
149 /**
150 * Returns the time zone used by computeMillis.
151 */
152 public DateTimeZone getZone() {
153 return iZone;
154 }
155
156 /**
157 * Set a time zone to be used when computeMillis is called.
158 */
159 public void setZone(DateTimeZone zone) {
160 iSavedState = null;
161 iZone = zone;
162 }
163
164 //-----------------------------------------------------------------------
165 /**
166 * Returns the time zone offset in milliseconds used by computeMillis.
167 * @deprecated use Integer version
168 */
169 @Deprecated
170 public int getOffset() {
171 return (iOffset != null ? iOffset : 0);
172 }
173
174 /**
175 * Returns the time zone offset in milliseconds used by computeMillis.
176 */
177 public Integer getOffsetInteger() {
178 return iOffset;
179 }
180
181 /**
182 * Set a time zone offset to be used when computeMillis is called.
183 * @deprecated use Integer version
184 */
185 @Deprecated
186 public void setOffset(int offset) {
187 iSavedState = null;
188 iOffset = offset;
189 }
190
191 /**
192 * Set a time zone offset to be used when computeMillis is called.
193 */
194 public void setOffset(Integer offset) {
195 iSavedState = null;
196 iOffset = offset;
197 }
198
199 //-----------------------------------------------------------------------
200 /**
201 * Returns the default year used when information is incomplete.
202 * <p>
203 * This is used for two-digit years and when the largest parsed field is
204 * months or days.
205 * <p>
206 * A null value for two-digit years means to use the value from DateTimeFormatterBuilder.
207 * A null value for month/day only parsing will cause the default of 2000 to be used.
208 *
209 * @return Integer value of the pivot year, null if not set
210 * @since 1.1
211 */
212 public Integer getPivotYear() {
213 return iPivotYear;
214 }
215
216 /**
217 * Sets the pivot year to use when parsing two digit years.
218 * <p>
219 * If the value is set to null, this will indicate that default
220 * behaviour should be used.
221 *
222 * @param pivotYear the pivot year to use
223 * @since 1.1
224 */
225 public void setPivotYear(Integer pivotYear) {
226 iPivotYear = pivotYear;
227 }
228
229 //-----------------------------------------------------------------------
230 /**
231 * Saves a datetime field value.
232 *
233 * @param field the field, whose chronology must match that of this bucket
234 * @param value the value
235 */
236 public void saveField(DateTimeField field, int value) {
237 saveField(new SavedField(field, value));
238 }
239
240 /**
241 * Saves a datetime field value.
242 *
243 * @param fieldType the field type
244 * @param value the value
245 */
246 public void saveField(DateTimeFieldType fieldType, int value) {
247 saveField(new SavedField(fieldType.getField(iChrono), value));
248 }
249
250 /**
251 * Saves a datetime field text value.
252 *
253 * @param fieldType the field type
254 * @param text the text value
255 * @param locale the locale to use
256 */
257 public void saveField(DateTimeFieldType fieldType, String text, Locale locale) {
258 saveField(new SavedField(fieldType.getField(iChrono), text, locale));
259 }
260
261 private void saveField(SavedField field) {
262 SavedField[] savedFields = iSavedFields;
263 int savedFieldsCount = iSavedFieldsCount;
264
265 if (savedFieldsCount == savedFields.length || iSavedFieldsShared) {
266 // Expand capacity or merely copy if saved fields are shared.
267 SavedField[] newArray = new SavedField
268 [savedFieldsCount == savedFields.length ? savedFieldsCount * 2 : savedFields.length];
269 System.arraycopy(savedFields, 0, newArray, 0, savedFieldsCount);
270 iSavedFields = savedFields = newArray;
271 iSavedFieldsShared = false;
272 }
273
274 iSavedState = null;
275 savedFields[savedFieldsCount] = field;
276 iSavedFieldsCount = savedFieldsCount + 1;
277 }
278
279 /**
280 * Saves the state of this bucket, returning it in an opaque object. Call
281 * restoreState to undo any changes that were made since the state was
282 * saved. Calls to saveState may be nested.
283 *
284 * @return opaque saved state, which may be passed to restoreState
285 */
286 public Object saveState() {
287 if (iSavedState == null) {
288 iSavedState = new SavedState();
289 }
290 return iSavedState;
291 }
292
293 /**
294 * Restores the state of this bucket from a previously saved state. The
295 * state object passed into this method is not consumed, and it can be used
296 * later to restore to that state again.
297 *
298 * @param savedState opaque saved state, returned from saveState
299 * @return true state object is valid and state restored
300 */
301 public boolean restoreState(Object savedState) {
302 if (savedState instanceof SavedState) {
303 if (((SavedState) savedState).restoreState(this)) {
304 iSavedState = savedState;
305 return true;
306 }
307 }
308 return false;
309 }
310
311 /**
312 * Computes the parsed datetime by setting the saved fields.
313 * This method is idempotent, but it is not thread-safe.
314 *
315 * @return milliseconds since 1970-01-01T00:00:00Z
316 * @throws IllegalArgumentException if any field is out of range
317 */
318 public long computeMillis() {
319 return computeMillis(false, null);
320 }
321
322 /**
323 * Computes the parsed datetime by setting the saved fields.
324 * This method is idempotent, but it is not thread-safe.
325 *
326 * @param resetFields false by default, but when true, unsaved field values are cleared
327 * @return milliseconds since 1970-01-01T00:00:00Z
328 * @throws IllegalArgumentException if any field is out of range
329 */
330 public long computeMillis(boolean resetFields) {
331 return computeMillis(resetFields, null);
332 }
333
334 /**
335 * Computes the parsed datetime by setting the saved fields.
336 * This method is idempotent, but it is not thread-safe.
337 *
338 * @param resetFields false by default, but when true, unsaved field values are cleared
339 * @param text optional text being parsed, to be included in any error message
340 * @return milliseconds since 1970-01-01T00:00:00Z
341 * @throws IllegalArgumentException if any field is out of range
342 * @since 1.3
343 */
344 public long computeMillis(boolean resetFields, String text) {
345 SavedField[] savedFields = iSavedFields;
346 int count = iSavedFieldsCount;
347 if (iSavedFieldsShared) {
348 iSavedFields = savedFields = (SavedField[])iSavedFields.clone();
349 iSavedFieldsShared = false;
350 }
351 sort(savedFields, count);
352 if (count > 0) {
353 // alter base year for parsing if first field is month or day
354 DurationField months = DurationFieldType.months().getField(iChrono);
355 DurationField days = DurationFieldType.days().getField(iChrono);
356 DurationField first = savedFields[0].iField.getDurationField();
357 if (compareReverse(first, months) >= 0 && compareReverse(first, days) <= 0) {
358 saveField(DateTimeFieldType.year(), iDefaultYear);
359 return computeMillis(resetFields, text);
360 }
361 }
362
363 long millis = iMillis;
364 try {
365 for (int i = 0; i < count; i++) {
366 millis = savedFields[i].set(millis, resetFields);
367 }
368 if (resetFields) {
369 for (int i = 0; i < count; i++) {
370 millis = savedFields[i].set(millis, i == (count - 1));
371 }
372 }
373 } catch (IllegalFieldValueException e) {
374 if (text != null) {
375 e.prependMessage("Cannot parse \"" + text + '"');
376 }
377 throw e;
378 }
379
380 if (iOffset != null) {
381 millis -= iOffset;
382 } else if (iZone != null) {
383 int offset = iZone.getOffsetFromLocal(millis);
384 millis -= offset;
385 if (offset != iZone.getOffset(millis)) {
386 String message = "Illegal instant due to time zone offset transition (" + iZone + ')';
387 if (text != null) {
388 message = "Cannot parse \"" + text + "\": " + message;
389 }
390 throw new IllegalInstantException(message);
391 }
392 }
393
394 return millis;
395 }
396
397 /**
398 * Sorts elements [0,high). Calling java.util.Arrays isn't always the right
399 * choice since it always creates an internal copy of the array, even if it
400 * doesn't need to. If the array slice is small enough, an insertion sort
401 * is chosen instead, but it doesn't need a copy!
402 * <p>
403 * This method has a modified version of that insertion sort, except it
404 * doesn't create an unnecessary array copy. If high is over 10, then
405 * java.util.Arrays is called, which will perform a merge sort, which is
406 * faster than insertion sort on large lists.
407 * <p>
408 * The end result is much greater performance when computeMillis is called.
409 * Since the amount of saved fields is small, the insertion sort is a
410 * better choice. Additional performance is gained since there is no extra
411 * array allocation and copying. Also, the insertion sort here does not
412 * perform any casting operations. The version in java.util.Arrays performs
413 * casts within the insertion sort loop.
414 */
415 private static void sort(SavedField[] array, int high) {
416 if (high > 10) {
417 Arrays.sort(array, 0, high);
418 } else {
419 for (int i=0; i<high; i++) {
420 for (int j=i; j>0 && (array[j-1]).compareTo(array[j])>0; j--) {
421 SavedField t = array[j];
422 array[j] = array[j-1];
423 array[j-1] = t;
424 }
425 }
426 }
427 }
428
429 class SavedState {
430 final DateTimeZone iZone;
431 final Integer iOffset;
432 final SavedField[] iSavedFields;
433 final int iSavedFieldsCount;
434
435 SavedState() {
436 this.iZone = DateTimeParserBucket.this.iZone;
437 this.iOffset = DateTimeParserBucket.this.iOffset;
438 this.iSavedFields = DateTimeParserBucket.this.iSavedFields;
439 this.iSavedFieldsCount = DateTimeParserBucket.this.iSavedFieldsCount;
440 }
441
442 boolean restoreState(DateTimeParserBucket enclosing) {
443 if (enclosing != DateTimeParserBucket.this) {
444 return false;
445 }
446 enclosing.iZone = this.iZone;
447 enclosing.iOffset = this.iOffset;
448 enclosing.iSavedFields = this.iSavedFields;
449 if (this.iSavedFieldsCount < enclosing.iSavedFieldsCount) {
450 // Since count is being restored to a lower count, the
451 // potential exists for new saved fields to destroy data being
452 // shared by another state. Set this flag such that the array
453 // of saved fields is cloned prior to modification.
454 enclosing.iSavedFieldsShared = true;
455 }
456 enclosing.iSavedFieldsCount = this.iSavedFieldsCount;
457 return true;
458 }
459 }
460
461 static class SavedField implements Comparable<SavedField> {
462 final DateTimeField iField;
463 final int iValue;
464 final String iText;
465 final Locale iLocale;
466
467 SavedField(DateTimeField field, int value) {
468 iField = field;
469 iValue = value;
470 iText = null;
471 iLocale = null;
472 }
473
474 SavedField(DateTimeField field, String text, Locale locale) {
475 iField = field;
476 iValue = 0;
477 iText = text;
478 iLocale = locale;
479 }
480
481 long set(long millis, boolean reset) {
482 if (iText == null) {
483 millis = iField.set(millis, iValue);
484 } else {
485 millis = iField.set(millis, iText, iLocale);
486 }
487 if (reset) {
488 millis = iField.roundFloor(millis);
489 }
490 return millis;
491 }
492
493 /**
494 * The field with the longer range duration is ordered first, where
495 * null is considered infinite. If the ranges match, then the field
496 * with the longer duration is ordered first.
497 */
498 public int compareTo(SavedField obj) {
499 DateTimeField other = obj.iField;
500 int result = compareReverse
501 (iField.getRangeDurationField(), other.getRangeDurationField());
502 if (result != 0) {
503 return result;
504 }
505 return compareReverse
506 (iField.getDurationField(), other.getDurationField());
507 }
508 }
509
510 static int compareReverse(DurationField a, DurationField b) {
511 if (a == null || !a.isSupported()) {
512 if (b == null || !b.isSupported()) {
513 return 0;
514 }
515 return -1;
516 }
517 if (b == null || !b.isSupported()) {
518 return 1;
519 }
520 return -a.compareTo(b);
521 }
522 }