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 */ 016package org.joda.time.format; 017 018import java.util.Arrays; 019import java.util.Locale; 020 021import org.joda.time.Chronology; 022import org.joda.time.DateTimeField; 023import org.joda.time.DateTimeFieldType; 024import org.joda.time.DateTimeUtils; 025import org.joda.time.DateTimeZone; 026import org.joda.time.DurationField; 027import 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 */ 054public 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}