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