001 /* 002 * Copyright 2001-2013 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 import org.joda.time.IllegalInstantException; 030 031 /** 032 * DateTimeParserBucket is an advanced class, intended mainly for parser 033 * implementations. It can also be used during normal parsing operations to 034 * capture more information about the parse. 035 * <p> 036 * This class allows fields to be saved in any order, but be physically set in 037 * a consistent order. This is useful for parsing against formats that allow 038 * field values to contradict each other. 039 * <p> 040 * Field values are applied in an order where the "larger" fields are set 041 * first, making their value less likely to stick. A field is larger than 042 * another when it's range duration is longer. If both ranges are the same, 043 * then the larger field has the longer duration. If it cannot be determined 044 * which field is larger, then the fields are set in the order they were saved. 045 * <p> 046 * For example, these fields were saved in this order: dayOfWeek, monthOfYear, 047 * dayOfMonth, dayOfYear. When computeMillis is called, the fields are set in 048 * this order: monthOfYear, dayOfYear, dayOfMonth, dayOfWeek. 049 * <p> 050 * DateTimeParserBucket is mutable and not thread-safe. 051 * 052 * @author Brian S O'Neill 053 * @author Fredrik Borgh 054 * @since 1.0 055 */ 056 public class DateTimeParserBucket { 057 058 /** The chronology to use for parsing. */ 059 private final Chronology iChrono; 060 private final long iMillis; 061 062 /** The parsed zone, initialised to formatter zone. */ 063 private DateTimeZone iZone; 064 /** The parsed offset. */ 065 private Integer iOffset; 066 /** The locale to use for parsing. */ 067 private Locale iLocale; 068 /** Used for parsing two-digit years. */ 069 private Integer iPivotYear; 070 /** Used for parsing month/day without year. */ 071 private int iDefaultYear; 072 073 private SavedField[] iSavedFields = new SavedField[8]; 074 private int iSavedFieldsCount; 075 private boolean iSavedFieldsShared; 076 077 private Object iSavedState; 078 079 /** 080 * Constructs a bucket. 081 * 082 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time 083 * @param chrono the chronology to use 084 * @param locale the locale to use 085 * @deprecated Use longer constructor 086 */ 087 @Deprecated 088 public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale) { 089 this(instantLocal, chrono, locale, null, 2000); 090 } 091 092 /** 093 * Constructs a bucket, with the option of specifying the pivot year for 094 * two-digit year parsing. 095 * 096 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time 097 * @param chrono the chronology to use 098 * @param locale the locale to use 099 * @param pivotYear the pivot year to use when parsing two-digit years 100 * @since 1.1 101 * @deprecated Use longer constructor 102 */ 103 @Deprecated 104 public DateTimeParserBucket(long instantLocal, Chronology chrono, Locale locale, Integer pivotYear) { 105 this(instantLocal, chrono, locale, pivotYear, 2000); 106 } 107 108 /** 109 * Constructs a bucket, with the option of specifying the pivot year for 110 * two-digit year parsing. 111 * 112 * @param instantLocal the initial millis from 1970-01-01T00:00:00, local time 113 * @param chrono the chronology to use 114 * @param locale the locale to use 115 * @param pivotYear the pivot year to use when parsing two-digit years 116 * @since 2.0 117 */ 118 public DateTimeParserBucket(long instantLocal, Chronology chrono, 119 Locale locale, Integer pivotYear, int defaultYear) { 120 super(); 121 chrono = DateTimeUtils.getChronology(chrono); 122 iMillis = instantLocal; 123 iZone = chrono.getZone(); 124 iChrono = chrono.withUTC(); 125 iLocale = (locale == null ? Locale.getDefault() : locale); 126 iPivotYear = pivotYear; 127 iDefaultYear = defaultYear; 128 } 129 130 //----------------------------------------------------------------------- 131 /** 132 * Gets the chronology of the bucket, which will be a local (UTC) chronology. 133 */ 134 public Chronology getChronology() { 135 return iChrono; 136 } 137 138 //----------------------------------------------------------------------- 139 /** 140 * Returns the locale to be used during parsing. 141 * 142 * @return the locale to use 143 */ 144 public Locale getLocale() { 145 return iLocale; 146 } 147 148 //----------------------------------------------------------------------- 149 /** 150 * Returns the time zone used by computeMillis. 151 */ 152 public DateTimeZone getZone() { 153 return iZone; 154 } 155 156 /** 157 * Set a time zone to be used when computeMillis is called. 158 */ 159 public void setZone(DateTimeZone zone) { 160 iSavedState = null; 161 iZone = zone; 162 } 163 164 //----------------------------------------------------------------------- 165 /** 166 * Returns the time zone offset in milliseconds used by computeMillis. 167 * @deprecated use Integer version 168 */ 169 @Deprecated 170 public int getOffset() { 171 return (iOffset != null ? iOffset : 0); 172 } 173 174 /** 175 * Returns the time zone offset in milliseconds used by computeMillis. 176 */ 177 public Integer getOffsetInteger() { 178 return iOffset; 179 } 180 181 /** 182 * Set a time zone offset to be used when computeMillis is called. 183 * @deprecated use Integer version 184 */ 185 @Deprecated 186 public void setOffset(int offset) { 187 iSavedState = null; 188 iOffset = offset; 189 } 190 191 /** 192 * Set a time zone offset to be used when computeMillis is called. 193 */ 194 public void setOffset(Integer offset) { 195 iSavedState = null; 196 iOffset = offset; 197 } 198 199 //----------------------------------------------------------------------- 200 /** 201 * Returns the default year used when information is incomplete. 202 * <p> 203 * This is used for two-digit years and when the largest parsed field is 204 * months or days. 205 * <p> 206 * A null value for two-digit years means to use the value from DateTimeFormatterBuilder. 207 * A null value for month/day only parsing will cause the default of 2000 to be used. 208 * 209 * @return Integer value of the pivot year, null if not set 210 * @since 1.1 211 */ 212 public Integer getPivotYear() { 213 return iPivotYear; 214 } 215 216 /** 217 * Sets the pivot year to use when parsing two digit years. 218 * <p> 219 * If the value is set to null, this will indicate that default 220 * behaviour should be used. 221 * 222 * @param pivotYear the pivot year to use 223 * @since 1.1 224 */ 225 public void setPivotYear(Integer pivotYear) { 226 iPivotYear = pivotYear; 227 } 228 229 //----------------------------------------------------------------------- 230 /** 231 * Saves a datetime field value. 232 * 233 * @param field the field, whose chronology must match that of this bucket 234 * @param value the value 235 */ 236 public void saveField(DateTimeField field, int value) { 237 saveField(new SavedField(field, value)); 238 } 239 240 /** 241 * Saves a datetime field value. 242 * 243 * @param fieldType the field type 244 * @param value the value 245 */ 246 public void saveField(DateTimeFieldType fieldType, int value) { 247 saveField(new SavedField(fieldType.getField(iChrono), value)); 248 } 249 250 /** 251 * Saves a datetime field text value. 252 * 253 * @param fieldType the field type 254 * @param text the text value 255 * @param locale the locale to use 256 */ 257 public void saveField(DateTimeFieldType fieldType, String text, Locale locale) { 258 saveField(new SavedField(fieldType.getField(iChrono), text, locale)); 259 } 260 261 private void saveField(SavedField field) { 262 SavedField[] savedFields = iSavedFields; 263 int savedFieldsCount = iSavedFieldsCount; 264 265 if (savedFieldsCount == savedFields.length || iSavedFieldsShared) { 266 // Expand capacity or merely copy if saved fields are shared. 267 SavedField[] newArray = new SavedField 268 [savedFieldsCount == savedFields.length ? savedFieldsCount * 2 : savedFields.length]; 269 System.arraycopy(savedFields, 0, newArray, 0, savedFieldsCount); 270 iSavedFields = savedFields = newArray; 271 iSavedFieldsShared = false; 272 } 273 274 iSavedState = null; 275 savedFields[savedFieldsCount] = field; 276 iSavedFieldsCount = savedFieldsCount + 1; 277 } 278 279 /** 280 * Saves the state of this bucket, returning it in an opaque object. Call 281 * restoreState to undo any changes that were made since the state was 282 * saved. Calls to saveState may be nested. 283 * 284 * @return opaque saved state, which may be passed to restoreState 285 */ 286 public Object saveState() { 287 if (iSavedState == null) { 288 iSavedState = new SavedState(); 289 } 290 return iSavedState; 291 } 292 293 /** 294 * Restores the state of this bucket from a previously saved state. The 295 * state object passed into this method is not consumed, and it can be used 296 * later to restore to that state again. 297 * 298 * @param savedState opaque saved state, returned from saveState 299 * @return true state object is valid and state restored 300 */ 301 public boolean restoreState(Object savedState) { 302 if (savedState instanceof SavedState) { 303 if (((SavedState) savedState).restoreState(this)) { 304 iSavedState = savedState; 305 return true; 306 } 307 } 308 return false; 309 } 310 311 /** 312 * Computes the parsed datetime by setting the saved fields. 313 * This method is idempotent, but it is not thread-safe. 314 * 315 * @return milliseconds since 1970-01-01T00:00:00Z 316 * @throws IllegalArgumentException if any field is out of range 317 */ 318 public long computeMillis() { 319 return computeMillis(false, null); 320 } 321 322 /** 323 * Computes the parsed datetime by setting the saved fields. 324 * This method is idempotent, but it is not thread-safe. 325 * 326 * @param resetFields false by default, but when true, unsaved field values are cleared 327 * @return milliseconds since 1970-01-01T00:00:00Z 328 * @throws IllegalArgumentException if any field is out of range 329 */ 330 public long computeMillis(boolean resetFields) { 331 return computeMillis(resetFields, null); 332 } 333 334 /** 335 * Computes the parsed datetime by setting the saved fields. 336 * This method is idempotent, but it is not thread-safe. 337 * 338 * @param resetFields false by default, but when true, unsaved field values are cleared 339 * @param text optional text being parsed, to be included in any error message 340 * @return milliseconds since 1970-01-01T00:00:00Z 341 * @throws IllegalArgumentException if any field is out of range 342 * @since 1.3 343 */ 344 public long computeMillis(boolean resetFields, String text) { 345 SavedField[] savedFields = iSavedFields; 346 int count = iSavedFieldsCount; 347 if (iSavedFieldsShared) { 348 iSavedFields = savedFields = (SavedField[])iSavedFields.clone(); 349 iSavedFieldsShared = false; 350 } 351 sort(savedFields, count); 352 if (count > 0) { 353 // alter base year for parsing if first field is month or day 354 DurationField months = DurationFieldType.months().getField(iChrono); 355 DurationField days = DurationFieldType.days().getField(iChrono); 356 DurationField first = savedFields[0].iField.getDurationField(); 357 if (compareReverse(first, months) >= 0 && compareReverse(first, days) <= 0) { 358 saveField(DateTimeFieldType.year(), iDefaultYear); 359 return computeMillis(resetFields, text); 360 } 361 } 362 363 long millis = iMillis; 364 try { 365 for (int i = 0; i < count; i++) { 366 millis = savedFields[i].set(millis, resetFields); 367 } 368 if (resetFields) { 369 for (int i = 0; i < count; i++) { 370 millis = savedFields[i].set(millis, i == (count - 1)); 371 } 372 } 373 } catch (IllegalFieldValueException e) { 374 if (text != null) { 375 e.prependMessage("Cannot parse \"" + text + '"'); 376 } 377 throw e; 378 } 379 380 if (iOffset != null) { 381 millis -= iOffset; 382 } else if (iZone != null) { 383 int offset = iZone.getOffsetFromLocal(millis); 384 millis -= offset; 385 if (offset != iZone.getOffset(millis)) { 386 String message = "Illegal instant due to time zone offset transition (" + iZone + ')'; 387 if (text != null) { 388 message = "Cannot parse \"" + text + "\": " + message; 389 } 390 throw new IllegalInstantException(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 }