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     */
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    import 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     */
056    public 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    }