View Javadoc

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 }