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    }