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