001 /*
002 * Copyright 2001-2011 Stephen Colebourne
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.joda.time.format;
017
018 import java.util.Arrays;
019 import java.util.Locale;
020
021 import org.joda.time.Chronology;
022 import org.joda.time.DateTimeField;
023 import org.joda.time.DateTimeFieldType;
024 import org.joda.time.DateTimeUtils;
025 import org.joda.time.DateTimeZone;
026 import org.joda.time.DurationField;
027 import org.joda.time.DurationFieldType;
028 import org.joda.time.IllegalFieldValueException;
029
030 /**
031 * DateTimeParserBucket is an advanced class, intended mainly for parser
032 * implementations. It can also be used during normal parsing operations to
033 * capture more information about the parse.
034 * <p>
035 * This class allows fields to be saved in any order, but be physically set in
036 * a consistent order. This is useful for parsing against formats that allow
037 * field values to contradict each other.
038 * <p>
039 * Field values are applied in an order where the "larger" fields are set
040 * first, making their value less likely to stick. A field is larger than
041 * another when it's range duration is longer. If both ranges are the same,
042 * then the larger field has the longer duration. If it cannot be determined
043 * which field is larger, then the fields are set in the order they were saved.
044 * <p>
045 * For example, these fields were saved in this order: dayOfWeek, monthOfYear,
046 * dayOfMonth, dayOfYear. When computeMillis is called, the fields are set in
047 * this order: monthOfYear, dayOfYear, dayOfMonth, dayOfWeek.
048 * <p>
049 * DateTimeParserBucket is mutable and not thread-safe.
050 *
051 * @author Brian S O'Neill
052 * @author Fredrik Borgh
053 * @since 1.0
054 */
055 public class DateTimeParserBucket {
056
057 /** The chronology to use for parsing. */
058 private final Chronology iChrono;
059 private final long iMillis;
060
061 /** The parsed zone, initialised to formatter zone. */
062 private DateTimeZone iZone;
063 /** The parsed offset. */
064 private Integer iOffset;
065 /** The locale to use for parsing. */
066 private Locale iLocale;
067 /** Used for parsing two-digit years. */
068 private Integer iPivotYear;
069 /** Used for parsing month/day without year. */
070 private int iDefaultYear;
071
072 private SavedField[] iSavedFields = new SavedField[8];
073 private int iSavedFieldsCount;
074 private boolean iSavedFieldsShared;
075
076 private Object iSavedState;
077
078 /**
079 * Constructs a bucket.
080 *
081 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time
082 * @param chrono the chronology to use
083 * @param locale the locale to use
084 * @deprecated Use longer constructor
085 */
086 @Deprecated
087 public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale) {
088 this(instantLocal, chrono, locale, null, 2000);
089 }
090
091 /**
092 * Constructs a bucket, with the option of specifying the pivot year for
093 * two-digit year parsing.
094 *
095 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time
096 * @param chrono the chronology to use
097 * @param locale the locale to use
098 * @param pivotYear the pivot year to use when parsing two-digit years
099 * @since 1.1
100 * @deprecated Use longer constructor
101 */
102 @Deprecated
103 public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale, Integer pivotYear) {
104 this(instantLocal, chrono, locale, pivotYear, 2000);
105 }
106
107 /**
108 * Constructs a bucket, with the option of specifying the pivot year for
109 * two-digit year parsing.
110 *
111 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time
112 * @param chrono the chronology to use
113 * @param locale the locale to use
114 * @param pivotYear the pivot year to use when parsing two-digit years
115 * @since 2.0
116 */
117 public DateTimeParserBucket(long instantLocal, Chronology chrono,
118 Locale locale, Integer pivotYear, int defaultYear) {
119 super();
120 chrono = DateTimeUtils.getChronology(chrono);
121 iMillis = instantLocal;
122 iZone = chrono.getZone();
123 iChrono = chrono.withUTC();
124 iLocale = (locale == null ? Locale.getDefault() : locale);
125 iPivotYear = pivotYear;
126 iDefaultYear = defaultYear;
127 }
128
129 //-----------------------------------------------------------------------
130 /**
131 * Gets the chronology of the bucket, which will be a local (UTC) chronology.
132 */
133 public Chronology getChronology() {
134 return iChrono;
135 }
136
137 //-----------------------------------------------------------------------
138 /**
139 * Returns the locale to be used during parsing.
140 *
141 * @return the locale to use
142 */
143 public Locale getLocale() {
144 return iLocale;
145 }
146
147 //-----------------------------------------------------------------------
148 /**
149 * Returns the time zone used by computeMillis.
150 */
151 public DateTimeZone getZone() {
152 return iZone;
153 }
154
155 /**
156 * Set a time zone to be used when computeMillis is called.
157 */
158 public void setZone(DateTimeZone zone) {
159 iSavedState = null;
160 iZone = zone;
161 }
162
163 //-----------------------------------------------------------------------
164 /**
165 * Returns the time zone offset in milliseconds used by computeMillis.
166 * @deprecated use Integer version
167 */
168 @Deprecated
169 public int getOffset() {
170 return (iOffset != null ? iOffset : 0);
171 }
172
173 /**
174 * Returns the time zone offset in milliseconds used by computeMillis.
175 */
176 public Integer getOffsetInteger() {
177 return iOffset;
178 }
179
180 /**
181 * Set a time zone offset to be used when computeMillis is called.
182 * @deprecated use Integer version
183 */
184 @Deprecated
185 public void setOffset(int offset) {
186 iSavedState = null;
187 iOffset = offset;
188 }
189
190 /**
191 * Set a time zone offset to be used when computeMillis is called.
192 */
193 public void setOffset(Integer offset) {
194 iSavedState = null;
195 iOffset = offset;
196 }
197
198 //-----------------------------------------------------------------------
199 /**
200 * Returns the default year used when information is incomplete.
201 * <p>
202 * This is used for two-digit years and when the largest parsed field is
203 * months or days.
204 * <p>
205 * A null value for two-digit years means to use the value from DateTimeFormatterBuilder.
206 * A null value for month/day only parsing will cause the default of 2000 to be used.
207 *
208 * @return Integer value of the pivot year, null if not set
209 * @since 1.1
210 */
211 public Integer getPivotYear() {
212 return iPivotYear;
213 }
214
215 /**
216 * Sets the pivot year to use when parsing two digit years.
217 * <p>
218 * If the value is set to null, this will indicate that default
219 * behaviour should be used.
220 *
221 * @param pivotYear the pivot year to use
222 * @since 1.1
223 */
224 public void setPivotYear(Integer pivotYear) {
225 iPivotYear = pivotYear;
226 }
227
228 //-----------------------------------------------------------------------
229 /**
230 * Saves a datetime field value.
231 *
232 * @param field the field, whose chronology must match that of this bucket
233 * @param value the value
234 */
235 public void saveField(DateTimeField field, int value) {
236 saveField(new SavedField(field, value));
237 }
238
239 /**
240 * Saves a datetime field value.
241 *
242 * @param fieldType the field type
243 * @param value the value
244 */
245 public void saveField(DateTimeFieldType fieldType, int value) {
246 saveField(new SavedField(fieldType.getField(iChrono), value));
247 }
248
249 /**
250 * Saves a datetime field text value.
251 *
252 * @param fieldType the field type
253 * @param text the text value
254 * @param locale the locale to use
255 */
256 public void saveField(DateTimeFieldType fieldType, String text, Locale locale) {
257 saveField(new SavedField(fieldType.getField(iChrono), text, locale));
258 }
259
260 private void saveField(SavedField field) {
261 SavedField[] savedFields = iSavedFields;
262 int savedFieldsCount = iSavedFieldsCount;
263
264 if (savedFieldsCount == savedFields.length || iSavedFieldsShared) {
265 // Expand capacity or merely copy if saved fields are shared.
266 SavedField[] newArray = new SavedField
267 [savedFieldsCount == savedFields.length ? savedFieldsCount * 2 : savedFields.length];
268 System.arraycopy(savedFields, 0, newArray, 0, savedFieldsCount);
269 iSavedFields = savedFields = newArray;
270 iSavedFieldsShared = false;
271 }
272
273 iSavedState = null;
274 savedFields[savedFieldsCount] = field;
275 iSavedFieldsCount = savedFieldsCount + 1;
276 }
277
278 /**
279 * Saves the state of this bucket, returning it in an opaque object. Call
280 * restoreState to undo any changes that were made since the state was
281 * saved. Calls to saveState may be nested.
282 *
283 * @return opaque saved state, which may be passed to restoreState
284 */
285 public Object saveState() {
286 if (iSavedState == null) {
287 iSavedState = new SavedState();
288 }
289 return iSavedState;
290 }
291
292 /**
293 * Restores the state of this bucket from a previously saved state. The
294 * state object passed into this method is not consumed, and it can be used
295 * later to restore to that state again.
296 *
297 * @param savedState opaque saved state, returned from saveState
298 * @return true state object is valid and state restored
299 */
300 public boolean restoreState(Object savedState) {
301 if (savedState instanceof SavedState) {
302 if (((SavedState) savedState).restoreState(this)) {
303 iSavedState = savedState;
304 return true;
305 }
306 }
307 return false;
308 }
309
310 /**
311 * Computes the parsed datetime by setting the saved fields.
312 * This method is idempotent, but it is not thread-safe.
313 *
314 * @return milliseconds since 1970-01-01T00:00:00Z
315 * @throws IllegalArgumentException if any field is out of range
316 */
317 public long computeMillis() {
318 return computeMillis(false, null);
319 }
320
321 /**
322 * Computes the parsed datetime by setting the saved fields.
323 * This method is idempotent, but it is not thread-safe.
324 *
325 * @param resetFields false by default, but when true, unsaved field values are cleared
326 * @return milliseconds since 1970-01-01T00:00:00Z
327 * @throws IllegalArgumentException if any field is out of range
328 */
329 public long computeMillis(boolean resetFields) {
330 return computeMillis(resetFields, null);
331 }
332
333 /**
334 * Computes the parsed datetime by setting the saved fields.
335 * This method is idempotent, but it is not thread-safe.
336 *
337 * @param resetFields false by default, but when true, unsaved field values are cleared
338 * @param text optional text being parsed, to be included in any error message
339 * @return milliseconds since 1970-01-01T00:00:00Z
340 * @throws IllegalArgumentException if any field is out of range
341 * @since 1.3
342 */
343 public long computeMillis(boolean resetFields, String text) {
344 SavedField[] savedFields = iSavedFields;
345 int count = iSavedFieldsCount;
346 if (iSavedFieldsShared) {
347 iSavedFields = savedFields = (SavedField[])iSavedFields.clone();
348 iSavedFieldsShared = false;
349 }
350 sort(savedFields, count);
351 if (count > 0) {
352 // alter base year for parsing if first field is month or day
353 DurationField months = DurationFieldType.months().getField(iChrono);
354 DurationField days = DurationFieldType.days().getField(iChrono);
355 DurationField first = savedFields[0].iField.getDurationField();
356 if (compareReverse(first, months) >= 0 && compareReverse(first, days) <= 0) {
357 saveField(DateTimeFieldType.year(), iDefaultYear);
358 return computeMillis(resetFields, text);
359 }
360 }
361
362 long millis = iMillis;
363 try {
364 for (int i = 0; i < count; i++) {
365 millis = savedFields[i].set(millis, resetFields);
366 }
367 if (resetFields) {
368 for (int i = 0; i < count; i++) {
369 millis = savedFields[i].set(millis, i == (count - 1));
370 }
371 }
372 } catch (IllegalFieldValueException e) {
373 if (text != null) {
374 e.prependMessage("Cannot parse \"" + text + '"');
375 }
376 throw e;
377 }
378
379 if (iOffset != null) {
380 millis -= iOffset;
381 } else if (iZone != null) {
382 int offset = iZone.getOffsetFromLocal(millis);
383 millis -= offset;
384 if (offset != iZone.getOffset(millis)) {
385 String message =
386 "Illegal instant due to time zone offset transition (" + iZone + ')';
387 if (text != null) {
388 message = "Cannot parse \"" + text + "\": " + message;
389 }
390 throw new IllegalArgumentException(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 }