1 | /* |
2 | * Copyright 2001-2006 Stephen Colebourne |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | package org.joda.time.format; |
17 | |
18 | import java.util.Arrays; |
19 | import java.util.Locale; |
20 | |
21 | import org.joda.time.Chronology; |
22 | import org.joda.time.DateTimeField; |
23 | import org.joda.time.DateTimeFieldType; |
24 | import org.joda.time.DateTimeUtils; |
25 | import org.joda.time.DateTimeZone; |
26 | import org.joda.time.DurationField; |
27 | import org.joda.time.IllegalFieldValueException; |
28 | |
29 | /** |
30 | * DateTimeParserBucket is an advanced class, intended mainly for parser |
31 | * implementations. It can also be used during normal parsing operations to |
32 | * capture more information about the parse. |
33 | * <p> |
34 | * This class allows fields to be saved in any order, but be physically set in |
35 | * a consistent order. This is useful for parsing against formats that allow |
36 | * field values to contradict each other. |
37 | * <p> |
38 | * Field values are applied in an order where the "larger" fields are set |
39 | * first, making their value less likely to stick. A field is larger than |
40 | * another when it's range duration is longer. If both ranges are the same, |
41 | * then the larger field has the longer duration. If it cannot be determined |
42 | * which field is larger, then the fields are set in the order they were saved. |
43 | * <p> |
44 | * For example, these fields were saved in this order: dayOfWeek, monthOfYear, |
45 | * dayOfMonth, dayOfYear. When computeMillis is called, the fields are set in |
46 | * this order: monthOfYear, dayOfYear, dayOfMonth, dayOfWeek. |
47 | * <p> |
48 | * DateTimeParserBucket is mutable and not thread-safe. |
49 | * |
50 | * @author Brian S O'Neill |
51 | * @author Fredrik Borgh |
52 | * @since 1.0 |
53 | */ |
54 | public class DateTimeParserBucket { |
55 | |
56 | /** The chronology to use for parsing. */ |
57 | private final Chronology iChrono; |
58 | private final long iMillis; |
59 | |
60 | // TimeZone to switch to in computeMillis. If null, use offset. |
61 | private DateTimeZone iZone; |
62 | private int iOffset; |
63 | /** The locale to use for parsing. */ |
64 | private Locale iLocale; |
65 | /** Used for parsing two-digit years. */ |
66 | private Integer iPivotYear; |
67 | |
68 | private SavedField[] iSavedFields = new SavedField[8]; |
69 | private int iSavedFieldsCount; |
70 | private boolean iSavedFieldsShared; |
71 | |
72 | private Object iSavedState; |
73 | |
74 | /** |
75 | * Constucts a bucket. |
76 | * |
77 | * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time |
78 | * @param chrono the chronology to use |
79 | * @param locale the locale to use |
80 | */ |
81 | public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale) { |
82 | this(instantLocal, chrono, locale, null); |
83 | } |
84 | |
85 | /** |
86 | * Constucts a bucket, with the option of specifying the pivot year for |
87 | * two-digit year parsing. |
88 | * |
89 | * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time |
90 | * @param chrono the chronology to use |
91 | * @param locale the locale to use |
92 | * @param pivotYear the pivot year to use when parsing two-digit years |
93 | * @since 1.1 |
94 | */ |
95 | public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale, Integer pivotYear) { |
96 | super(); |
97 | chrono = DateTimeUtils.getChronology(chrono); |
98 | iMillis = instantLocal; |
99 | 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 | } |