1 /* 2 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * * Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * * Neither the name of JSR-310 nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package org.threeten.bp.format; 33 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Locale; 38 import java.util.Map; 39 40 import org.threeten.bp.Period; 41 import org.threeten.bp.ZoneId; 42 import org.threeten.bp.chrono.Chronology; 43 import org.threeten.bp.chrono.IsoChronology; 44 import org.threeten.bp.format.DateTimeFormatterBuilder.ReducedPrinterParser; 45 import org.threeten.bp.jdk8.DefaultInterfaceTemporalAccessor; 46 import org.threeten.bp.jdk8.Jdk8Methods; 47 import org.threeten.bp.temporal.TemporalField; 48 import org.threeten.bp.temporal.TemporalQueries; 49 import org.threeten.bp.temporal.TemporalQuery; 50 import org.threeten.bp.temporal.UnsupportedTemporalTypeException; 51 52 /** 53 * Context object used during date and time parsing. 54 * <p> 55 * This class represents the current state of the parse. 56 * It has the ability to store and retrieve the parsed values and manage optional segments. 57 * It also provides key information to the parsing methods. 58 * <p> 59 * Once parsing is complete, the {@link #toBuilder()} is typically used 60 * to obtain a builder that can combine the separate parsed fields into meaningful values. 61 * 62 * <h3>Specification for implementors</h3> 63 * This class is a mutable context intended for use from a single thread. 64 * Usage of the class is thread-safe within standard parsing as a new instance of this class 65 * is automatically created for each parse and parsing is single-threaded 66 */ 67 final class DateTimeParseContext { 68 69 /** 70 * The locale, not null. 71 */ 72 private Locale locale; 73 /** 74 * The symbols, not null. 75 */ 76 private DecimalStyle symbols; 77 /** 78 * The override chronology. 79 */ 80 private Chronology overrideChronology; 81 /** 82 * The override zone. 83 */ 84 private ZoneId overrideZone; 85 /** 86 * Whether to parse using case sensitively. 87 */ 88 private boolean caseSensitive = true; 89 /** 90 * Whether to parse using strict rules. 91 */ 92 private boolean strict = true; 93 /** 94 * The list of parsed data. 95 */ 96 private final ArrayList<Parsed> parsed = new ArrayList<Parsed>(); 97 98 /** 99 * Creates a new instance of the context. 100 * 101 * @param formatter the formatter controlling the parse, not null 102 */ DateTimeParseContext(DateTimeFormatter formatter)103 DateTimeParseContext(DateTimeFormatter formatter) { 104 super(); 105 this.locale = formatter.getLocale(); 106 this.symbols = formatter.getDecimalStyle(); 107 this.overrideChronology = formatter.getChronology(); 108 this.overrideZone = formatter.getZone(); 109 parsed.add(new Parsed()); 110 } 111 112 // for testing DateTimeParseContext(Locale locale, DecimalStyle symbols, Chronology chronology)113 DateTimeParseContext(Locale locale, DecimalStyle symbols, Chronology chronology) { 114 super(); 115 this.locale = locale; 116 this.symbols = symbols; 117 this.overrideChronology = chronology; 118 this.overrideZone = null; 119 parsed.add(new Parsed()); 120 } 121 DateTimeParseContext(DateTimeParseContext other)122 DateTimeParseContext(DateTimeParseContext other) { 123 super(); 124 this.locale = other.locale; 125 this.symbols = other.symbols; 126 this.overrideChronology = other.overrideChronology; 127 this.overrideZone = other.overrideZone; 128 this.caseSensitive = other.caseSensitive; 129 this.strict = other.strict; 130 parsed.add(new Parsed()); 131 } 132 133 /** 134 * Creates a copy of this context. 135 */ copy()136 DateTimeParseContext copy() { 137 return new DateTimeParseContext(this); 138 } 139 140 //----------------------------------------------------------------------- 141 /** 142 * Gets the locale. 143 * <p> 144 * This locale is used to control localization in the parse except 145 * where localization is controlled by the symbols. 146 * 147 * @return the locale, not null 148 */ getLocale()149 Locale getLocale() { 150 return locale; 151 } 152 153 /** 154 * Gets the formatting symbols. 155 * <p> 156 * The symbols control the localization of numeric parsing. 157 * 158 * @return the formatting symbols, not null 159 */ getSymbols()160 DecimalStyle getSymbols() { 161 return symbols; 162 } 163 164 /** 165 * Gets the effective chronology during parsing. 166 * 167 * @return the effective parsing chronology, not null 168 */ getEffectiveChronology()169 Chronology getEffectiveChronology() { 170 Chronology chrono = currentParsed().chrono; 171 if (chrono == null) { 172 chrono = overrideChronology; 173 if (chrono == null) { 174 chrono = IsoChronology.INSTANCE; 175 } 176 } 177 return chrono; 178 } 179 180 //----------------------------------------------------------------------- 181 /** 182 * Checks if parsing is case sensitive. 183 * 184 * @return true if parsing is case sensitive, false if case insensitive 185 */ isCaseSensitive()186 boolean isCaseSensitive() { 187 return caseSensitive; 188 } 189 190 /** 191 * Sets whether the parsing is case sensitive or not. 192 * 193 * @param caseSensitive changes the parsing to be case sensitive or not from now on 194 */ setCaseSensitive(boolean caseSensitive)195 void setCaseSensitive(boolean caseSensitive) { 196 this.caseSensitive = caseSensitive; 197 } 198 199 /** 200 * Helper to compare two {@code CharSequence} instances. 201 * This uses {@link #isCaseSensitive()}. 202 * 203 * @param cs1 the first character sequence, not null 204 * @param offset1 the offset into the first sequence, valid 205 * @param cs2 the second character sequence, not null 206 * @param offset2 the offset into the second sequence, valid 207 * @param length the length to check, valid 208 * @return true if equal 209 */ subSequenceEquals(CharSequence cs1, int offset1, CharSequence cs2, int offset2, int length)210 boolean subSequenceEquals(CharSequence cs1, int offset1, CharSequence cs2, int offset2, int length) { 211 if (offset1 + length > cs1.length() || offset2 + length > cs2.length()) { 212 return false; 213 } 214 if (isCaseSensitive()) { 215 for (int i = 0; i < length; i++) { 216 char ch1 = cs1.charAt(offset1 + i); 217 char ch2 = cs2.charAt(offset2 + i); 218 if (ch1 != ch2) { 219 return false; 220 } 221 } 222 } else { 223 for (int i = 0; i < length; i++) { 224 char ch1 = cs1.charAt(offset1 + i); 225 char ch2 = cs2.charAt(offset2 + i); 226 if (ch1 != ch2 && Character.toUpperCase(ch1) != Character.toUpperCase(ch2) && 227 Character.toLowerCase(ch1) != Character.toLowerCase(ch2)) { 228 return false; 229 } 230 } 231 } 232 return true; 233 } 234 235 /** 236 * Helper to compare two {@code char}. 237 * This uses {@link #isCaseSensitive()}. 238 * 239 * @param ch1 the first character 240 * @param ch2 the second character 241 * @return true if equal 242 */ charEquals(char ch1, char ch2)243 boolean charEquals(char ch1, char ch2) { 244 if (isCaseSensitive()) { 245 return ch1 == ch2; 246 } 247 return charEqualsIgnoreCase(ch1, ch2); 248 } 249 250 /** 251 * Compares two characters ignoring case. 252 * 253 * @param c1 the first 254 * @param c2 the second 255 * @return true if equal 256 */ charEqualsIgnoreCase(char c1, char c2)257 static boolean charEqualsIgnoreCase(char c1, char c2) { 258 return c1 == c2 || 259 Character.toUpperCase(c1) == Character.toUpperCase(c2) || 260 Character.toLowerCase(c1) == Character.toLowerCase(c2); 261 } 262 263 //----------------------------------------------------------------------- 264 /** 265 * Checks if parsing is strict. 266 * <p> 267 * Strict parsing requires exact matching of the text and sign styles. 268 * 269 * @return true if parsing is strict, false if lenient 270 */ isStrict()271 boolean isStrict() { 272 return strict; 273 } 274 275 /** 276 * Sets whether parsing is strict or lenient. 277 * 278 * @param strict changes the parsing to be strict or lenient from now on 279 */ setStrict(boolean strict)280 void setStrict(boolean strict) { 281 this.strict = strict; 282 } 283 284 //----------------------------------------------------------------------- 285 /** 286 * Starts the parsing of an optional segment of the input. 287 */ startOptional()288 void startOptional() { 289 parsed.add(currentParsed().copy()); 290 } 291 292 /** 293 * Ends the parsing of an optional segment of the input. 294 * 295 * @param successful whether the optional segment was successfully parsed 296 */ endOptional(boolean successful)297 void endOptional(boolean successful) { 298 if (successful) { 299 parsed.remove(parsed.size() - 2); 300 } else { 301 parsed.remove(parsed.size() - 1); 302 } 303 } 304 305 //----------------------------------------------------------------------- 306 /** 307 * Gets the currently active temporal objects. 308 * 309 * @return the current temporal objects, not null 310 */ currentParsed()311 private Parsed currentParsed() { 312 return parsed.get(parsed.size() - 1); 313 } 314 315 //----------------------------------------------------------------------- 316 /** 317 * Gets the first value that was parsed for the specified field. 318 * <p> 319 * This searches the results of the parse, returning the first value found 320 * for the specified field. No attempt is made to derive a value. 321 * The field may have an out of range value. 322 * For example, the day-of-month might be set to 50, or the hour to 1000. 323 * 324 * @param field the field to query from the map, null returns null 325 * @return the value mapped to the specified field, null if field was not parsed 326 */ getParsed(TemporalField field)327 Long getParsed(TemporalField field) { 328 return currentParsed().fieldValues.get(field); 329 } 330 331 /** 332 * Stores the parsed field. 333 * <p> 334 * This stores a field-value pair that has been parsed. 335 * The value stored may be out of range for the field - no checks are performed. 336 * 337 * @param field the field to set in the field-value map, not null 338 * @param value the value to set in the field-value map 339 * @param errorPos the position of the field being parsed 340 * @param successPos the position after the field being parsed 341 * @return the new position 342 */ setParsedField(TemporalField field, long value, int errorPos, int successPos)343 int setParsedField(TemporalField field, long value, int errorPos, int successPos) { 344 Jdk8Methods.requireNonNull(field, "field"); 345 Long old = currentParsed().fieldValues.put(field, value); 346 return (old != null && old.longValue() != value) ? ~errorPos : successPos; 347 } 348 349 /** 350 * Stores the parsed chronology. 351 * <p> 352 * This stores the chronology that has been parsed. 353 * No validation is performed other than ensuring it is not null. 354 * 355 * @param chrono the parsed chronology, not null 356 */ setParsed(Chronology chrono)357 void setParsed(Chronology chrono) { 358 Jdk8Methods.requireNonNull(chrono, "chrono"); 359 Parsed currentParsed = currentParsed(); 360 currentParsed.chrono = chrono; 361 if (currentParsed.callbacks != null) { 362 List<Object[]> callbacks = new ArrayList<Object[]>(currentParsed.callbacks); 363 currentParsed.callbacks.clear(); 364 for (Object[] objects : callbacks) { 365 ReducedPrinterParser pp = (ReducedPrinterParser) objects[0]; 366 pp.setValue(this, (Long) objects[1], (Integer) objects[2], (Integer) objects[3]); 367 } 368 } 369 } 370 addChronologyChangedParser(ReducedPrinterParser reducedPrinterParser, long value, int errorPos, int successPos)371 void addChronologyChangedParser(ReducedPrinterParser reducedPrinterParser, long value, int errorPos, int successPos) { 372 Parsed currentParsed = currentParsed(); 373 if (currentParsed.callbacks == null) { 374 currentParsed.callbacks = new ArrayList<Object[]>(2); 375 } 376 currentParsed.callbacks.add(new Object[] {reducedPrinterParser, value, errorPos, successPos}); 377 } 378 379 /** 380 * Stores the parsed zone. 381 * <p> 382 * This stores the zone that has been parsed. 383 * No validation is performed other than ensuring it is not null. 384 * 385 * @param zone the parsed zone, not null 386 */ setParsed(ZoneId zone)387 void setParsed(ZoneId zone) { 388 Jdk8Methods.requireNonNull(zone, "zone"); 389 currentParsed().zone = zone; 390 } 391 392 /** 393 * Stores the leap second. 394 */ setParsedLeapSecond()395 void setParsedLeapSecond() { 396 currentParsed().leapSecond = true; 397 } 398 399 //----------------------------------------------------------------------- 400 /** 401 * Returns a {@code TemporalAccessor} that can be used to interpret 402 * the results of the parse. 403 * 404 * @return an accessor with the results of the parse, not null 405 */ toParsed()406 Parsed toParsed() { 407 return currentParsed(); 408 } 409 410 //----------------------------------------------------------------------- 411 /** 412 * Returns a string version of the context for debugging. 413 * 414 * @return a string representation of the context data, not null 415 */ 416 @Override toString()417 public String toString() { 418 return currentParsed().toString(); 419 } 420 421 //----------------------------------------------------------------------- 422 /** 423 * Temporary store of parsed data. 424 */ 425 final class Parsed extends DefaultInterfaceTemporalAccessor { 426 Chronology chrono = null; 427 ZoneId zone = null; 428 final Map<TemporalField, Long> fieldValues = new HashMap<TemporalField, Long>(); 429 boolean leapSecond; 430 Period excessDays = Period.ZERO; 431 List<Object[]> callbacks; 432 Parsed()433 private Parsed() { 434 } copy()435 protected Parsed copy() { 436 Parsed cloned = new Parsed(); 437 cloned.chrono = this.chrono; 438 cloned.zone = this.zone; 439 cloned.fieldValues.putAll(this.fieldValues); 440 cloned.leapSecond = this.leapSecond; 441 return cloned; 442 } 443 @Override toString()444 public String toString() { 445 return fieldValues.toString() + "," + chrono + "," + zone; 446 } 447 @Override isSupported(TemporalField field)448 public boolean isSupported(TemporalField field) { 449 return fieldValues.containsKey(field); 450 } 451 @Override get(TemporalField field)452 public int get(TemporalField field) { 453 if (fieldValues.containsKey(field) == false) { 454 throw new UnsupportedTemporalTypeException("Unsupported field: " + field); 455 } 456 long value = fieldValues.get(field); 457 return Jdk8Methods.safeToInt(value); 458 } 459 @Override getLong(TemporalField field)460 public long getLong(TemporalField field) { 461 if (fieldValues.containsKey(field) == false) { 462 throw new UnsupportedTemporalTypeException("Unsupported field: " + field); 463 } 464 return fieldValues.get(field); 465 } 466 @SuppressWarnings("unchecked") 467 @Override query(TemporalQuery<R> query)468 public <R> R query(TemporalQuery<R> query) { 469 if (query == TemporalQueries.chronology()) { 470 return (R) chrono; 471 } 472 if (query == TemporalQueries.zoneId() || query == TemporalQueries.zone()) { 473 return (R) zone; 474 } 475 return super.query(query); 476 } 477 478 /** 479 * Returns a {@code DateTimeBuilder} that can be used to interpret 480 * the results of the parse. 481 * <p> 482 * This method is typically used once parsing is complete to obtain the parsed data. 483 * Parsing will typically result in separate fields, such as year, month and day. 484 * The returned builder can be used to combine the parsed data into meaningful 485 * objects such as {@code LocalDate}, potentially applying complex processing 486 * to handle invalid parsed data. 487 * 488 * @return a new builder with the results of the parse, not null 489 */ toBuilder()490 DateTimeBuilder toBuilder() { 491 DateTimeBuilder builder = new DateTimeBuilder(); 492 builder.fieldValues.putAll(fieldValues); 493 builder.chrono = getEffectiveChronology(); 494 if (zone != null) { 495 builder.zone = zone; 496 } else { 497 builder.zone = overrideZone; 498 } 499 builder.leapSecond = leapSecond; 500 builder.excessDays = excessDays; 501 return builder; 502 } 503 } 504 505 //------------------------------------------------------------------------- 506 // for testing 507 /** 508 * Sets the locale. 509 * <p> 510 * This locale is used to control localization in the print output except 511 * where localization is controlled by the symbols. 512 * 513 * @param locale the locale, not null 514 */ setLocale(Locale locale)515 void setLocale(Locale locale) { 516 Jdk8Methods.requireNonNull(locale, "locale"); 517 this.locale = locale; 518 } 519 520 } 521