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 }