001    /*
002     *  Copyright 2001-2006 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.IllegalFieldValueException;
028    
029    /**
030     * DateTimeParserBucket is an advanced class, intended mainly for parser
031     * implementations. It can also be used during normal parsing operations to
032     * capture more information about the parse.
033     * <p>
034     * This class allows fields to be saved in any order, but be physically set in
035     * a consistent order. This is useful for parsing against formats that allow
036     * field values to contradict each other.
037     * <p>
038     * Field values are applied in an order where the "larger" fields are set
039     * first, making their value less likely to stick.  A field is larger than
040     * another when it's range duration is longer. If both ranges are the same,
041     * then the larger field has the longer duration. If it cannot be determined
042     * which field is larger, then the fields are set in the order they were saved.
043     * <p>
044     * For example, these fields were saved in this order: dayOfWeek, monthOfYear,
045     * dayOfMonth, dayOfYear. When computeMillis is called, the fields are set in
046     * this order: monthOfYear, dayOfYear, dayOfMonth, dayOfWeek.
047     * <p>
048     * DateTimeParserBucket is mutable and not thread-safe.
049     *
050     * @author Brian S O'Neill
051     * @author Fredrik Borgh
052     * @since 1.0
053     */
054    public class DateTimeParserBucket {
055    
056        /** The chronology to use for parsing. */
057        private final Chronology iChrono;
058        private final long iMillis;
059        
060        // TimeZone to switch to in computeMillis. If null, use offset.
061        private DateTimeZone iZone;
062        private int iOffset;
063        /** The locale to use for parsing. */
064        private Locale iLocale;
065        /** Used for parsing two-digit years. */
066        private Integer iPivotYear;
067    
068        private SavedField[] iSavedFields = new SavedField[8];
069        private int iSavedFieldsCount;
070        private boolean iSavedFieldsShared;
071        
072        private Object iSavedState;
073    
074        /**
075         * Constucts a bucket.
076         * 
077         * @param instantLocal  the initial millis from 1970-01-01T00:00:00, local time
078         * @param chrono  the chronology to use
079         * @param locale  the locale to use
080         */
081        public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale) {
082            this(instantLocal, chrono, locale, null);
083        }
084    
085        /**
086         * Constucts a bucket, with the option of specifying the pivot year for
087         * two-digit year parsing.
088         *
089         * @param instantLocal  the initial millis from 1970-01-01T00:00:00, local time
090         * @param chrono  the chronology to use
091         * @param locale  the locale to use
092         * @param pivotYear  the pivot year to use when parsing two-digit years
093         * @since 1.1
094         */
095        public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale, Integer pivotYear) {
096            super();
097            chrono = DateTimeUtils.getChronology(chrono);
098            iMillis = instantLocal;
099            iChrono = chrono.withUTC();
100            iLocale = (locale == null ? Locale.getDefault() : locale);
101            setZone(chrono.getZone());
102            iPivotYear = pivotYear;
103        }
104    
105        //-----------------------------------------------------------------------
106        /**
107         * Gets the chronology of the bucket, which will be a local (UTC) chronology.
108         */
109        public Chronology getChronology() {
110            return iChrono;
111        }
112    
113        //-----------------------------------------------------------------------
114        /**
115         * Returns the locale to be used during parsing.
116         * 
117         * @return the locale to use
118         */
119        public Locale getLocale() {
120            return iLocale;
121        }
122    
123        //-----------------------------------------------------------------------
124        /**
125         * Returns the time zone used by computeMillis, or null if an offset is
126         * used instead.
127         */
128        public DateTimeZone getZone() {
129            return iZone;
130        }
131        
132        /**
133         * Set a time zone to be used when computeMillis is called, which
134         * overrides any set time zone offset.
135         *
136         * @param zone the date time zone to operate in, or null if UTC
137         */
138        public void setZone(DateTimeZone zone) {
139            iSavedState = null;
140            iZone = zone == DateTimeZone.UTC ? null : zone;
141            iOffset = 0;
142        }
143        
144        //-----------------------------------------------------------------------
145        /**
146         * Returns the time zone offset in milliseconds used by computeMillis,
147         * unless getZone doesn't return null.
148         */
149        public int getOffset() {
150            return iOffset;
151        }
152        
153        /**
154         * Set a time zone offset to be used when computeMillis is called, which
155         * overrides the time zone.
156         */
157        public void setOffset(int offset) {
158            iSavedState = null;
159            iOffset = offset;
160            iZone = null;
161        }
162    
163        //-----------------------------------------------------------------------
164        /**
165         * Returns the pivot year used for parsing two-digit years.
166         * <p>
167         * If null is returned, this indicates default behaviour
168         *
169         * @return Integer value of the pivot year, null if not set
170         * @since 1.1
171         */
172        public Integer getPivotYear() {
173            return iPivotYear;
174        }
175    
176        /**
177         * Sets the pivot year to use when parsing two digit years.
178         * <p>
179         * If the value is set to null, this will indicate that default
180         * behaviour should be used.
181         *
182         * @param pivotYear  the pivot year to use
183         * @since 1.1
184         */
185        public void setPivotYear(Integer pivotYear) {
186            iPivotYear = pivotYear;
187        }
188    
189        //-----------------------------------------------------------------------
190        /**
191         * Saves a datetime field value.
192         * 
193         * @param field  the field, whose chronology must match that of this bucket
194         * @param value  the value
195         */
196        public void saveField(DateTimeField field, int value) {
197            saveField(new SavedField(field, value));
198        }
199        
200        /**
201         * Saves a datetime field value.
202         * 
203         * @param fieldType  the field type
204         * @param value  the value
205         */
206        public void saveField(DateTimeFieldType fieldType, int value) {
207            saveField(new SavedField(fieldType.getField(iChrono), value));
208        }
209        
210        /**
211         * Saves a datetime field text value.
212         * 
213         * @param fieldType  the field type
214         * @param text  the text value
215         * @param locale  the locale to use
216         */
217        public void saveField(DateTimeFieldType fieldType, String text, Locale locale) {
218            saveField(new SavedField(fieldType.getField(iChrono), text, locale));
219        }
220        
221        private void saveField(SavedField field) {
222            SavedField[] savedFields = iSavedFields;
223            int savedFieldsCount = iSavedFieldsCount;
224            
225            if (savedFieldsCount == savedFields.length || iSavedFieldsShared) {
226                // Expand capacity or merely copy if saved fields are shared.
227                SavedField[] newArray = new SavedField
228                    [savedFieldsCount == savedFields.length ? savedFieldsCount * 2 : savedFields.length];
229                System.arraycopy(savedFields, 0, newArray, 0, savedFieldsCount);
230                iSavedFields = savedFields = newArray;
231                iSavedFieldsShared = false;
232            }
233            
234            iSavedState = null;
235            savedFields[savedFieldsCount] = field;
236            iSavedFieldsCount = savedFieldsCount + 1;
237        }
238        
239        /**
240         * Saves the state of this bucket, returning it in an opaque object. Call
241         * restoreState to undo any changes that were made since the state was
242         * saved. Calls to saveState may be nested.
243         *
244         * @return opaque saved state, which may be passed to restoreState
245         */
246        public Object saveState() {
247            if (iSavedState == null) {
248                iSavedState = new SavedState();
249            }
250            return iSavedState;
251        }
252        
253        /**
254         * Restores the state of this bucket from a previously saved state. The
255         * state object passed into this method is not consumed, and it can be used
256         * later to restore to that state again.
257         *
258         * @param savedState opaque saved state, returned from saveState
259         * @return true state object is valid and state restored
260         */
261        public boolean restoreState(Object savedState) {
262            if (savedState instanceof SavedState) {
263                if (((SavedState) savedState).restoreState(this)) {
264                    iSavedState = savedState;
265                    return true;
266                }
267            }
268            return false;
269        }
270        
271        /**
272         * Computes the parsed datetime by setting the saved fields.
273         * This method is idempotent, but it is not thread-safe.
274         *
275         * @return milliseconds since 1970-01-01T00:00:00Z
276         * @throws IllegalArgumentException if any field is out of range
277         */
278        public long computeMillis() {
279            return computeMillis(false, null);
280        }
281        
282        /**
283         * Computes the parsed datetime by setting the saved fields.
284         * This method is idempotent, but it is not thread-safe.
285         *
286         * @param resetFields false by default, but when true, unsaved field values are cleared
287         * @return milliseconds since 1970-01-01T00:00:00Z
288         * @throws IllegalArgumentException if any field is out of range
289         */
290        public long computeMillis(boolean resetFields) {
291            return computeMillis(resetFields, null);
292        }
293    
294        /**
295         * Computes the parsed datetime by setting the saved fields.
296         * This method is idempotent, but it is not thread-safe.
297         *
298         * @param resetFields false by default, but when true, unsaved field values are cleared
299         * @param text optional text being parsed, to be included in any error message
300         * @return milliseconds since 1970-01-01T00:00:00Z
301         * @throws IllegalArgumentException if any field is out of range
302         * @since 1.3
303         */
304        public long computeMillis(boolean resetFields, String text) {
305            SavedField[] savedFields = iSavedFields;
306            int count = iSavedFieldsCount;
307            if (iSavedFieldsShared) {
308                iSavedFields = savedFields = (SavedField[])iSavedFields.clone();
309                iSavedFieldsShared = false;
310            }
311            sort(savedFields, count);
312    
313            long millis = iMillis;
314            try {
315                for (int i=0; i<count; i++) {
316                    millis = savedFields[i].set(millis, resetFields);
317                }
318            } catch (IllegalFieldValueException e) {
319                if (text != null) {
320                    e.prependMessage("Cannot parse \"" + text + '"');
321                }
322                throw e;
323            }
324            
325            if (iZone == null) {
326                millis -= iOffset;
327            } else {
328                int offset = iZone.getOffsetFromLocal(millis);
329                millis -= offset;
330                if (offset != iZone.getOffset(millis)) {
331                    String message =
332                        "Illegal instant due to time zone offset transition (" + iZone + ')';
333                    if (text != null) {
334                        message = "Cannot parse \"" + text + "\": " + message;
335                    }
336                    throw new IllegalArgumentException(message);
337                }
338            }
339            
340            return millis;
341        }
342        
343        /**
344         * Sorts elements [0,high). Calling java.util.Arrays isn't always the right
345         * choice since it always creates an internal copy of the array, even if it
346         * doesn't need to. If the array slice is small enough, an insertion sort
347         * is chosen instead, but it doesn't need a copy!
348         * <p>
349         * This method has a modified version of that insertion sort, except it
350         * doesn't create an unnecessary array copy. If high is over 10, then
351         * java.util.Arrays is called, which will perform a merge sort, which is
352         * faster than insertion sort on large lists.
353         * <p>
354         * The end result is much greater performace when computeMillis is called.
355         * Since the amount of saved fields is small, the insertion sort is a
356         * better choice. Additional performance is gained since there is no extra
357         * array allocation and copying. Also, the insertion sort here does not
358         * perform any casting operations. The version in java.util.Arrays performs
359         * casts within the insertion sort loop.
360         */
361        private static void sort(Comparable[] array, int high) {
362            if (high > 10) {
363                Arrays.sort(array, 0, high);
364            } else {
365                for (int i=0; i<high; i++) {
366                    for (int j=i; j>0 && (array[j-1]).compareTo(array[j])>0; j--) {
367                        Comparable t = array[j];
368                        array[j] = array[j-1];
369                        array[j-1] = t;
370                    }
371                }
372            }
373        }
374    
375        class SavedState {
376            final DateTimeZone iZone;
377            final int iOffset;
378            final SavedField[] iSavedFields;
379            final int iSavedFieldsCount;
380            
381            SavedState() {
382                this.iZone = DateTimeParserBucket.this.iZone;
383                this.iOffset = DateTimeParserBucket.this.iOffset;
384                this.iSavedFields = DateTimeParserBucket.this.iSavedFields;
385                this.iSavedFieldsCount = DateTimeParserBucket.this.iSavedFieldsCount;
386            }
387            
388            boolean restoreState(DateTimeParserBucket enclosing) {
389                if (enclosing != DateTimeParserBucket.this) {
390                    return false;
391                }
392                enclosing.iZone = this.iZone;
393                enclosing.iOffset = this.iOffset;
394                enclosing.iSavedFields = this.iSavedFields;
395                if (this.iSavedFieldsCount < enclosing.iSavedFieldsCount) {
396                    // Since count is being restored to a lower count, the
397                    // potential exists for new saved fields to destroy data being
398                    // shared by another state. Set this flag such that the array
399                    // of saved fields is cloned prior to modification.
400                    enclosing.iSavedFieldsShared = true;
401                }
402                enclosing.iSavedFieldsCount = this.iSavedFieldsCount;
403                return true;
404            }
405        }
406        
407        static class SavedField implements Comparable {
408            final DateTimeField iField;
409            final int iValue;
410            final String iText;
411            final Locale iLocale;
412            
413            SavedField(DateTimeField field, int value) {
414                iField = field;
415                iValue = value;
416                iText = null;
417                iLocale = null;
418            }
419            
420            SavedField(DateTimeField field, String text, Locale locale) {
421                iField = field;
422                iValue = 0;
423                iText = text;
424                iLocale = locale;
425            }
426            
427            long set(long millis, boolean reset) {
428                if (iText == null) {
429                    millis = iField.set(millis, iValue);
430                } else {
431                    millis = iField.set(millis, iText, iLocale);
432                }
433                if (reset) {
434                    millis = iField.roundFloor(millis);
435                }
436                return millis;
437            }
438            
439            /**
440             * The field with the longer range duration is ordered first, where
441             * null is considered infinite. If the ranges match, then the field
442             * with the longer duration is ordered first.
443             */
444            public int compareTo(Object obj) {
445                DateTimeField other = ((SavedField)obj).iField;
446                int result = compareReverse
447                    (iField.getRangeDurationField(), other.getRangeDurationField());
448                if (result != 0) {
449                    return result;
450                }
451                return compareReverse
452                    (iField.getDurationField(), other.getDurationField());
453            }
454            
455            private int compareReverse(DurationField a, DurationField b) {
456                if (a == null || !a.isSupported()) {
457                    if (b == null || !b.isSupported()) {
458                        return 0;
459                    }
460                    return -1;
461                }
462                if (b == null || !b.isSupported()) {
463                    return 1;
464                }
465                return -a.compareTo(b);
466            }
467        }
468    }