001/*
002 *  Copyright 2001-2013 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 */
016package org.joda.time.format;
017
018import java.util.Arrays;
019import java.util.Locale;
020
021import org.joda.time.Chronology;
022import org.joda.time.DateTimeField;
023import org.joda.time.DateTimeFieldType;
024import org.joda.time.DateTimeUtils;
025import org.joda.time.DateTimeZone;
026import org.joda.time.DurationField;
027import org.joda.time.DurationFieldType;
028import org.joda.time.IllegalFieldValueException;
029import org.joda.time.IllegalInstantException;
030
031/**
032 * DateTimeParserBucket is an advanced class, intended mainly for parser
033 * implementations. It can also be used during normal parsing operations to
034 * capture more information about the parse.
035 * <p>
036 * This class allows fields to be saved in any order, but be physically set in
037 * a consistent order. This is useful for parsing against formats that allow
038 * field values to contradict each other.
039 * <p>
040 * Field values are applied in an order where the "larger" fields are set
041 * first, making their value less likely to stick.  A field is larger than
042 * another when it's range duration is longer. If both ranges are the same,
043 * then the larger field has the longer duration. If it cannot be determined
044 * which field is larger, then the fields are set in the order they were saved.
045 * <p>
046 * For example, these fields were saved in this order: dayOfWeek, monthOfYear,
047 * dayOfMonth, dayOfYear. When computeMillis is called, the fields are set in
048 * this order: monthOfYear, dayOfYear, dayOfMonth, dayOfWeek.
049 * <p>
050 * DateTimeParserBucket is mutable and not thread-safe.
051 *
052 * @author Brian S O'Neill
053 * @author Fredrik Borgh
054 * @since 1.0
055 */
056public class DateTimeParserBucket {
057
058    /** The chronology to use for parsing. */
059    private final Chronology iChrono;
060    private final long iMillis;
061    
062    /** The parsed zone, initialised to formatter zone. */
063    private DateTimeZone iZone;
064    /** The parsed offset. */
065    private Integer iOffset;
066    /** The locale to use for parsing. */
067    private Locale iLocale;
068    /** Used for parsing two-digit years. */
069    private Integer iPivotYear;
070    /** Used for parsing month/day without year. */
071    private int iDefaultYear;
072
073    private SavedField[] iSavedFields = new SavedField[8];
074    private int iSavedFieldsCount;
075    private boolean iSavedFieldsShared;
076    
077    private Object iSavedState;
078
079    /**
080     * Constructs a bucket.
081     * 
082     * @param instantLocal  the initial millis from 1970-01-01T00:00:00, local time
083     * @param chrono  the chronology to use
084     * @param locale  the locale to use
085     * @deprecated Use longer constructor
086     */
087    @Deprecated
088    public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale) {
089        this(instantLocal, chrono, locale, null, 2000);
090    }
091
092    /**
093     * Constructs a bucket, with the option of specifying the pivot year for
094     * two-digit year parsing.
095     *
096     * @param instantLocal  the initial millis from 1970-01-01T00:00:00, local time
097     * @param chrono  the chronology to use
098     * @param locale  the locale to use
099     * @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}