1 /* 2 * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 /* 27 * This file is available under and governed by the GNU General Public 28 * License version 2 only, as published by the Free Software Foundation. 29 * However, the following notice accompanied the original version of this 30 * file: 31 * 32 * Copyright (c) 2008-2012, Stephen Colebourne & Michael Nascimento Santos 33 * 34 * All rights hg qreserved. 35 * 36 * Redistribution and use in source and binary forms, with or without 37 * modification, are permitted provided that the following conditions are met: 38 * 39 * * Redistributions of source code must retain the above copyright notice, 40 * this list of conditions and the following disclaimer. 41 * 42 * * Redistributions in binary form must reproduce the above copyright notice, 43 * this list of conditions and the following disclaimer in the documentation 44 * and/or other materials provided with the distribution. 45 * 46 * * Neither the name of JSR-310 nor the names of its contributors 47 * may be used to endorse or promote products derived from this software 48 * without specific prior written permission. 49 * 50 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 51 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 52 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 53 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 54 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 55 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 56 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 57 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 58 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 59 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 60 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 61 */ 62 package java.time.format; 63 64 import android.icu.text.LocaleDisplayNames; 65 import android.icu.text.TimeZoneFormat; 66 import android.icu.text.TimeZoneNames; 67 import android.icu.util.Calendar; 68 import android.icu.util.ULocale; 69 70 import static java.time.temporal.ChronoField.DAY_OF_MONTH; 71 import static java.time.temporal.ChronoField.HOUR_OF_DAY; 72 import static java.time.temporal.ChronoField.INSTANT_SECONDS; 73 import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; 74 import static java.time.temporal.ChronoField.MONTH_OF_YEAR; 75 import static java.time.temporal.ChronoField.NANO_OF_SECOND; 76 import static java.time.temporal.ChronoField.OFFSET_SECONDS; 77 import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; 78 import static java.time.temporal.ChronoField.YEAR; 79 80 import com.android.icu.util.ExtendedCalendar; 81 82 import libcore.icu.ICU; 83 84 import java.lang.ref.SoftReference; 85 import java.math.BigDecimal; 86 import java.math.BigInteger; 87 import java.math.RoundingMode; 88 import java.text.ParsePosition; 89 import java.time.DateTimeException; 90 import java.time.Instant; 91 import java.time.LocalDate; 92 import java.time.LocalDateTime; 93 import java.time.ZoneId; 94 import java.time.ZoneOffset; 95 import java.time.chrono.ChronoLocalDate; 96 import java.time.chrono.Chronology; 97 import java.time.chrono.IsoChronology; 98 import java.time.format.DateTimeTextProvider.LocaleStore; 99 import java.time.temporal.ChronoField; 100 import java.time.temporal.IsoFields; 101 import java.time.temporal.TemporalAccessor; 102 import java.time.temporal.TemporalField; 103 import java.time.temporal.TemporalQueries; 104 import java.time.temporal.TemporalQuery; 105 import java.time.temporal.ValueRange; 106 import java.time.temporal.WeekFields; 107 import java.time.zone.ZoneRulesProvider; 108 import java.util.AbstractMap.SimpleImmutableEntry; 109 import java.util.ArrayList; 110 import java.util.Collections; 111 import java.util.Comparator; 112 import java.util.HashMap; 113 import java.util.HashSet; 114 import java.util.Iterator; 115 import java.util.LinkedHashMap; 116 import java.util.List; 117 import java.util.Locale; 118 import java.util.Map; 119 import java.util.Map.Entry; 120 import java.util.Objects; 121 import java.util.Set; 122 import java.util.TimeZone; 123 import java.util.concurrent.ConcurrentHashMap; 124 import java.util.concurrent.ConcurrentMap; 125 126 /** 127 * Builder to create date-time formatters. 128 * <p> 129 * This allows a {@code DateTimeFormatter} to be created. 130 * All date-time formatters are created ultimately using this builder. 131 * <p> 132 * The basic elements of date-time can all be added: 133 * <ul> 134 * <li>Value - a numeric value</li> 135 * <li>Fraction - a fractional value including the decimal place. Always use this when 136 * outputting fractions to ensure that the fraction is parsed correctly</li> 137 * <li>Text - the textual equivalent for the value</li> 138 * <li>OffsetId/Offset - the {@linkplain ZoneOffset zone offset}</li> 139 * <li>ZoneId - the {@linkplain ZoneId time-zone} id</li> 140 * <li>ZoneText - the name of the time-zone</li> 141 * <li>ChronologyId - the {@linkplain Chronology chronology} id</li> 142 * <li>ChronologyText - the name of the chronology</li> 143 * <li>Literal - a text literal</li> 144 * <li>Nested and Optional - formats can be nested or made optional</li> 145 * </ul> 146 * In addition, any of the elements may be decorated by padding, either with spaces or any other character. 147 * <p> 148 * Finally, a shorthand pattern, mostly compatible with {@code java.text.SimpleDateFormat SimpleDateFormat} 149 * can be used, see {@link #appendPattern(String)}. 150 * In practice, this simply parses the pattern and calls other methods on the builder. 151 * 152 * @implSpec 153 * This class is a mutable builder intended for use from a single thread. 154 * 155 * @since 1.8 156 */ 157 public final class DateTimeFormatterBuilder { 158 159 /** 160 * Query for a time-zone that is region-only. 161 */ 162 private static final TemporalQuery<ZoneId> QUERY_REGION_ONLY = (temporal) -> { 163 ZoneId zone = temporal.query(TemporalQueries.zoneId()); 164 return (zone != null && zone instanceof ZoneOffset == false ? zone : null); 165 }; 166 167 /** 168 * The currently active builder, used by the outermost builder. 169 */ 170 private DateTimeFormatterBuilder active = this; 171 /** 172 * The parent builder, null for the outermost builder. 173 */ 174 private final DateTimeFormatterBuilder parent; 175 /** 176 * The list of printers that will be used. 177 */ 178 private final List<DateTimePrinterParser> printerParsers = new ArrayList<>(); 179 /** 180 * Whether this builder produces an optional formatter. 181 */ 182 private final boolean optional; 183 /** 184 * The width to pad the next field to. 185 */ 186 private int padNextWidth; 187 /** 188 * The character to pad the next field with. 189 */ 190 private char padNextChar; 191 /** 192 * The index of the last variable width value parser. 193 */ 194 private int valueParserIndex = -1; 195 196 /** 197 * Gets the formatting pattern for date and time styles for a locale and chronology. 198 * The locale and chronology are used to lookup the locale specific format 199 * for the requested dateStyle and/or timeStyle. 200 * 201 * @param dateStyle the FormatStyle for the date, null for time-only pattern 202 * @param timeStyle the FormatStyle for the time, null for date-only pattern 203 * @param chrono the Chronology, non-null 204 * @param locale the locale, non-null 205 * @return the locale and Chronology specific formatting pattern 206 * @throws IllegalArgumentException if both dateStyle and timeStyle are null 207 */ getLocalizedDateTimePattern(FormatStyle dateStyle, FormatStyle timeStyle, Chronology chrono, Locale locale)208 public static String getLocalizedDateTimePattern(FormatStyle dateStyle, FormatStyle timeStyle, 209 Chronology chrono, Locale locale) { 210 Objects.requireNonNull(locale, "locale"); 211 Objects.requireNonNull(chrono, "chrono"); 212 if (dateStyle == null && timeStyle == null) { 213 throw new IllegalArgumentException("Either dateStyle or timeStyle must be non-null"); 214 } 215 216 // BEGIN Android-changed: get format string from ICU. 217 // LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased() 218 // .getLocaleResources(locale); 219 // String pattern = lr.getJavaTimeDateTimePattern( 220 // convertStyle(timeStyle), convertStyle(dateStyle), chrono.getCalendarType()); 221 ExtendedCalendar extendedCalendar = ICU.getExtendedCalendar(locale, 222 chrono.getCalendarType()); 223 String pattern = extendedCalendar.getDateTimePattern(convertStyle(dateStyle), 224 convertStyle(timeStyle)); 225 // Transform the pattern coming from ICU because DateTimeFormatter does not handle some date 226 // symbols, e.g. 'B' / 'b', and thus we use a heuristic algorithm to remove the symbol. 227 // See http://b/174804526. 228 pattern = ICU.transformIcuDateTimePattern_forJavaTime(pattern); 229 // END Android-changed: get format string from ICU. 230 return pattern; 231 } 232 233 /** 234 * Converts the given FormatStyle to the java.text.DateFormat style. 235 * 236 * @param style the FormatStyle style 237 * @return the int style, or -1 if style is null, indicating un-required 238 */ convertStyle(FormatStyle style)239 private static int convertStyle(FormatStyle style) { 240 if (style == null) { 241 return -1; 242 } 243 return style.ordinal(); // indices happen to align 244 } 245 246 /** 247 * Constructs a new instance of the builder. 248 */ DateTimeFormatterBuilder()249 public DateTimeFormatterBuilder() { 250 super(); 251 parent = null; 252 optional = false; 253 } 254 255 /** 256 * Constructs a new instance of the builder. 257 * 258 * @param parent the parent builder, not null 259 * @param optional whether the formatter is optional, not null 260 */ DateTimeFormatterBuilder(DateTimeFormatterBuilder parent, boolean optional)261 private DateTimeFormatterBuilder(DateTimeFormatterBuilder parent, boolean optional) { 262 super(); 263 this.parent = parent; 264 this.optional = optional; 265 } 266 267 //----------------------------------------------------------------------- 268 /** 269 * Changes the parse style to be case sensitive for the remainder of the formatter. 270 * <p> 271 * Parsing can be case sensitive or insensitive - by default it is case sensitive. 272 * This method allows the case sensitivity setting of parsing to be changed. 273 * <p> 274 * Calling this method changes the state of the builder such that all 275 * subsequent builder method calls will parse text in case sensitive mode. 276 * See {@link #parseCaseInsensitive} for the opposite setting. 277 * The parse case sensitive/insensitive methods may be called at any point 278 * in the builder, thus the parser can swap between case parsing modes 279 * multiple times during the parse. 280 * <p> 281 * Since the default is case sensitive, this method should only be used after 282 * a previous call to {@code #parseCaseInsensitive}. 283 * 284 * @return this, for chaining, not null 285 */ parseCaseSensitive()286 public DateTimeFormatterBuilder parseCaseSensitive() { 287 appendInternal(SettingsParser.SENSITIVE); 288 return this; 289 } 290 291 /** 292 * Changes the parse style to be case insensitive for the remainder of the formatter. 293 * <p> 294 * Parsing can be case sensitive or insensitive - by default it is case sensitive. 295 * This method allows the case sensitivity setting of parsing to be changed. 296 * <p> 297 * Calling this method changes the state of the builder such that all 298 * subsequent builder method calls will parse text in case insensitive mode. 299 * See {@link #parseCaseSensitive()} for the opposite setting. 300 * The parse case sensitive/insensitive methods may be called at any point 301 * in the builder, thus the parser can swap between case parsing modes 302 * multiple times during the parse. 303 * 304 * @return this, for chaining, not null 305 */ parseCaseInsensitive()306 public DateTimeFormatterBuilder parseCaseInsensitive() { 307 appendInternal(SettingsParser.INSENSITIVE); 308 return this; 309 } 310 311 //----------------------------------------------------------------------- 312 /** 313 * Changes the parse style to be strict for the remainder of the formatter. 314 * <p> 315 * Parsing can be strict or lenient - by default its strict. 316 * This controls the degree of flexibility in matching the text and sign styles. 317 * <p> 318 * When used, this method changes the parsing to be strict from this point onwards. 319 * As strict is the default, this is normally only needed after calling {@link #parseLenient()}. 320 * The change will remain in force until the end of the formatter that is eventually 321 * constructed or until {@code parseLenient} is called. 322 * 323 * @return this, for chaining, not null 324 */ parseStrict()325 public DateTimeFormatterBuilder parseStrict() { 326 appendInternal(SettingsParser.STRICT); 327 return this; 328 } 329 330 /** 331 * Changes the parse style to be lenient for the remainder of the formatter. 332 * Note that case sensitivity is set separately to this method. 333 * <p> 334 * Parsing can be strict or lenient - by default its strict. 335 * This controls the degree of flexibility in matching the text and sign styles. 336 * Applications calling this method should typically also call {@link #parseCaseInsensitive()}. 337 * <p> 338 * When used, this method changes the parsing to be lenient from this point onwards. 339 * The change will remain in force until the end of the formatter that is eventually 340 * constructed or until {@code parseStrict} is called. 341 * 342 * @return this, for chaining, not null 343 */ parseLenient()344 public DateTimeFormatterBuilder parseLenient() { 345 appendInternal(SettingsParser.LENIENT); 346 return this; 347 } 348 349 //----------------------------------------------------------------------- 350 /** 351 * Appends a default value for a field to the formatter for use in parsing. 352 * <p> 353 * This appends an instruction to the builder to inject a default value 354 * into the parsed result. This is especially useful in conjunction with 355 * optional parts of the formatter. 356 * <p> 357 * For example, consider a formatter that parses the year, followed by 358 * an optional month, with a further optional day-of-month. Using such a 359 * formatter would require the calling code to check whether a full date, 360 * year-month or just a year had been parsed. This method can be used to 361 * default the month and day-of-month to a sensible value, such as the 362 * first of the month, allowing the calling code to always get a date. 363 * <p> 364 * During formatting, this method has no effect. 365 * <p> 366 * During parsing, the current state of the parse is inspected. 367 * If the specified field has no associated value, because it has not been 368 * parsed successfully at that point, then the specified value is injected 369 * into the parse result. Injection is immediate, thus the field-value pair 370 * will be visible to any subsequent elements in the formatter. 371 * As such, this method is normally called at the end of the builder. 372 * 373 * @param field the field to default the value of, not null 374 * @param value the value to default the field to 375 * @return this, for chaining, not null 376 */ parseDefaulting(TemporalField field, long value)377 public DateTimeFormatterBuilder parseDefaulting(TemporalField field, long value) { 378 Objects.requireNonNull(field, "field"); 379 appendInternal(new DefaultValueParser(field, value)); 380 return this; 381 } 382 383 //----------------------------------------------------------------------- 384 /** 385 * Appends the value of a date-time field to the formatter using a normal 386 * output style. 387 * <p> 388 * The value of the field will be output during a format. 389 * If the value cannot be obtained then an exception will be thrown. 390 * <p> 391 * The value will be printed as per the normal format of an integer value. 392 * Only negative numbers will be signed. No padding will be added. 393 * <p> 394 * The parser for a variable width value such as this normally behaves greedily, 395 * requiring one digit, but accepting as many digits as possible. 396 * This behavior can be affected by 'adjacent value parsing'. 397 * See {@link #appendValue(java.time.temporal.TemporalField, int)} for full details. 398 * 399 * @param field the field to append, not null 400 * @return this, for chaining, not null 401 */ appendValue(TemporalField field)402 public DateTimeFormatterBuilder appendValue(TemporalField field) { 403 Objects.requireNonNull(field, "field"); 404 appendValue(new NumberPrinterParser(field, 1, 19, SignStyle.NORMAL)); 405 return this; 406 } 407 408 /** 409 * Appends the value of a date-time field to the formatter using a fixed 410 * width, zero-padded approach. 411 * <p> 412 * The value of the field will be output during a format. 413 * If the value cannot be obtained then an exception will be thrown. 414 * <p> 415 * The value will be zero-padded on the left. If the size of the value 416 * means that it cannot be printed within the width then an exception is thrown. 417 * If the value of the field is negative then an exception is thrown during formatting. 418 * <p> 419 * This method supports a special technique of parsing known as 'adjacent value parsing'. 420 * This technique solves the problem where a value, variable or fixed width, is followed by one or more 421 * fixed length values. The standard parser is greedy, and thus it would normally 422 * steal the digits that are needed by the fixed width value parsers that follow the 423 * variable width one. 424 * <p> 425 * No action is required to initiate 'adjacent value parsing'. 426 * When a call to {@code appendValue} is made, the builder 427 * enters adjacent value parsing setup mode. If the immediately subsequent method 428 * call or calls on the same builder are for a fixed width value, then the parser will reserve 429 * space so that the fixed width values can be parsed. 430 * <p> 431 * For example, consider {@code builder.appendValue(YEAR).appendValue(MONTH_OF_YEAR, 2);} 432 * The year is a variable width parse of between 1 and 19 digits. 433 * The month is a fixed width parse of 2 digits. 434 * Because these were appended to the same builder immediately after one another, 435 * the year parser will reserve two digits for the month to parse. 436 * Thus, the text '201106' will correctly parse to a year of 2011 and a month of 6. 437 * Without adjacent value parsing, the year would greedily parse all six digits and leave 438 * nothing for the month. 439 * <p> 440 * Adjacent value parsing applies to each set of fixed width not-negative values in the parser 441 * that immediately follow any kind of value, variable or fixed width. 442 * Calling any other append method will end the setup of adjacent value parsing. 443 * Thus, in the unlikely event that you need to avoid adjacent value parsing behavior, 444 * simply add the {@code appendValue} to another {@code DateTimeFormatterBuilder} 445 * and add that to this builder. 446 * <p> 447 * If adjacent parsing is active, then parsing must match exactly the specified 448 * number of digits in both strict and lenient modes. 449 * In addition, no positive or negative sign is permitted. 450 * 451 * @param field the field to append, not null 452 * @param width the width of the printed field, from 1 to 19 453 * @return this, for chaining, not null 454 * @throws IllegalArgumentException if the width is invalid 455 */ appendValue(TemporalField field, int width)456 public DateTimeFormatterBuilder appendValue(TemporalField field, int width) { 457 Objects.requireNonNull(field, "field"); 458 if (width < 1 || width > 19) { 459 throw new IllegalArgumentException("The width must be from 1 to 19 inclusive but was " + width); 460 } 461 NumberPrinterParser pp = new NumberPrinterParser(field, width, width, SignStyle.NOT_NEGATIVE); 462 appendValue(pp); 463 return this; 464 } 465 466 /** 467 * Appends the value of a date-time field to the formatter providing full 468 * control over formatting. 469 * <p> 470 * The value of the field will be output during a format. 471 * If the value cannot be obtained then an exception will be thrown. 472 * <p> 473 * This method provides full control of the numeric formatting, including 474 * zero-padding and the positive/negative sign. 475 * <p> 476 * The parser for a variable width value such as this normally behaves greedily, 477 * accepting as many digits as possible. 478 * This behavior can be affected by 'adjacent value parsing'. 479 * See {@link #appendValue(java.time.temporal.TemporalField, int)} for full details. 480 * <p> 481 * In strict parsing mode, the minimum number of parsed digits is {@code minWidth} 482 * and the maximum is {@code maxWidth}. 483 * In lenient parsing mode, the minimum number of parsed digits is one 484 * and the maximum is 19 (except as limited by adjacent value parsing). 485 * <p> 486 * If this method is invoked with equal minimum and maximum widths and a sign style of 487 * {@code NOT_NEGATIVE} then it delegates to {@code appendValue(TemporalField,int)}. 488 * In this scenario, the formatting and parsing behavior described there occur. 489 * 490 * @param field the field to append, not null 491 * @param minWidth the minimum field width of the printed field, from 1 to 19 492 * @param maxWidth the maximum field width of the printed field, from 1 to 19 493 * @param signStyle the positive/negative output style, not null 494 * @return this, for chaining, not null 495 * @throws IllegalArgumentException if the widths are invalid 496 */ appendValue( TemporalField field, int minWidth, int maxWidth, SignStyle signStyle)497 public DateTimeFormatterBuilder appendValue( 498 TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) { 499 if (minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE) { 500 return appendValue(field, maxWidth); 501 } 502 Objects.requireNonNull(field, "field"); 503 Objects.requireNonNull(signStyle, "signStyle"); 504 if (minWidth < 1 || minWidth > 19) { 505 throw new IllegalArgumentException("The minimum width must be from 1 to 19 inclusive but was " + minWidth); 506 } 507 if (maxWidth < 1 || maxWidth > 19) { 508 throw new IllegalArgumentException("The maximum width must be from 1 to 19 inclusive but was " + maxWidth); 509 } 510 if (maxWidth < minWidth) { 511 throw new IllegalArgumentException("The maximum width must exceed or equal the minimum width but " + 512 maxWidth + " < " + minWidth); 513 } 514 NumberPrinterParser pp = new NumberPrinterParser(field, minWidth, maxWidth, signStyle); 515 appendValue(pp); 516 return this; 517 } 518 519 //----------------------------------------------------------------------- 520 /** 521 * Appends the reduced value of a date-time field to the formatter. 522 * <p> 523 * Since fields such as year vary by chronology, it is recommended to use the 524 * {@link #appendValueReduced(TemporalField, int, int, ChronoLocalDate)} date} 525 * variant of this method in most cases. This variant is suitable for 526 * simple fields or working with only the ISO chronology. 527 * <p> 528 * For formatting, the {@code width} and {@code maxWidth} are used to 529 * determine the number of characters to format. 530 * If they are equal then the format is fixed width. 531 * If the value of the field is within the range of the {@code baseValue} using 532 * {@code width} characters then the reduced value is formatted otherwise the value is 533 * truncated to fit {@code maxWidth}. 534 * The rightmost characters are output to match the width, left padding with zero. 535 * <p> 536 * For strict parsing, the number of characters allowed by {@code width} to {@code maxWidth} are parsed. 537 * For lenient parsing, the number of characters must be at least 1 and less than 10. 538 * If the number of digits parsed is equal to {@code width} and the value is positive, 539 * the value of the field is computed to be the first number greater than 540 * or equal to the {@code baseValue} with the same least significant characters, 541 * otherwise the value parsed is the field value. 542 * This allows a reduced value to be entered for values in range of the baseValue 543 * and width and absolute values can be entered for values outside the range. 544 * <p> 545 * For example, a base value of {@code 1980} and a width of {@code 2} will have 546 * valid values from {@code 1980} to {@code 2079}. 547 * During parsing, the text {@code "12"} will result in the value {@code 2012} as that 548 * is the value within the range where the last two characters are "12". 549 * By contrast, parsing the text {@code "1915"} will result in the value {@code 1915}. 550 * 551 * @param field the field to append, not null 552 * @param width the field width of the printed and parsed field, from 1 to 10 553 * @param maxWidth the maximum field width of the printed field, from 1 to 10 554 * @param baseValue the base value of the range of valid values 555 * @return this, for chaining, not null 556 * @throws IllegalArgumentException if the width or base value is invalid 557 */ appendValueReduced(TemporalField field, int width, int maxWidth, int baseValue)558 public DateTimeFormatterBuilder appendValueReduced(TemporalField field, 559 int width, int maxWidth, int baseValue) { 560 Objects.requireNonNull(field, "field"); 561 ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, baseValue, null); 562 appendValue(pp); 563 return this; 564 } 565 566 /** 567 * Appends the reduced value of a date-time field to the formatter. 568 * <p> 569 * This is typically used for formatting and parsing a two digit year. 570 * <p> 571 * The base date is used to calculate the full value during parsing. 572 * For example, if the base date is 1950-01-01 then parsed values for 573 * a two digit year parse will be in the range 1950-01-01 to 2049-12-31. 574 * Only the year would be extracted from the date, thus a base date of 575 * 1950-08-25 would also parse to the range 1950-01-01 to 2049-12-31. 576 * This behavior is necessary to support fields such as week-based-year 577 * or other calendar systems where the parsed value does not align with 578 * standard ISO years. 579 * <p> 580 * The exact behavior is as follows. Parse the full set of fields and 581 * determine the effective chronology using the last chronology if 582 * it appears more than once. Then convert the base date to the 583 * effective chronology. Then extract the specified field from the 584 * chronology-specific base date and use it to determine the 585 * {@code baseValue} used below. 586 * <p> 587 * For formatting, the {@code width} and {@code maxWidth} are used to 588 * determine the number of characters to format. 589 * If they are equal then the format is fixed width. 590 * If the value of the field is within the range of the {@code baseValue} using 591 * {@code width} characters then the reduced value is formatted otherwise the value is 592 * truncated to fit {@code maxWidth}. 593 * The rightmost characters are output to match the width, left padding with zero. 594 * <p> 595 * For strict parsing, the number of characters allowed by {@code width} to {@code maxWidth} are parsed. 596 * For lenient parsing, the number of characters must be at least 1 and less than 10. 597 * If the number of digits parsed is equal to {@code width} and the value is positive, 598 * the value of the field is computed to be the first number greater than 599 * or equal to the {@code baseValue} with the same least significant characters, 600 * otherwise the value parsed is the field value. 601 * This allows a reduced value to be entered for values in range of the baseValue 602 * and width and absolute values can be entered for values outside the range. 603 * <p> 604 * For example, a base value of {@code 1980} and a width of {@code 2} will have 605 * valid values from {@code 1980} to {@code 2079}. 606 * During parsing, the text {@code "12"} will result in the value {@code 2012} as that 607 * is the value within the range where the last two characters are "12". 608 * By contrast, parsing the text {@code "1915"} will result in the value {@code 1915}. 609 * 610 * @param field the field to append, not null 611 * @param width the field width of the printed and parsed field, from 1 to 10 612 * @param maxWidth the maximum field width of the printed field, from 1 to 10 613 * @param baseDate the base date used to calculate the base value for the range 614 * of valid values in the parsed chronology, not null 615 * @return this, for chaining, not null 616 * @throws IllegalArgumentException if the width or base value is invalid 617 */ appendValueReduced( TemporalField field, int width, int maxWidth, ChronoLocalDate baseDate)618 public DateTimeFormatterBuilder appendValueReduced( 619 TemporalField field, int width, int maxWidth, ChronoLocalDate baseDate) { 620 Objects.requireNonNull(field, "field"); 621 Objects.requireNonNull(baseDate, "baseDate"); 622 ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, 0, baseDate); 623 appendValue(pp); 624 return this; 625 } 626 627 /** 628 * Appends a fixed or variable width printer-parser handling adjacent value mode. 629 * If a PrinterParser is not active then the new PrinterParser becomes 630 * the active PrinterParser. 631 * Otherwise, the active PrinterParser is modified depending on the new PrinterParser. 632 * If the new PrinterParser is fixed width and has sign style {@code NOT_NEGATIVE} 633 * then its width is added to the active PP and 634 * the new PrinterParser is forced to be fixed width. 635 * If the new PrinterParser is variable width, the active PrinterParser is changed 636 * to be fixed width and the new PrinterParser becomes the active PP. 637 * 638 * @param pp the printer-parser, not null 639 * @return this, for chaining, not null 640 */ appendValue(NumberPrinterParser pp)641 private DateTimeFormatterBuilder appendValue(NumberPrinterParser pp) { 642 if (active.valueParserIndex >= 0) { 643 final int activeValueParser = active.valueParserIndex; 644 645 // adjacent parsing mode, update setting in previous parsers 646 NumberPrinterParser basePP = (NumberPrinterParser) active.printerParsers.get(activeValueParser); 647 if (pp.minWidth == pp.maxWidth && pp.signStyle == SignStyle.NOT_NEGATIVE) { 648 // Append the width to the subsequentWidth of the active parser 649 basePP = basePP.withSubsequentWidth(pp.maxWidth); 650 // Append the new parser as a fixed width 651 appendInternal(pp.withFixedWidth()); 652 // Retain the previous active parser 653 active.valueParserIndex = activeValueParser; 654 } else { 655 // Modify the active parser to be fixed width 656 basePP = basePP.withFixedWidth(); 657 // The new parser becomes the mew active parser 658 active.valueParserIndex = appendInternal(pp); 659 } 660 // Replace the modified parser with the updated one 661 active.printerParsers.set(activeValueParser, basePP); 662 } else { 663 // The new Parser becomes the active parser 664 active.valueParserIndex = appendInternal(pp); 665 } 666 return this; 667 } 668 669 //----------------------------------------------------------------------- 670 /** 671 * Appends the fractional value of a date-time field to the formatter. 672 * <p> 673 * The fractional value of the field will be output including the 674 * preceding decimal point. The preceding value is not output. 675 * For example, the second-of-minute value of 15 would be output as {@code .25}. 676 * <p> 677 * The width of the printed fraction can be controlled. Setting the 678 * minimum width to zero will cause no output to be generated. 679 * The printed fraction will have the minimum width necessary between 680 * the minimum and maximum widths - trailing zeroes are omitted. 681 * No rounding occurs due to the maximum width - digits are simply dropped. 682 * <p> 683 * When parsing in strict mode, the number of parsed digits must be between 684 * the minimum and maximum width. When parsing in lenient mode, the minimum 685 * width is considered to be zero and the maximum is nine. 686 * <p> 687 * If the value cannot be obtained then an exception will be thrown. 688 * If the value is negative an exception will be thrown. 689 * If the field does not have a fixed set of valid values then an 690 * exception will be thrown. 691 * If the field value in the date-time to be printed is invalid it 692 * cannot be printed and an exception will be thrown. 693 * 694 * @param field the field to append, not null 695 * @param minWidth the minimum width of the field excluding the decimal point, from 0 to 9 696 * @param maxWidth the maximum width of the field excluding the decimal point, from 1 to 9 697 * @param decimalPoint whether to output the localized decimal point symbol 698 * @return this, for chaining, not null 699 * @throws IllegalArgumentException if the field has a variable set of valid values or 700 * either width is invalid 701 */ appendFraction( TemporalField field, int minWidth, int maxWidth, boolean decimalPoint)702 public DateTimeFormatterBuilder appendFraction( 703 TemporalField field, int minWidth, int maxWidth, boolean decimalPoint) { 704 appendInternal(new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint)); 705 return this; 706 } 707 708 //----------------------------------------------------------------------- 709 /** 710 * Appends the text of a date-time field to the formatter using the full 711 * text style. 712 * <p> 713 * The text of the field will be output during a format. 714 * The value must be within the valid range of the field. 715 * If the value cannot be obtained then an exception will be thrown. 716 * If the field has no textual representation, then the numeric value will be used. 717 * <p> 718 * The value will be printed as per the normal format of an integer value. 719 * Only negative numbers will be signed. No padding will be added. 720 * 721 * @param field the field to append, not null 722 * @return this, for chaining, not null 723 */ appendText(TemporalField field)724 public DateTimeFormatterBuilder appendText(TemporalField field) { 725 return appendText(field, TextStyle.FULL); 726 } 727 728 /** 729 * Appends the text of a date-time field to the formatter. 730 * <p> 731 * The text of the field will be output during a format. 732 * The value must be within the valid range of the field. 733 * If the value cannot be obtained then an exception will be thrown. 734 * If the field has no textual representation, then the numeric value will be used. 735 * <p> 736 * The value will be printed as per the normal format of an integer value. 737 * Only negative numbers will be signed. No padding will be added. 738 * 739 * @param field the field to append, not null 740 * @param textStyle the text style to use, not null 741 * @return this, for chaining, not null 742 */ appendText(TemporalField field, TextStyle textStyle)743 public DateTimeFormatterBuilder appendText(TemporalField field, TextStyle textStyle) { 744 Objects.requireNonNull(field, "field"); 745 Objects.requireNonNull(textStyle, "textStyle"); 746 appendInternal(new TextPrinterParser(field, textStyle, DateTimeTextProvider.getInstance())); 747 return this; 748 } 749 750 /** 751 * Appends the text of a date-time field to the formatter using the specified 752 * map to supply the text. 753 * <p> 754 * The standard text outputting methods use the localized text in the JDK. 755 * This method allows that text to be specified directly. 756 * The supplied map is not validated by the builder to ensure that formatting or 757 * parsing is possible, thus an invalid map may throw an error during later use. 758 * <p> 759 * Supplying the map of text provides considerable flexibility in formatting and parsing. 760 * For example, a legacy application might require or supply the months of the 761 * year as "JNY", "FBY", "MCH" etc. These do not match the standard set of text 762 * for localized month names. Using this method, a map can be created which 763 * defines the connection between each value and the text: 764 * <pre> 765 * Map<Long, String> map = new HashMap<>(); 766 * map.put(1L, "JNY"); 767 * map.put(2L, "FBY"); 768 * map.put(3L, "MCH"); 769 * ... 770 * builder.appendText(MONTH_OF_YEAR, map); 771 * </pre> 772 * <p> 773 * Other uses might be to output the value with a suffix, such as "1st", "2nd", "3rd", 774 * or as Roman numerals "I", "II", "III", "IV". 775 * <p> 776 * During formatting, the value is obtained and checked that it is in the valid range. 777 * If text is not available for the value then it is output as a number. 778 * During parsing, the parser will match against the map of text and numeric values. 779 * 780 * @param field the field to append, not null 781 * @param textLookup the map from the value to the text 782 * @return this, for chaining, not null 783 */ appendText(TemporalField field, Map<Long, String> textLookup)784 public DateTimeFormatterBuilder appendText(TemporalField field, Map<Long, String> textLookup) { 785 Objects.requireNonNull(field, "field"); 786 Objects.requireNonNull(textLookup, "textLookup"); 787 Map<Long, String> copy = new LinkedHashMap<>(textLookup); 788 Map<TextStyle, Map<Long, String>> map = Collections.singletonMap(TextStyle.FULL, copy); 789 final LocaleStore store = new LocaleStore(map); 790 DateTimeTextProvider provider = new DateTimeTextProvider() { 791 @Override 792 public String getText(TemporalField field, long value, TextStyle style, Locale locale) { 793 return store.getText(value, style); 794 } 795 @Override 796 public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) { 797 return store.getTextIterator(style); 798 } 799 }; 800 appendInternal(new TextPrinterParser(field, TextStyle.FULL, provider)); 801 return this; 802 } 803 804 //----------------------------------------------------------------------- 805 /** 806 * Appends an instant using ISO-8601 to the formatter, formatting fractional 807 * digits in groups of three. 808 * <p> 809 * Instants have a fixed output format. 810 * They are converted to a date-time with a zone-offset of UTC and formatted 811 * using the standard ISO-8601 format. 812 * With this method, formatting nano-of-second outputs zero, three, six 813 * or nine digits digits as necessary. 814 * The localized decimal style is not used. 815 * <p> 816 * The instant is obtained using {@link ChronoField#INSTANT_SECONDS INSTANT_SECONDS} 817 * and optionally (@code NANO_OF_SECOND). The value of {@code INSTANT_SECONDS} 818 * may be outside the maximum range of {@code LocalDateTime}. 819 * <p> 820 * The {@linkplain ResolverStyle resolver style} has no effect on instant parsing. 821 * The end-of-day time of '24:00' is handled as midnight at the start of the following day. 822 * The leap-second time of '23:59:59' is handled to some degree, see 823 * {@link DateTimeFormatter#parsedLeapSecond()} for full details. 824 * <p> 825 * An alternative to this method is to format/parse the instant as a single 826 * epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}. 827 * 828 * @return this, for chaining, not null 829 */ appendInstant()830 public DateTimeFormatterBuilder appendInstant() { 831 appendInternal(new InstantPrinterParser(-2)); 832 return this; 833 } 834 835 /** 836 * Appends an instant using ISO-8601 to the formatter with control over 837 * the number of fractional digits. 838 * <p> 839 * Instants have a fixed output format, although this method provides some 840 * control over the fractional digits. They are converted to a date-time 841 * with a zone-offset of UTC and printed using the standard ISO-8601 format. 842 * The localized decimal style is not used. 843 * <p> 844 * The {@code fractionalDigits} parameter allows the output of the fractional 845 * second to be controlled. Specifying zero will cause no fractional digits 846 * to be output. From 1 to 9 will output an increasing number of digits, using 847 * zero right-padding if necessary. The special value -1 is used to output as 848 * many digits as necessary to avoid any trailing zeroes. 849 * <p> 850 * When parsing in strict mode, the number of parsed digits must match the 851 * fractional digits. When parsing in lenient mode, any number of fractional 852 * digits from zero to nine are accepted. 853 * <p> 854 * The instant is obtained using {@link ChronoField#INSTANT_SECONDS INSTANT_SECONDS} 855 * and optionally (@code NANO_OF_SECOND). The value of {@code INSTANT_SECONDS} 856 * may be outside the maximum range of {@code LocalDateTime}. 857 * <p> 858 * The {@linkplain ResolverStyle resolver style} has no effect on instant parsing. 859 * The end-of-day time of '24:00' is handled as midnight at the start of the following day. 860 * The leap-second time of '23:59:60' is handled to some degree, see 861 * {@link DateTimeFormatter#parsedLeapSecond()} for full details. 862 * <p> 863 * An alternative to this method is to format/parse the instant as a single 864 * epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}. 865 * 866 * @param fractionalDigits the number of fractional second digits to format with, 867 * from 0 to 9, or -1 to use as many digits as necessary 868 * @return this, for chaining, not null 869 */ appendInstant(int fractionalDigits)870 public DateTimeFormatterBuilder appendInstant(int fractionalDigits) { 871 if (fractionalDigits < -1 || fractionalDigits > 9) { 872 throw new IllegalArgumentException("The fractional digits must be from -1 to 9 inclusive but was " + fractionalDigits); 873 } 874 appendInternal(new InstantPrinterParser(fractionalDigits)); 875 return this; 876 } 877 878 //----------------------------------------------------------------------- 879 /** 880 * Appends the zone offset, such as '+01:00', to the formatter. 881 * <p> 882 * This appends an instruction to format/parse the offset ID to the builder. 883 * This is equivalent to calling {@code appendOffset("+HH:MM:ss", "Z")}. 884 * 885 * @return this, for chaining, not null 886 */ appendOffsetId()887 public DateTimeFormatterBuilder appendOffsetId() { 888 appendInternal(OffsetIdPrinterParser.INSTANCE_ID_Z); 889 return this; 890 } 891 892 /** 893 * Appends the zone offset, such as '+01:00', to the formatter. 894 * <p> 895 * This appends an instruction to format/parse the offset ID to the builder. 896 * <p> 897 * During formatting, the offset is obtained using a mechanism equivalent 898 * to querying the temporal with {@link TemporalQueries#offset()}. 899 * It will be printed using the format defined below. 900 * If the offset cannot be obtained then an exception is thrown unless the 901 * section of the formatter is optional. 902 * <p> 903 * During parsing, the offset is parsed using the format defined below. 904 * If the offset cannot be parsed then an exception is thrown unless the 905 * section of the formatter is optional. 906 * <p> 907 * The format of the offset is controlled by a pattern which must be one 908 * of the following: 909 * <ul> 910 * <li>{@code +HH} - hour only, ignoring minute and second 911 * <li>{@code +HHmm} - hour, with minute if non-zero, ignoring second, no colon 912 * <li>{@code +HH:mm} - hour, with minute if non-zero, ignoring second, with colon 913 * <li>{@code +HHMM} - hour and minute, ignoring second, no colon 914 * <li>{@code +HH:MM} - hour and minute, ignoring second, with colon 915 * <li>{@code +HHMMss} - hour and minute, with second if non-zero, no colon 916 * <li>{@code +HH:MM:ss} - hour and minute, with second if non-zero, with colon 917 * <li>{@code +HHMMSS} - hour, minute and second, no colon 918 * <li>{@code +HH:MM:SS} - hour, minute and second, with colon 919 * </ul> 920 * The "no offset" text controls what text is printed when the total amount of 921 * the offset fields to be output is zero. 922 * Example values would be 'Z', '+00:00', 'UTC' or 'GMT'. 923 * Three formats are accepted for parsing UTC - the "no offset" text, and the 924 * plus and minus versions of zero defined by the pattern. 925 * 926 * @param pattern the pattern to use, not null 927 * @param noOffsetText the text to use when the offset is zero, not null 928 * @return this, for chaining, not null 929 */ appendOffset(String pattern, String noOffsetText)930 public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText) { 931 appendInternal(new OffsetIdPrinterParser(pattern, noOffsetText)); 932 return this; 933 } 934 935 /** 936 * Appends the localized zone offset, such as 'GMT+01:00', to the formatter. 937 * <p> 938 * This appends a localized zone offset to the builder, the format of the 939 * localized offset is controlled by the specified {@link FormatStyle style} 940 * to this method: 941 * <ul> 942 * <li>{@link TextStyle#FULL full} - formats with localized offset text, such 943 * as 'GMT, 2-digit hour and minute field, optional second field if non-zero, 944 * and colon. 945 * <li>{@link TextStyle#SHORT short} - formats with localized offset text, 946 * such as 'GMT, hour without leading zero, optional 2-digit minute and 947 * second if non-zero, and colon. 948 * </ul> 949 * <p> 950 * During formatting, the offset is obtained using a mechanism equivalent 951 * to querying the temporal with {@link TemporalQueries#offset()}. 952 * If the offset cannot be obtained then an exception is thrown unless the 953 * section of the formatter is optional. 954 * <p> 955 * During parsing, the offset is parsed using the format defined above. 956 * If the offset cannot be parsed then an exception is thrown unless the 957 * section of the formatter is optional. 958 * <p> 959 * @param style the format style to use, not null 960 * @return this, for chaining, not null 961 * @throws IllegalArgumentException if style is neither {@link TextStyle#FULL 962 * full} nor {@link TextStyle#SHORT short} 963 */ appendLocalizedOffset(TextStyle style)964 public DateTimeFormatterBuilder appendLocalizedOffset(TextStyle style) { 965 Objects.requireNonNull(style, "style"); 966 if (style != TextStyle.FULL && style != TextStyle.SHORT) { 967 throw new IllegalArgumentException("Style must be either full or short"); 968 } 969 appendInternal(new LocalizedOffsetIdPrinterParser(style)); 970 return this; 971 } 972 973 //----------------------------------------------------------------------- 974 /** 975 * Appends the time-zone ID, such as 'Europe/Paris' or '+02:00', to the formatter. 976 * <p> 977 * This appends an instruction to format/parse the zone ID to the builder. 978 * The zone ID is obtained in a strict manner suitable for {@code ZonedDateTime}. 979 * By contrast, {@code OffsetDateTime} does not have a zone ID suitable 980 * for use with this method, see {@link #appendZoneOrOffsetId()}. 981 * <p> 982 * During formatting, the zone is obtained using a mechanism equivalent 983 * to querying the temporal with {@link TemporalQueries#zoneId()}. 984 * It will be printed using the result of {@link ZoneId#getId()}. 985 * If the zone cannot be obtained then an exception is thrown unless the 986 * section of the formatter is optional. 987 * <p> 988 * During parsing, the text must match a known zone or offset. 989 * There are two types of zone ID, offset-based, such as '+01:30' and 990 * region-based, such as 'Europe/London'. These are parsed differently. 991 * If the parse starts with '+', '-', 'UT', 'UTC' or 'GMT', then the parser 992 * expects an offset-based zone and will not match region-based zones. 993 * The offset ID, such as '+02:30', may be at the start of the parse, 994 * or prefixed by 'UT', 'UTC' or 'GMT'. The offset ID parsing is 995 * equivalent to using {@link #appendOffset(String, String)} using the 996 * arguments 'HH:MM:ss' and the no offset string '0'. 997 * If the parse starts with 'UT', 'UTC' or 'GMT', and the parser cannot 998 * match a following offset ID, then {@link ZoneOffset#UTC} is selected. 999 * In all other cases, the list of known region-based zones is used to 1000 * find the longest available match. If no match is found, and the parse 1001 * starts with 'Z', then {@code ZoneOffset.UTC} is selected. 1002 * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. 1003 * <p> 1004 * For example, the following will parse: 1005 * <pre> 1006 * "Europe/London" -- ZoneId.of("Europe/London") 1007 * "Z" -- ZoneOffset.UTC 1008 * "UT" -- ZoneId.of("UT") 1009 * "UTC" -- ZoneId.of("UTC") 1010 * "GMT" -- ZoneId.of("GMT") 1011 * "+01:30" -- ZoneOffset.of("+01:30") 1012 * "UT+01:30" -- ZoneOffset.of("+01:30") 1013 * "UTC+01:30" -- ZoneOffset.of("+01:30") 1014 * "GMT+01:30" -- ZoneOffset.of("+01:30") 1015 * </pre> 1016 * 1017 * @return this, for chaining, not null 1018 * @see #appendZoneRegionId() 1019 */ appendZoneId()1020 public DateTimeFormatterBuilder appendZoneId() { 1021 appendInternal(new ZoneIdPrinterParser(TemporalQueries.zoneId(), "ZoneId()")); 1022 return this; 1023 } 1024 1025 /** 1026 * Appends the time-zone region ID, such as 'Europe/Paris', to the formatter, 1027 * rejecting the zone ID if it is a {@code ZoneOffset}. 1028 * <p> 1029 * This appends an instruction to format/parse the zone ID to the builder 1030 * only if it is a region-based ID. 1031 * <p> 1032 * During formatting, the zone is obtained using a mechanism equivalent 1033 * to querying the temporal with {@link TemporalQueries#zoneId()}. 1034 * If the zone is a {@code ZoneOffset} or it cannot be obtained then 1035 * an exception is thrown unless the section of the formatter is optional. 1036 * If the zone is not an offset, then the zone will be printed using 1037 * the zone ID from {@link ZoneId#getId()}. 1038 * <p> 1039 * During parsing, the text must match a known zone or offset. 1040 * There are two types of zone ID, offset-based, such as '+01:30' and 1041 * region-based, such as 'Europe/London'. These are parsed differently. 1042 * If the parse starts with '+', '-', 'UT', 'UTC' or 'GMT', then the parser 1043 * expects an offset-based zone and will not match region-based zones. 1044 * The offset ID, such as '+02:30', may be at the start of the parse, 1045 * or prefixed by 'UT', 'UTC' or 'GMT'. The offset ID parsing is 1046 * equivalent to using {@link #appendOffset(String, String)} using the 1047 * arguments 'HH:MM:ss' and the no offset string '0'. 1048 * If the parse starts with 'UT', 'UTC' or 'GMT', and the parser cannot 1049 * match a following offset ID, then {@link ZoneOffset#UTC} is selected. 1050 * In all other cases, the list of known region-based zones is used to 1051 * find the longest available match. If no match is found, and the parse 1052 * starts with 'Z', then {@code ZoneOffset.UTC} is selected. 1053 * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. 1054 * <p> 1055 * For example, the following will parse: 1056 * <pre> 1057 * "Europe/London" -- ZoneId.of("Europe/London") 1058 * "Z" -- ZoneOffset.UTC 1059 * "UT" -- ZoneId.of("UT") 1060 * "UTC" -- ZoneId.of("UTC") 1061 * "GMT" -- ZoneId.of("GMT") 1062 * "+01:30" -- ZoneOffset.of("+01:30") 1063 * "UT+01:30" -- ZoneOffset.of("+01:30") 1064 * "UTC+01:30" -- ZoneOffset.of("+01:30") 1065 * "GMT+01:30" -- ZoneOffset.of("+01:30") 1066 * </pre> 1067 * <p> 1068 * Note that this method is identical to {@code appendZoneId()} except 1069 * in the mechanism used to obtain the zone. 1070 * Note also that parsing accepts offsets, whereas formatting will never 1071 * produce one. 1072 * 1073 * @return this, for chaining, not null 1074 * @see #appendZoneId() 1075 */ appendZoneRegionId()1076 public DateTimeFormatterBuilder appendZoneRegionId() { 1077 appendInternal(new ZoneIdPrinterParser(QUERY_REGION_ONLY, "ZoneRegionId()")); 1078 return this; 1079 } 1080 1081 /** 1082 * Appends the time-zone ID, such as 'Europe/Paris' or '+02:00', to 1083 * the formatter, using the best available zone ID. 1084 * <p> 1085 * This appends an instruction to format/parse the best available 1086 * zone or offset ID to the builder. 1087 * The zone ID is obtained in a lenient manner that first attempts to 1088 * find a true zone ID, such as that on {@code ZonedDateTime}, and 1089 * then attempts to find an offset, such as that on {@code OffsetDateTime}. 1090 * <p> 1091 * During formatting, the zone is obtained using a mechanism equivalent 1092 * to querying the temporal with {@link TemporalQueries#zone()}. 1093 * It will be printed using the result of {@link ZoneId#getId()}. 1094 * If the zone cannot be obtained then an exception is thrown unless the 1095 * section of the formatter is optional. 1096 * <p> 1097 * During parsing, the text must match a known zone or offset. 1098 * There are two types of zone ID, offset-based, such as '+01:30' and 1099 * region-based, such as 'Europe/London'. These are parsed differently. 1100 * If the parse starts with '+', '-', 'UT', 'UTC' or 'GMT', then the parser 1101 * expects an offset-based zone and will not match region-based zones. 1102 * The offset ID, such as '+02:30', may be at the start of the parse, 1103 * or prefixed by 'UT', 'UTC' or 'GMT'. The offset ID parsing is 1104 * equivalent to using {@link #appendOffset(String, String)} using the 1105 * arguments 'HH:MM:ss' and the no offset string '0'. 1106 * If the parse starts with 'UT', 'UTC' or 'GMT', and the parser cannot 1107 * match a following offset ID, then {@link ZoneOffset#UTC} is selected. 1108 * In all other cases, the list of known region-based zones is used to 1109 * find the longest available match. If no match is found, and the parse 1110 * starts with 'Z', then {@code ZoneOffset.UTC} is selected. 1111 * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. 1112 * <p> 1113 * For example, the following will parse: 1114 * <pre> 1115 * "Europe/London" -- ZoneId.of("Europe/London") 1116 * "Z" -- ZoneOffset.UTC 1117 * "UT" -- ZoneId.of("UT") 1118 * "UTC" -- ZoneId.of("UTC") 1119 * "GMT" -- ZoneId.of("GMT") 1120 * "+01:30" -- ZoneOffset.of("+01:30") 1121 * "UT+01:30" -- ZoneOffset.of("UT+01:30") 1122 * "UTC+01:30" -- ZoneOffset.of("UTC+01:30") 1123 * "GMT+01:30" -- ZoneOffset.of("GMT+01:30") 1124 * </pre> 1125 * <p> 1126 * Note that this method is identical to {@code appendZoneId()} except 1127 * in the mechanism used to obtain the zone. 1128 * 1129 * @return this, for chaining, not null 1130 * @see #appendZoneId() 1131 */ appendZoneOrOffsetId()1132 public DateTimeFormatterBuilder appendZoneOrOffsetId() { 1133 appendInternal(new ZoneIdPrinterParser(TemporalQueries.zone(), "ZoneOrOffsetId()")); 1134 return this; 1135 } 1136 1137 /** 1138 * Appends the time-zone name, such as 'British Summer Time', to the formatter. 1139 * <p> 1140 * This appends an instruction to format/parse the textual name of the zone to 1141 * the builder. 1142 * <p> 1143 * During formatting, the zone is obtained using a mechanism equivalent 1144 * to querying the temporal with {@link TemporalQueries#zoneId()}. 1145 * If the zone is a {@code ZoneOffset} it will be printed using the 1146 * result of {@link ZoneOffset#getId()}. 1147 * If the zone is not an offset, the textual name will be looked up 1148 * for the locale set in the {@link DateTimeFormatter}. 1149 * If the temporal object being printed represents an instant, then the text 1150 * will be the summer or winter time text as appropriate. 1151 * If the lookup for text does not find any suitable result, then the 1152 * {@link ZoneId#getId() ID} will be printed instead. 1153 * If the zone cannot be obtained then an exception is thrown unless the 1154 * section of the formatter is optional. 1155 * <p> 1156 * During parsing, either the textual zone name, the zone ID or the offset 1157 * is accepted. Many textual zone names are not unique, such as CST can be 1158 * for both "Central Standard Time" and "China Standard Time". In this 1159 * situation, the zone id will be determined by the region information from 1160 * formatter's {@link DateTimeFormatter#getLocale() locale} and the standard 1161 * zone id for that area, for example, America/New_York for the America Eastern 1162 * zone. The {@link #appendZoneText(TextStyle, Set)} may be used 1163 * to specify a set of preferred {@link ZoneId} in this situation. 1164 * 1165 * @param textStyle the text style to use, not null 1166 * @return this, for chaining, not null 1167 */ appendZoneText(TextStyle textStyle)1168 public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle) { 1169 appendInternal(new ZoneTextPrinterParser(textStyle, null)); 1170 return this; 1171 } 1172 1173 /** 1174 * Appends the time-zone name, such as 'British Summer Time', to the formatter. 1175 * <p> 1176 * This appends an instruction to format/parse the textual name of the zone to 1177 * the builder. 1178 * <p> 1179 * During formatting, the zone is obtained using a mechanism equivalent 1180 * to querying the temporal with {@link TemporalQueries#zoneId()}. 1181 * If the zone is a {@code ZoneOffset} it will be printed using the 1182 * result of {@link ZoneOffset#getId()}. 1183 * If the zone is not an offset, the textual name will be looked up 1184 * for the locale set in the {@link DateTimeFormatter}. 1185 * If the temporal object being printed represents an instant, then the text 1186 * will be the summer or winter time text as appropriate. 1187 * If the lookup for text does not find any suitable result, then the 1188 * {@link ZoneId#getId() ID} will be printed instead. 1189 * If the zone cannot be obtained then an exception is thrown unless the 1190 * section of the formatter is optional. 1191 * <p> 1192 * During parsing, either the textual zone name, the zone ID or the offset 1193 * is accepted. Many textual zone names are not unique, such as CST can be 1194 * for both "Central Standard Time" and "China Standard Time". In this 1195 * situation, the zone id will be determined by the region information from 1196 * formatter's {@link DateTimeFormatter#getLocale() locale} and the standard 1197 * zone id for that area, for example, America/New_York for the America Eastern 1198 * zone. This method also allows a set of preferred {@link ZoneId} to be 1199 * specified for parsing. The matched preferred zone id will be used if the 1200 * textural zone name being parsed is not unique. 1201 * <p> 1202 * If the zone cannot be parsed then an exception is thrown unless the 1203 * section of the formatter is optional. 1204 * 1205 * @param textStyle the text style to use, not null 1206 * @param preferredZones the set of preferred zone ids, not null 1207 * @return this, for chaining, not null 1208 */ appendZoneText(TextStyle textStyle, Set<ZoneId> preferredZones)1209 public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle, 1210 Set<ZoneId> preferredZones) { 1211 Objects.requireNonNull(preferredZones, "preferredZones"); 1212 appendInternal(new ZoneTextPrinterParser(textStyle, preferredZones)); 1213 return this; 1214 } 1215 1216 //----------------------------------------------------------------------- 1217 /** 1218 * Appends the chronology ID, such as 'ISO' or 'ThaiBuddhist', to the formatter. 1219 * <p> 1220 * This appends an instruction to format/parse the chronology ID to the builder. 1221 * <p> 1222 * During formatting, the chronology is obtained using a mechanism equivalent 1223 * to querying the temporal with {@link TemporalQueries#chronology()}. 1224 * It will be printed using the result of {@link Chronology#getId()}. 1225 * If the chronology cannot be obtained then an exception is thrown unless the 1226 * section of the formatter is optional. 1227 * <p> 1228 * During parsing, the chronology is parsed and must match one of the chronologies 1229 * in {@link Chronology#getAvailableChronologies()}. 1230 * If the chronology cannot be parsed then an exception is thrown unless the 1231 * section of the formatter is optional. 1232 * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. 1233 * 1234 * @return this, for chaining, not null 1235 */ appendChronologyId()1236 public DateTimeFormatterBuilder appendChronologyId() { 1237 appendInternal(new ChronoPrinterParser(null)); 1238 return this; 1239 } 1240 1241 /** 1242 * Appends the chronology name to the formatter. 1243 * <p> 1244 * The calendar system name will be output during a format. 1245 * If the chronology cannot be obtained then an exception will be thrown. 1246 * 1247 * @param textStyle the text style to use, not null 1248 * @return this, for chaining, not null 1249 */ appendChronologyText(TextStyle textStyle)1250 public DateTimeFormatterBuilder appendChronologyText(TextStyle textStyle) { 1251 Objects.requireNonNull(textStyle, "textStyle"); 1252 appendInternal(new ChronoPrinterParser(textStyle)); 1253 return this; 1254 } 1255 1256 //----------------------------------------------------------------------- 1257 /** 1258 * Appends a localized date-time pattern to the formatter. 1259 * <p> 1260 * This appends a localized section to the builder, suitable for outputting 1261 * a date, time or date-time combination. The format of the localized 1262 * section is lazily looked up based on four items: 1263 * <ul> 1264 * <li>the {@code dateStyle} specified to this method 1265 * <li>the {@code timeStyle} specified to this method 1266 * <li>the {@code Locale} of the {@code DateTimeFormatter} 1267 * <li>the {@code Chronology}, selecting the best available 1268 * </ul> 1269 * During formatting, the chronology is obtained from the temporal object 1270 * being formatted, which may have been overridden by 1271 * {@link DateTimeFormatter#withChronology(Chronology)}. 1272 * <p> 1273 * During parsing, if a chronology has already been parsed, then it is used. 1274 * Otherwise the default from {@code DateTimeFormatter.withChronology(Chronology)} 1275 * is used, with {@code IsoChronology} as the fallback. 1276 * <p> 1277 * Note that this method provides similar functionality to methods on 1278 * {@code DateFormat} such as {@link java.text.DateFormat#getDateTimeInstance(int, int)}. 1279 * 1280 * @param dateStyle the date style to use, null means no date required 1281 * @param timeStyle the time style to use, null means no time required 1282 * @return this, for chaining, not null 1283 * @throws IllegalArgumentException if both the date and time styles are null 1284 */ appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle)1285 public DateTimeFormatterBuilder appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle) { 1286 if (dateStyle == null && timeStyle == null) { 1287 throw new IllegalArgumentException("Either the date or time style must be non-null"); 1288 } 1289 appendInternal(new LocalizedPrinterParser(dateStyle, timeStyle)); 1290 return this; 1291 } 1292 1293 //----------------------------------------------------------------------- 1294 /** 1295 * Appends a character literal to the formatter. 1296 * <p> 1297 * This character will be output during a format. 1298 * 1299 * @param literal the literal to append, not null 1300 * @return this, for chaining, not null 1301 */ appendLiteral(char literal)1302 public DateTimeFormatterBuilder appendLiteral(char literal) { 1303 appendInternal(new CharLiteralPrinterParser(literal)); 1304 return this; 1305 } 1306 1307 /** 1308 * Appends a string literal to the formatter. 1309 * <p> 1310 * This string will be output during a format. 1311 * <p> 1312 * If the literal is empty, nothing is added to the formatter. 1313 * 1314 * @param literal the literal to append, not null 1315 * @return this, for chaining, not null 1316 */ appendLiteral(String literal)1317 public DateTimeFormatterBuilder appendLiteral(String literal) { 1318 Objects.requireNonNull(literal, "literal"); 1319 if (literal.length() > 0) { 1320 if (literal.length() == 1) { 1321 appendInternal(new CharLiteralPrinterParser(literal.charAt(0))); 1322 } else { 1323 appendInternal(new StringLiteralPrinterParser(literal)); 1324 } 1325 } 1326 return this; 1327 } 1328 1329 //----------------------------------------------------------------------- 1330 /** 1331 * Appends all the elements of a formatter to the builder. 1332 * <p> 1333 * This method has the same effect as appending each of the constituent 1334 * parts of the formatter directly to this builder. 1335 * 1336 * @param formatter the formatter to add, not null 1337 * @return this, for chaining, not null 1338 */ append(DateTimeFormatter formatter)1339 public DateTimeFormatterBuilder append(DateTimeFormatter formatter) { 1340 Objects.requireNonNull(formatter, "formatter"); 1341 appendInternal(formatter.toPrinterParser(false)); 1342 return this; 1343 } 1344 1345 /** 1346 * Appends a formatter to the builder which will optionally format/parse. 1347 * <p> 1348 * This method has the same effect as appending each of the constituent 1349 * parts directly to this builder surrounded by an {@link #optionalStart()} and 1350 * {@link #optionalEnd()}. 1351 * <p> 1352 * The formatter will format if data is available for all the fields contained within it. 1353 * The formatter will parse if the string matches, otherwise no error is returned. 1354 * 1355 * @param formatter the formatter to add, not null 1356 * @return this, for chaining, not null 1357 */ appendOptional(DateTimeFormatter formatter)1358 public DateTimeFormatterBuilder appendOptional(DateTimeFormatter formatter) { 1359 Objects.requireNonNull(formatter, "formatter"); 1360 appendInternal(formatter.toPrinterParser(true)); 1361 return this; 1362 } 1363 1364 //----------------------------------------------------------------------- 1365 /** 1366 * Appends the elements defined by the specified pattern to the builder. 1367 * <p> 1368 * All letters 'A' to 'Z' and 'a' to 'z' are reserved as pattern letters. 1369 * The characters '#', '{' and '}' are reserved for future use. 1370 * The characters '[' and ']' indicate optional patterns. 1371 * The following pattern letters are defined: 1372 * <pre> 1373 * Symbol Meaning Presentation Examples 1374 * ------ ------- ------------ ------- 1375 * G era text AD; Anno Domini; A 1376 * u year year 2004; 04 1377 * y year-of-era year 2004; 04 1378 * D day-of-year number 189 1379 * M/L month-of-year number/text 7; 07; Jul; July; J 1380 * d day-of-month number 10 1381 * 1382 * Q/q quarter-of-year number/text 3; 03; Q3; 3rd quarter 1383 * Y week-based-year year 1996; 96 1384 * w week-of-week-based-year number 27 1385 * W week-of-month number 4 1386 * E day-of-week text Tue; Tuesday; T 1387 * e/c localized day-of-week number/text 2; 02; Tue; Tuesday; T 1388 * F week-of-month number 3 1389 * 1390 * a am-pm-of-day text PM 1391 * h clock-hour-of-am-pm (1-12) number 12 1392 * K hour-of-am-pm (0-11) number 0 1393 * k clock-hour-of-am-pm (1-24) number 0 1394 * 1395 * H hour-of-day (0-23) number 0 1396 * m minute-of-hour number 30 1397 * s second-of-minute number 55 1398 * S fraction-of-second fraction 978 1399 * A milli-of-day number 1234 1400 * n nano-of-second number 987654321 1401 * N nano-of-day number 1234000000 1402 * 1403 * V time-zone ID zone-id America/Los_Angeles; Z; -08:30 1404 * z time-zone name zone-name Pacific Standard Time; PST 1405 * O localized zone-offset offset-O GMT+8; GMT+08:00; UTC-08:00; 1406 * X zone-offset 'Z' for zero offset-X Z; -08; -0830; -08:30; -083015; -08:30:15; 1407 * x zone-offset offset-x +0000; -08; -0830; -08:30; -083015; -08:30:15; 1408 * Z zone-offset offset-Z +0000; -0800; -08:00; 1409 * 1410 * p pad next pad modifier 1 1411 * 1412 * ' escape for text delimiter 1413 * '' single quote literal ' 1414 * [ optional section start 1415 * ] optional section end 1416 * # reserved for future use 1417 * { reserved for future use 1418 * } reserved for future use 1419 * </pre> 1420 * <p> 1421 * The count of pattern letters determine the format. 1422 * See <a href="DateTimeFormatter.html#patterns">DateTimeFormatter</a> for a user-focused description of the patterns. 1423 * The following tables define how the pattern letters map to the builder. 1424 * <p> 1425 * <b>Date fields</b>: Pattern letters to output a date. 1426 * <pre> 1427 * Pattern Count Equivalent builder methods 1428 * ------- ----- -------------------------- 1429 * G 1 appendText(ChronoField.ERA, TextStyle.SHORT) 1430 * GG 2 appendText(ChronoField.ERA, TextStyle.SHORT) 1431 * GGG 3 appendText(ChronoField.ERA, TextStyle.SHORT) 1432 * GGGG 4 appendText(ChronoField.ERA, TextStyle.FULL) 1433 * GGGGG 5 appendText(ChronoField.ERA, TextStyle.NARROW) 1434 * 1435 * u 1 appendValue(ChronoField.YEAR, 1, 19, SignStyle.NORMAL); 1436 * uu 2 appendValueReduced(ChronoField.YEAR, 2, 2000); 1437 * uuu 3 appendValue(ChronoField.YEAR, 3, 19, SignStyle.NORMAL); 1438 * u..u 4..n appendValue(ChronoField.YEAR, n, 19, SignStyle.EXCEEDS_PAD); 1439 * y 1 appendValue(ChronoField.YEAR_OF_ERA, 1, 19, SignStyle.NORMAL); 1440 * yy 2 appendValueReduced(ChronoField.YEAR_OF_ERA, 2, 2000); 1441 * yyy 3 appendValue(ChronoField.YEAR_OF_ERA, 3, 19, SignStyle.NORMAL); 1442 * y..y 4..n appendValue(ChronoField.YEAR_OF_ERA, n, 19, SignStyle.EXCEEDS_PAD); 1443 * Y 1 append special localized WeekFields element for numeric week-based-year 1444 * YY 2 append special localized WeekFields element for reduced numeric week-based-year 2 digits; 1445 * YYY 3 append special localized WeekFields element for numeric week-based-year (3, 19, SignStyle.NORMAL); 1446 * Y..Y 4..n append special localized WeekFields element for numeric week-based-year (n, 19, SignStyle.EXCEEDS_PAD); 1447 * 1448 * Q 1 appendValue(IsoFields.QUARTER_OF_YEAR); 1449 * QQ 2 appendValue(IsoFields.QUARTER_OF_YEAR, 2); 1450 * QQQ 3 appendText(IsoFields.QUARTER_OF_YEAR, TextStyle.SHORT) 1451 * QQQQ 4 appendText(IsoFields.QUARTER_OF_YEAR, TextStyle.FULL) 1452 * QQQQQ 5 appendText(IsoFields.QUARTER_OF_YEAR, TextStyle.NARROW) 1453 * q 1 appendValue(IsoFields.QUARTER_OF_YEAR); 1454 * qq 2 appendValue(IsoFields.QUARTER_OF_YEAR, 2); 1455 * qqq 3 appendText(IsoFields.QUARTER_OF_YEAR, TextStyle.SHORT_STANDALONE) 1456 * qqqq 4 appendText(IsoFields.QUARTER_OF_YEAR, TextStyle.FULL_STANDALONE) 1457 * qqqqq 5 appendText(IsoFields.QUARTER_OF_YEAR, TextStyle.NARROW_STANDALONE) 1458 * 1459 * M 1 appendValue(ChronoField.MONTH_OF_YEAR); 1460 * MM 2 appendValue(ChronoField.MONTH_OF_YEAR, 2); 1461 * MMM 3 appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT) 1462 * MMMM 4 appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL) 1463 * MMMMM 5 appendText(ChronoField.MONTH_OF_YEAR, TextStyle.NARROW) 1464 * L 1 appendValue(ChronoField.MONTH_OF_YEAR); 1465 * LL 2 appendValue(ChronoField.MONTH_OF_YEAR, 2); 1466 * LLL 3 appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT_STANDALONE) 1467 * LLLL 4 appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL_STANDALONE) 1468 * LLLLL 5 appendText(ChronoField.MONTH_OF_YEAR, TextStyle.NARROW_STANDALONE) 1469 * 1470 * w 1 append special localized WeekFields element for numeric week-of-year 1471 * ww 2 append special localized WeekFields element for numeric week-of-year, zero-padded 1472 * W 1 append special localized WeekFields element for numeric week-of-month 1473 * d 1 appendValue(ChronoField.DAY_OF_MONTH) 1474 * dd 2 appendValue(ChronoField.DAY_OF_MONTH, 2) 1475 * D 1 appendValue(ChronoField.DAY_OF_YEAR) 1476 * DD 2 appendValue(ChronoField.DAY_OF_YEAR, 2) 1477 * DDD 3 appendValue(ChronoField.DAY_OF_YEAR, 3) 1478 * F 1 appendValue(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH) 1479 * E 1 appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) 1480 * EE 2 appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) 1481 * EEE 3 appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) 1482 * EEEE 4 appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL) 1483 * EEEEE 5 appendText(ChronoField.DAY_OF_WEEK, TextStyle.NARROW) 1484 * e 1 append special localized WeekFields element for numeric day-of-week 1485 * ee 2 append special localized WeekFields element for numeric day-of-week, zero-padded 1486 * eee 3 appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) 1487 * eeee 4 appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL) 1488 * eeeee 5 appendText(ChronoField.DAY_OF_WEEK, TextStyle.NARROW) 1489 * c 1 append special localized WeekFields element for numeric day-of-week 1490 * ccc 3 appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT_STANDALONE) 1491 * cccc 4 appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL_STANDALONE) 1492 * ccccc 5 appendText(ChronoField.DAY_OF_WEEK, TextStyle.NARROW_STANDALONE) 1493 * </pre> 1494 * <p> 1495 * <b>Time fields</b>: Pattern letters to output a time. 1496 * <pre> 1497 * Pattern Count Equivalent builder methods 1498 * ------- ----- -------------------------- 1499 * a 1 appendText(ChronoField.AMPM_OF_DAY, TextStyle.SHORT) 1500 * h 1 appendValue(ChronoField.CLOCK_HOUR_OF_AMPM) 1501 * hh 2 appendValue(ChronoField.CLOCK_HOUR_OF_AMPM, 2) 1502 * H 1 appendValue(ChronoField.HOUR_OF_DAY) 1503 * HH 2 appendValue(ChronoField.HOUR_OF_DAY, 2) 1504 * k 1 appendValue(ChronoField.CLOCK_HOUR_OF_DAY) 1505 * kk 2 appendValue(ChronoField.CLOCK_HOUR_OF_DAY, 2) 1506 * K 1 appendValue(ChronoField.HOUR_OF_AMPM) 1507 * KK 2 appendValue(ChronoField.HOUR_OF_AMPM, 2) 1508 * m 1 appendValue(ChronoField.MINUTE_OF_HOUR) 1509 * mm 2 appendValue(ChronoField.MINUTE_OF_HOUR, 2) 1510 * s 1 appendValue(ChronoField.SECOND_OF_MINUTE) 1511 * ss 2 appendValue(ChronoField.SECOND_OF_MINUTE, 2) 1512 * 1513 * S..S 1..n appendFraction(ChronoField.NANO_OF_SECOND, n, n, false) 1514 * A 1 appendValue(ChronoField.MILLI_OF_DAY) 1515 * A..A 2..n appendValue(ChronoField.MILLI_OF_DAY, n) 1516 * n 1 appendValue(ChronoField.NANO_OF_SECOND) 1517 * n..n 2..n appendValue(ChronoField.NANO_OF_SECOND, n) 1518 * N 1 appendValue(ChronoField.NANO_OF_DAY) 1519 * N..N 2..n appendValue(ChronoField.NANO_OF_DAY, n) 1520 * </pre> 1521 * <p> 1522 * <b>Zone ID</b>: Pattern letters to output {@code ZoneId}. 1523 * <pre> 1524 * Pattern Count Equivalent builder methods 1525 * ------- ----- -------------------------- 1526 * VV 2 appendZoneId() 1527 * z 1 appendZoneText(TextStyle.SHORT) 1528 * zz 2 appendZoneText(TextStyle.SHORT) 1529 * zzz 3 appendZoneText(TextStyle.SHORT) 1530 * zzzz 4 appendZoneText(TextStyle.FULL) 1531 * </pre> 1532 * <p> 1533 * <b>Zone offset</b>: Pattern letters to output {@code ZoneOffset}. 1534 * <pre> 1535 * Pattern Count Equivalent builder methods 1536 * ------- ----- -------------------------- 1537 * O 1 appendLocalizedOffsetPrefixed(TextStyle.SHORT); 1538 * OOOO 4 appendLocalizedOffsetPrefixed(TextStyle.FULL); 1539 * X 1 appendOffset("+HHmm","Z") 1540 * XX 2 appendOffset("+HHMM","Z") 1541 * XXX 3 appendOffset("+HH:MM","Z") 1542 * XXXX 4 appendOffset("+HHMMss","Z") 1543 * XXXXX 5 appendOffset("+HH:MM:ss","Z") 1544 * x 1 appendOffset("+HHmm","+00") 1545 * xx 2 appendOffset("+HHMM","+0000") 1546 * xxx 3 appendOffset("+HH:MM","+00:00") 1547 * xxxx 4 appendOffset("+HHMMss","+0000") 1548 * xxxxx 5 appendOffset("+HH:MM:ss","+00:00") 1549 * Z 1 appendOffset("+HHMM","+0000") 1550 * ZZ 2 appendOffset("+HHMM","+0000") 1551 * ZZZ 3 appendOffset("+HHMM","+0000") 1552 * ZZZZ 4 appendLocalizedOffset(TextStyle.FULL); 1553 * ZZZZZ 5 appendOffset("+HH:MM:ss","Z") 1554 * </pre> 1555 * <p> 1556 * <b>Modifiers</b>: Pattern letters that modify the rest of the pattern: 1557 * <pre> 1558 * Pattern Count Equivalent builder methods 1559 * ------- ----- -------------------------- 1560 * [ 1 optionalStart() 1561 * ] 1 optionalEnd() 1562 * p..p 1..n padNext(n) 1563 * </pre> 1564 * <p> 1565 * Any sequence of letters not specified above, unrecognized letter or 1566 * reserved character will throw an exception. 1567 * Future versions may add to the set of patterns. 1568 * It is recommended to use single quotes around all characters that you want 1569 * to output directly to ensure that future changes do not break your application. 1570 * <p> 1571 * Note that the pattern string is similar, but not identical, to 1572 * {@link java.text.SimpleDateFormat SimpleDateFormat}. 1573 * The pattern string is also similar, but not identical, to that defined by the 1574 * Unicode Common Locale Data Repository (CLDR/LDML). 1575 * Pattern letters 'X' and 'u' are aligned with Unicode CLDR/LDML. 1576 * By contrast, {@code SimpleDateFormat} uses 'u' for the numeric day of week. 1577 * Pattern letters 'y' and 'Y' parse years of two digits and more than 4 digits differently. 1578 * Pattern letters 'n', 'A', 'N', and 'p' are added. 1579 * Number types will reject large numbers. 1580 * 1581 * @param pattern the pattern to add, not null 1582 * @return this, for chaining, not null 1583 * @throws IllegalArgumentException if the pattern is invalid 1584 */ appendPattern(String pattern)1585 public DateTimeFormatterBuilder appendPattern(String pattern) { 1586 Objects.requireNonNull(pattern, "pattern"); 1587 parsePattern(pattern); 1588 return this; 1589 } 1590 parsePattern(String pattern)1591 private void parsePattern(String pattern) { 1592 for (int pos = 0; pos < pattern.length(); pos++) { 1593 char cur = pattern.charAt(pos); 1594 if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) { 1595 int start = pos++; 1596 for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop 1597 int count = pos - start; 1598 // padding 1599 if (cur == 'p') { 1600 int pad = 0; 1601 if (pos < pattern.length()) { 1602 cur = pattern.charAt(pos); 1603 if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) { 1604 pad = count; 1605 start = pos++; 1606 for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop 1607 count = pos - start; 1608 } 1609 } 1610 if (pad == 0) { 1611 throw new IllegalArgumentException( 1612 "Pad letter 'p' must be followed by valid pad pattern: " + pattern); 1613 } 1614 padNext(pad); // pad and continue parsing 1615 } 1616 // main rules 1617 TemporalField field = FIELD_MAP.get(cur); 1618 if (field != null) { 1619 parseField(cur, count, field); 1620 } else if (cur == 'z') { 1621 if (count > 4) { 1622 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1623 } else if (count == 4) { 1624 appendZoneText(TextStyle.FULL); 1625 } else { 1626 appendZoneText(TextStyle.SHORT); 1627 } 1628 } else if (cur == 'V') { 1629 if (count != 2) { 1630 throw new IllegalArgumentException("Pattern letter count must be 2: " + cur); 1631 } 1632 appendZoneId(); 1633 } else if (cur == 'Z') { 1634 if (count < 4) { 1635 appendOffset("+HHMM", "+0000"); 1636 } else if (count == 4) { 1637 appendLocalizedOffset(TextStyle.FULL); 1638 } else if (count == 5) { 1639 appendOffset("+HH:MM:ss","Z"); 1640 } else { 1641 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1642 } 1643 } else if (cur == 'O') { 1644 if (count == 1) { 1645 appendLocalizedOffset(TextStyle.SHORT); 1646 } else if (count == 4) { 1647 appendLocalizedOffset(TextStyle.FULL); 1648 } else { 1649 throw new IllegalArgumentException("Pattern letter count must be 1 or 4: " + cur); 1650 } 1651 } else if (cur == 'X') { 1652 if (count > 5) { 1653 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1654 } 1655 appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], "Z"); 1656 } else if (cur == 'x') { 1657 if (count > 5) { 1658 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1659 } 1660 String zero = (count == 1 ? "+00" : (count % 2 == 0 ? "+0000" : "+00:00")); 1661 appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], zero); 1662 } else if (cur == 'W') { 1663 // Fields defined by Locale 1664 if (count > 1) { 1665 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1666 } 1667 appendInternal(new WeekBasedFieldPrinterParser(cur, count)); 1668 } else if (cur == 'w') { 1669 // Fields defined by Locale 1670 if (count > 2) { 1671 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1672 } 1673 appendInternal(new WeekBasedFieldPrinterParser(cur, count)); 1674 } else if (cur == 'Y') { 1675 // Fields defined by Locale 1676 appendInternal(new WeekBasedFieldPrinterParser(cur, count)); 1677 } else { 1678 throw new IllegalArgumentException("Unknown pattern letter: " + cur); 1679 } 1680 pos--; 1681 1682 } else if (cur == '\'') { 1683 // parse literals 1684 int start = pos++; 1685 for ( ; pos < pattern.length(); pos++) { 1686 if (pattern.charAt(pos) == '\'') { 1687 if (pos + 1 < pattern.length() && pattern.charAt(pos + 1) == '\'') { 1688 pos++; 1689 } else { 1690 break; // end of literal 1691 } 1692 } 1693 } 1694 if (pos >= pattern.length()) { 1695 throw new IllegalArgumentException("Pattern ends with an incomplete string literal: " + pattern); 1696 } 1697 String str = pattern.substring(start + 1, pos); 1698 if (str.length() == 0) { 1699 appendLiteral('\''); 1700 } else { 1701 appendLiteral(str.replace("''", "'")); 1702 } 1703 1704 } else if (cur == '[') { 1705 optionalStart(); 1706 1707 } else if (cur == ']') { 1708 if (active.parent == null) { 1709 throw new IllegalArgumentException("Pattern invalid as it contains ] without previous ["); 1710 } 1711 optionalEnd(); 1712 1713 } else if (cur == '{' || cur == '}' || cur == '#') { 1714 throw new IllegalArgumentException("Pattern includes reserved character: '" + cur + "'"); 1715 } else { 1716 appendLiteral(cur); 1717 } 1718 } 1719 } 1720 1721 @SuppressWarnings("fallthrough") parseField(char cur, int count, TemporalField field)1722 private void parseField(char cur, int count, TemporalField field) { 1723 boolean standalone = false; 1724 switch (cur) { 1725 case 'u': 1726 case 'y': 1727 if (count == 2) { 1728 appendValueReduced(field, 2, 2, ReducedPrinterParser.BASE_DATE); 1729 } else if (count < 4) { 1730 appendValue(field, count, 19, SignStyle.NORMAL); 1731 } else { 1732 appendValue(field, count, 19, SignStyle.EXCEEDS_PAD); 1733 } 1734 break; 1735 case 'c': 1736 if (count == 2) { 1737 throw new IllegalArgumentException("Invalid pattern \"cc\""); 1738 } 1739 /*fallthrough*/ 1740 case 'L': 1741 case 'q': 1742 standalone = true; 1743 /*fallthrough*/ 1744 case 'M': 1745 case 'Q': 1746 case 'E': 1747 case 'e': 1748 switch (count) { 1749 case 1: 1750 case 2: 1751 if (cur == 'c' || cur == 'e') { 1752 appendInternal(new WeekBasedFieldPrinterParser(cur, count)); 1753 } else if (cur == 'E') { 1754 appendText(field, TextStyle.SHORT); 1755 } else { 1756 if (count == 1) { 1757 appendValue(field); 1758 } else { 1759 appendValue(field, 2); 1760 } 1761 } 1762 break; 1763 case 3: 1764 appendText(field, standalone ? TextStyle.SHORT_STANDALONE : TextStyle.SHORT); 1765 break; 1766 case 4: 1767 appendText(field, standalone ? TextStyle.FULL_STANDALONE : TextStyle.FULL); 1768 break; 1769 case 5: 1770 appendText(field, standalone ? TextStyle.NARROW_STANDALONE : TextStyle.NARROW); 1771 break; 1772 default: 1773 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1774 } 1775 break; 1776 case 'a': 1777 if (count == 1) { 1778 appendText(field, TextStyle.SHORT); 1779 } else { 1780 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1781 } 1782 break; 1783 case 'G': 1784 switch (count) { 1785 case 1: 1786 case 2: 1787 case 3: 1788 appendText(field, TextStyle.SHORT); 1789 break; 1790 case 4: 1791 appendText(field, TextStyle.FULL); 1792 break; 1793 case 5: 1794 appendText(field, TextStyle.NARROW); 1795 break; 1796 default: 1797 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1798 } 1799 break; 1800 case 'S': 1801 appendFraction(NANO_OF_SECOND, count, count, false); 1802 break; 1803 case 'F': 1804 if (count == 1) { 1805 appendValue(field); 1806 } else { 1807 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1808 } 1809 break; 1810 case 'd': 1811 case 'h': 1812 case 'H': 1813 case 'k': 1814 case 'K': 1815 case 'm': 1816 case 's': 1817 if (count == 1) { 1818 appendValue(field); 1819 } else if (count == 2) { 1820 appendValue(field, count); 1821 } else { 1822 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1823 } 1824 break; 1825 case 'D': 1826 if (count == 1) { 1827 appendValue(field); 1828 } else if (count <= 3) { 1829 appendValue(field, count); 1830 } else { 1831 throw new IllegalArgumentException("Too many pattern letters: " + cur); 1832 } 1833 break; 1834 default: 1835 if (count == 1) { 1836 appendValue(field); 1837 } else { 1838 appendValue(field, count); 1839 } 1840 break; 1841 } 1842 } 1843 1844 /** Map of letters to fields. */ 1845 private static final Map<Character, TemporalField> FIELD_MAP = new HashMap<>(); 1846 static { 1847 // SDF = SimpleDateFormat 1848 FIELD_MAP.put('G', ChronoField.ERA); // SDF, LDML (different to both for 1/2 chars) 1849 FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // SDF, LDML 1850 FIELD_MAP.put('u', ChronoField.YEAR); // LDML (different in SDF) 1851 FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310) 1852 FIELD_MAP.put('q', IsoFields.QUARTER_OF_YEAR); // LDML (stand-alone) 1853 FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // SDF, LDML 1854 FIELD_MAP.put('L', ChronoField.MONTH_OF_YEAR); // SDF, LDML (stand-alone) 1855 FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // SDF, LDML 1856 FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // SDF, LDML 1857 FIELD_MAP.put('F', ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH); // SDF, LDML 1858 FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // SDF, LDML (different to both for 1/2 chars) 1859 FIELD_MAP.put('c', ChronoField.DAY_OF_WEEK); // LDML (stand-alone) 1860 FIELD_MAP.put('e', ChronoField.DAY_OF_WEEK); // LDML (needs localized week number) 1861 FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // SDF, LDML 1862 FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // SDF, LDML 1863 FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // SDF, LDML 1864 FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // SDF, LDML 1865 FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // SDF, LDML 1866 FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // SDF, LDML 1867 FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // SDF, LDML 1868 FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (SDF uses milli-of-second number) 1869 FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML 1870 FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML) 1871 FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML) 1872 // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 1873 // 310 - Z - matches SimpleDateFormat and LDML 1874 // 310 - V - time-zone id, matches LDML 1875 // 310 - p - prefix for padding 1876 // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 1877 // 310 - x - matches LDML 1878 // 310 - w, W, and Y are localized forms matching LDML 1879 // LDML - U - cycle year name, not supported by 310 yet 1880 // LDML - l - deprecated 1881 // LDML - j - not relevant 1882 // LDML - g - modified-julian-day 1883 // LDML - v,V - extended time-zone names 1884 } 1885 1886 //----------------------------------------------------------------------- 1887 /** 1888 * Causes the next added printer/parser to pad to a fixed width using a space. 1889 * <p> 1890 * This padding will pad to a fixed width using spaces. 1891 * <p> 1892 * During formatting, the decorated element will be output and then padded 1893 * to the specified width. An exception will be thrown during formatting if 1894 * the pad width is exceeded. 1895 * <p> 1896 * During parsing, the padding and decorated element are parsed. 1897 * If parsing is lenient, then the pad width is treated as a maximum. 1898 * The padding is parsed greedily. Thus, if the decorated element starts with 1899 * the pad character, it will not be parsed. 1900 * 1901 * @param padWidth the pad width, 1 or greater 1902 * @return this, for chaining, not null 1903 * @throws IllegalArgumentException if pad width is too small 1904 */ padNext(int padWidth)1905 public DateTimeFormatterBuilder padNext(int padWidth) { 1906 return padNext(padWidth, ' '); 1907 } 1908 1909 /** 1910 * Causes the next added printer/parser to pad to a fixed width. 1911 * <p> 1912 * This padding is intended for padding other than zero-padding. 1913 * Zero-padding should be achieved using the appendValue methods. 1914 * <p> 1915 * During formatting, the decorated element will be output and then padded 1916 * to the specified width. An exception will be thrown during formatting if 1917 * the pad width is exceeded. 1918 * <p> 1919 * During parsing, the padding and decorated element are parsed. 1920 * If parsing is lenient, then the pad width is treated as a maximum. 1921 * If parsing is case insensitive, then the pad character is matched ignoring case. 1922 * The padding is parsed greedily. Thus, if the decorated element starts with 1923 * the pad character, it will not be parsed. 1924 * 1925 * @param padWidth the pad width, 1 or greater 1926 * @param padChar the pad character 1927 * @return this, for chaining, not null 1928 * @throws IllegalArgumentException if pad width is too small 1929 */ padNext(int padWidth, char padChar)1930 public DateTimeFormatterBuilder padNext(int padWidth, char padChar) { 1931 if (padWidth < 1) { 1932 throw new IllegalArgumentException("The pad width must be at least one but was " + padWidth); 1933 } 1934 active.padNextWidth = padWidth; 1935 active.padNextChar = padChar; 1936 active.valueParserIndex = -1; 1937 return this; 1938 } 1939 1940 //----------------------------------------------------------------------- 1941 /** 1942 * Mark the start of an optional section. 1943 * <p> 1944 * The output of formatting can include optional sections, which may be nested. 1945 * An optional section is started by calling this method and ended by calling 1946 * {@link #optionalEnd()} or by ending the build process. 1947 * <p> 1948 * All elements in the optional section are treated as optional. 1949 * During formatting, the section is only output if data is available in the 1950 * {@code TemporalAccessor} for all the elements in the section. 1951 * During parsing, the whole section may be missing from the parsed string. 1952 * <p> 1953 * For example, consider a builder setup as 1954 * {@code builder.appendValue(HOUR_OF_DAY,2).optionalStart().appendValue(MINUTE_OF_HOUR,2)}. 1955 * The optional section ends automatically at the end of the builder. 1956 * During formatting, the minute will only be output if its value can be obtained from the date-time. 1957 * During parsing, the input will be successfully parsed whether the minute is present or not. 1958 * 1959 * @return this, for chaining, not null 1960 */ optionalStart()1961 public DateTimeFormatterBuilder optionalStart() { 1962 active.valueParserIndex = -1; 1963 active = new DateTimeFormatterBuilder(active, true); 1964 return this; 1965 } 1966 1967 /** 1968 * Ends an optional section. 1969 * <p> 1970 * The output of formatting can include optional sections, which may be nested. 1971 * An optional section is started by calling {@link #optionalStart()} and ended 1972 * using this method (or at the end of the builder). 1973 * <p> 1974 * Calling this method without having previously called {@code optionalStart} 1975 * will throw an exception. 1976 * Calling this method immediately after calling {@code optionalStart} has no effect 1977 * on the formatter other than ending the (empty) optional section. 1978 * <p> 1979 * All elements in the optional section are treated as optional. 1980 * During formatting, the section is only output if data is available in the 1981 * {@code TemporalAccessor} for all the elements in the section. 1982 * During parsing, the whole section may be missing from the parsed string. 1983 * <p> 1984 * For example, consider a builder setup as 1985 * {@code builder.appendValue(HOUR_OF_DAY,2).optionalStart().appendValue(MINUTE_OF_HOUR,2).optionalEnd()}. 1986 * During formatting, the minute will only be output if its value can be obtained from the date-time. 1987 * During parsing, the input will be successfully parsed whether the minute is present or not. 1988 * 1989 * @return this, for chaining, not null 1990 * @throws IllegalStateException if there was no previous call to {@code optionalStart} 1991 */ optionalEnd()1992 public DateTimeFormatterBuilder optionalEnd() { 1993 if (active.parent == null) { 1994 throw new IllegalStateException("Cannot call optionalEnd() as there was no previous call to optionalStart()"); 1995 } 1996 if (active.printerParsers.size() > 0) { 1997 CompositePrinterParser cpp = new CompositePrinterParser(active.printerParsers, active.optional); 1998 active = active.parent; 1999 appendInternal(cpp); 2000 } else { 2001 active = active.parent; 2002 } 2003 return this; 2004 } 2005 2006 //----------------------------------------------------------------------- 2007 /** 2008 * Appends a printer and/or parser to the internal list handling padding. 2009 * 2010 * @param pp the printer-parser to add, not null 2011 * @return the index into the active parsers list 2012 */ appendInternal(DateTimePrinterParser pp)2013 private int appendInternal(DateTimePrinterParser pp) { 2014 Objects.requireNonNull(pp, "pp"); 2015 if (active.padNextWidth > 0) { 2016 if (pp != null) { 2017 pp = new PadPrinterParserDecorator(pp, active.padNextWidth, active.padNextChar); 2018 } 2019 active.padNextWidth = 0; 2020 active.padNextChar = 0; 2021 } 2022 active.printerParsers.add(pp); 2023 active.valueParserIndex = -1; 2024 return active.printerParsers.size() - 1; 2025 } 2026 2027 //----------------------------------------------------------------------- 2028 /** 2029 * Completes this builder by creating the {@code DateTimeFormatter} 2030 * using the default locale. 2031 * <p> 2032 * This will create a formatter with the {@linkplain Locale#getDefault(Locale.Category) default FORMAT locale}. 2033 * Numbers will be printed and parsed using the standard DecimalStyle. 2034 * The resolver style will be {@link ResolverStyle#SMART SMART}. 2035 * <p> 2036 * Calling this method will end any open optional sections by repeatedly 2037 * calling {@link #optionalEnd()} before creating the formatter. 2038 * <p> 2039 * This builder can still be used after creating the formatter if desired, 2040 * although the state may have been changed by calls to {@code optionalEnd}. 2041 * 2042 * @return the created formatter, not null 2043 */ toFormatter()2044 public DateTimeFormatter toFormatter() { 2045 return toFormatter(Locale.getDefault(Locale.Category.FORMAT)); 2046 } 2047 2048 /** 2049 * Completes this builder by creating the {@code DateTimeFormatter} 2050 * using the specified locale. 2051 * <p> 2052 * This will create a formatter with the specified locale. 2053 * Numbers will be printed and parsed using the standard DecimalStyle. 2054 * The resolver style will be {@link ResolverStyle#SMART SMART}. 2055 * <p> 2056 * Calling this method will end any open optional sections by repeatedly 2057 * calling {@link #optionalEnd()} before creating the formatter. 2058 * <p> 2059 * This builder can still be used after creating the formatter if desired, 2060 * although the state may have been changed by calls to {@code optionalEnd}. 2061 * 2062 * @param locale the locale to use for formatting, not null 2063 * @return the created formatter, not null 2064 */ toFormatter(Locale locale)2065 public DateTimeFormatter toFormatter(Locale locale) { 2066 return toFormatter(locale, ResolverStyle.SMART, null); 2067 } 2068 2069 /** 2070 * Completes this builder by creating the formatter. 2071 * This uses the default locale. 2072 * 2073 * @param resolverStyle the resolver style to use, not null 2074 * @return the created formatter, not null 2075 */ toFormatter(ResolverStyle resolverStyle, Chronology chrono)2076 DateTimeFormatter toFormatter(ResolverStyle resolverStyle, Chronology chrono) { 2077 return toFormatter(Locale.getDefault(Locale.Category.FORMAT), resolverStyle, chrono); 2078 } 2079 2080 /** 2081 * Completes this builder by creating the formatter. 2082 * 2083 * @param locale the locale to use for formatting, not null 2084 * @param chrono the chronology to use, may be null 2085 * @return the created formatter, not null 2086 */ toFormatter(Locale locale, ResolverStyle resolverStyle, Chronology chrono)2087 private DateTimeFormatter toFormatter(Locale locale, ResolverStyle resolverStyle, Chronology chrono) { 2088 Objects.requireNonNull(locale, "locale"); 2089 while (active.parent != null) { 2090 optionalEnd(); 2091 } 2092 CompositePrinterParser pp = new CompositePrinterParser(printerParsers, false); 2093 return new DateTimeFormatter(pp, locale, DecimalStyle.STANDARD, 2094 resolverStyle, null, chrono, null); 2095 } 2096 2097 //----------------------------------------------------------------------- 2098 /** 2099 * Strategy for formatting/parsing date-time information. 2100 * <p> 2101 * The printer may format any part, or the whole, of the input date-time object. 2102 * Typically, a complete format is constructed from a number of smaller 2103 * units, each outputting a single field. 2104 * <p> 2105 * The parser may parse any piece of text from the input, storing the result 2106 * in the context. Typically, each individual parser will just parse one 2107 * field, such as the day-of-month, storing the value in the context. 2108 * Once the parse is complete, the caller will then resolve the parsed values 2109 * to create the desired object, such as a {@code LocalDate}. 2110 * <p> 2111 * The parse position will be updated during the parse. Parsing will start at 2112 * the specified index and the return value specifies the new parse position 2113 * for the next parser. If an error occurs, the returned index will be negative 2114 * and will have the error position encoded using the complement operator. 2115 * 2116 * @implSpec 2117 * This interface must be implemented with care to ensure other classes operate correctly. 2118 * All implementations that can be instantiated must be final, immutable and thread-safe. 2119 * <p> 2120 * The context is not a thread-safe object and a new instance will be created 2121 * for each format that occurs. The context must not be stored in an instance 2122 * variable or shared with any other threads. 2123 */ 2124 interface DateTimePrinterParser { 2125 2126 /** 2127 * Prints the date-time object to the buffer. 2128 * <p> 2129 * The context holds information to use during the format. 2130 * It also contains the date-time information to be printed. 2131 * <p> 2132 * The buffer must not be mutated beyond the content controlled by the implementation. 2133 * 2134 * @param context the context to format using, not null 2135 * @param buf the buffer to append to, not null 2136 * @return false if unable to query the value from the date-time, true otherwise 2137 * @throws DateTimeException if the date-time cannot be printed successfully 2138 */ format(DateTimePrintContext context, StringBuilder buf)2139 boolean format(DateTimePrintContext context, StringBuilder buf); 2140 2141 /** 2142 * Parses text into date-time information. 2143 * <p> 2144 * The context holds information to use during the parse. 2145 * It is also used to store the parsed date-time information. 2146 * 2147 * @param context the context to use and parse into, not null 2148 * @param text the input text to parse, not null 2149 * @param position the position to start parsing at, from 0 to the text length 2150 * @return the new parse position, where negative means an error with the 2151 * error position encoded using the complement ~ operator 2152 * @throws NullPointerException if the context or text is null 2153 * @throws IndexOutOfBoundsException if the position is invalid 2154 */ parse(DateTimeParseContext context, CharSequence text, int position)2155 int parse(DateTimeParseContext context, CharSequence text, int position); 2156 } 2157 2158 //----------------------------------------------------------------------- 2159 /** 2160 * Composite printer and parser. 2161 */ 2162 static final class CompositePrinterParser implements DateTimePrinterParser { 2163 private final DateTimePrinterParser[] printerParsers; 2164 private final boolean optional; 2165 CompositePrinterParser(List<DateTimePrinterParser> printerParsers, boolean optional)2166 CompositePrinterParser(List<DateTimePrinterParser> printerParsers, boolean optional) { 2167 this(printerParsers.toArray(new DateTimePrinterParser[printerParsers.size()]), optional); 2168 } 2169 CompositePrinterParser(DateTimePrinterParser[] printerParsers, boolean optional)2170 CompositePrinterParser(DateTimePrinterParser[] printerParsers, boolean optional) { 2171 this.printerParsers = printerParsers; 2172 this.optional = optional; 2173 } 2174 2175 /** 2176 * Returns a copy of this printer-parser with the optional flag changed. 2177 * 2178 * @param optional the optional flag to set in the copy 2179 * @return the new printer-parser, not null 2180 */ withOptional(boolean optional)2181 public CompositePrinterParser withOptional(boolean optional) { 2182 if (optional == this.optional) { 2183 return this; 2184 } 2185 return new CompositePrinterParser(printerParsers, optional); 2186 } 2187 2188 @Override format(DateTimePrintContext context, StringBuilder buf)2189 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2190 int length = buf.length(); 2191 if (optional) { 2192 context.startOptional(); 2193 } 2194 try { 2195 for (DateTimePrinterParser pp : printerParsers) { 2196 if (pp.format(context, buf) == false) { 2197 buf.setLength(length); // reset buffer 2198 return true; 2199 } 2200 } 2201 } finally { 2202 if (optional) { 2203 context.endOptional(); 2204 } 2205 } 2206 return true; 2207 } 2208 2209 @Override parse(DateTimeParseContext context, CharSequence text, int position)2210 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2211 if (optional) { 2212 context.startOptional(); 2213 int pos = position; 2214 for (DateTimePrinterParser pp : printerParsers) { 2215 pos = pp.parse(context, text, pos); 2216 if (pos < 0) { 2217 context.endOptional(false); 2218 return position; // return original position 2219 } 2220 } 2221 context.endOptional(true); 2222 return pos; 2223 } else { 2224 for (DateTimePrinterParser pp : printerParsers) { 2225 position = pp.parse(context, text, position); 2226 if (position < 0) { 2227 break; 2228 } 2229 } 2230 return position; 2231 } 2232 } 2233 2234 @Override toString()2235 public String toString() { 2236 StringBuilder buf = new StringBuilder(); 2237 if (printerParsers != null) { 2238 buf.append(optional ? "[" : "("); 2239 for (DateTimePrinterParser pp : printerParsers) { 2240 buf.append(pp); 2241 } 2242 buf.append(optional ? "]" : ")"); 2243 } 2244 return buf.toString(); 2245 } 2246 } 2247 2248 //----------------------------------------------------------------------- 2249 /** 2250 * Pads the output to a fixed width. 2251 */ 2252 static final class PadPrinterParserDecorator implements DateTimePrinterParser { 2253 private final DateTimePrinterParser printerParser; 2254 private final int padWidth; 2255 private final char padChar; 2256 2257 /** 2258 * Constructor. 2259 * 2260 * @param printerParser the printer, not null 2261 * @param padWidth the width to pad to, 1 or greater 2262 * @param padChar the pad character 2263 */ PadPrinterParserDecorator(DateTimePrinterParser printerParser, int padWidth, char padChar)2264 PadPrinterParserDecorator(DateTimePrinterParser printerParser, int padWidth, char padChar) { 2265 // input checked by DateTimeFormatterBuilder 2266 this.printerParser = printerParser; 2267 this.padWidth = padWidth; 2268 this.padChar = padChar; 2269 } 2270 2271 @Override format(DateTimePrintContext context, StringBuilder buf)2272 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2273 int preLen = buf.length(); 2274 if (printerParser.format(context, buf) == false) { 2275 return false; 2276 } 2277 int len = buf.length() - preLen; 2278 if (len > padWidth) { 2279 throw new DateTimeException( 2280 "Cannot print as output of " + len + " characters exceeds pad width of " + padWidth); 2281 } 2282 for (int i = 0; i < padWidth - len; i++) { 2283 buf.insert(preLen, padChar); 2284 } 2285 return true; 2286 } 2287 2288 @Override parse(DateTimeParseContext context, CharSequence text, int position)2289 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2290 // cache context before changed by decorated parser 2291 final boolean strict = context.isStrict(); 2292 // parse 2293 if (position > text.length()) { 2294 throw new IndexOutOfBoundsException(); 2295 } 2296 if (position == text.length()) { 2297 return ~position; // no more characters in the string 2298 } 2299 int endPos = position + padWidth; 2300 if (endPos > text.length()) { 2301 if (strict) { 2302 return ~position; // not enough characters in the string to meet the parse width 2303 } 2304 endPos = text.length(); 2305 } 2306 int pos = position; 2307 while (pos < endPos && context.charEquals(text.charAt(pos), padChar)) { 2308 pos++; 2309 } 2310 text = text.subSequence(0, endPos); 2311 int resultPos = printerParser.parse(context, text, pos); 2312 if (resultPos != endPos && strict) { 2313 return ~(position + pos); // parse of decorated field didn't parse to the end 2314 } 2315 return resultPos; 2316 } 2317 2318 @Override toString()2319 public String toString() { 2320 return "Pad(" + printerParser + "," + padWidth + (padChar == ' ' ? ")" : ",'" + padChar + "')"); 2321 } 2322 } 2323 2324 //----------------------------------------------------------------------- 2325 /** 2326 * Enumeration to apply simple parse settings. 2327 */ 2328 static enum SettingsParser implements DateTimePrinterParser { 2329 SENSITIVE, 2330 INSENSITIVE, 2331 STRICT, 2332 LENIENT; 2333 2334 @Override format(DateTimePrintContext context, StringBuilder buf)2335 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2336 return true; // nothing to do here 2337 } 2338 2339 @Override parse(DateTimeParseContext context, CharSequence text, int position)2340 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2341 // using ordinals to avoid javac synthetic inner class 2342 switch (ordinal()) { 2343 case 0: context.setCaseSensitive(true); break; 2344 case 1: context.setCaseSensitive(false); break; 2345 case 2: context.setStrict(true); break; 2346 case 3: context.setStrict(false); break; 2347 } 2348 return position; 2349 } 2350 2351 @Override toString()2352 public String toString() { 2353 // using ordinals to avoid javac synthetic inner class 2354 switch (ordinal()) { 2355 case 0: return "ParseCaseSensitive(true)"; 2356 case 1: return "ParseCaseSensitive(false)"; 2357 case 2: return "ParseStrict(true)"; 2358 case 3: return "ParseStrict(false)"; 2359 } 2360 throw new IllegalStateException("Unreachable"); 2361 } 2362 } 2363 2364 //----------------------------------------------------------------------- 2365 /** 2366 * Defaults a value into the parse if not currently present. 2367 */ 2368 static class DefaultValueParser implements DateTimePrinterParser { 2369 private final TemporalField field; 2370 private final long value; 2371 DefaultValueParser(TemporalField field, long value)2372 DefaultValueParser(TemporalField field, long value) { 2373 this.field = field; 2374 this.value = value; 2375 } 2376 format(DateTimePrintContext context, StringBuilder buf)2377 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2378 return true; 2379 } 2380 parse(DateTimeParseContext context, CharSequence text, int position)2381 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2382 if (context.getParsed(field) == null) { 2383 context.setParsedField(field, value, position, position); 2384 } 2385 return position; 2386 } 2387 } 2388 2389 //----------------------------------------------------------------------- 2390 /** 2391 * Prints or parses a character literal. 2392 */ 2393 static final class CharLiteralPrinterParser implements DateTimePrinterParser { 2394 private final char literal; 2395 CharLiteralPrinterParser(char literal)2396 CharLiteralPrinterParser(char literal) { 2397 this.literal = literal; 2398 } 2399 2400 @Override format(DateTimePrintContext context, StringBuilder buf)2401 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2402 buf.append(literal); 2403 return true; 2404 } 2405 2406 @Override parse(DateTimeParseContext context, CharSequence text, int position)2407 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2408 int length = text.length(); 2409 if (position == length) { 2410 return ~position; 2411 } 2412 char ch = text.charAt(position); 2413 if (ch != literal) { 2414 if (context.isCaseSensitive() || 2415 (Character.toUpperCase(ch) != Character.toUpperCase(literal) && 2416 Character.toLowerCase(ch) != Character.toLowerCase(literal))) { 2417 return ~position; 2418 } 2419 } 2420 return position + 1; 2421 } 2422 2423 @Override toString()2424 public String toString() { 2425 if (literal == '\'') { 2426 return "''"; 2427 } 2428 return "'" + literal + "'"; 2429 } 2430 } 2431 2432 //----------------------------------------------------------------------- 2433 /** 2434 * Prints or parses a string literal. 2435 */ 2436 static final class StringLiteralPrinterParser implements DateTimePrinterParser { 2437 private final String literal; 2438 StringLiteralPrinterParser(String literal)2439 StringLiteralPrinterParser(String literal) { 2440 this.literal = literal; // validated by caller 2441 } 2442 2443 @Override format(DateTimePrintContext context, StringBuilder buf)2444 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2445 buf.append(literal); 2446 return true; 2447 } 2448 2449 @Override parse(DateTimeParseContext context, CharSequence text, int position)2450 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2451 int length = text.length(); 2452 if (position > length || position < 0) { 2453 throw new IndexOutOfBoundsException(); 2454 } 2455 if (context.subSequenceEquals(text, position, literal, 0, literal.length()) == false) { 2456 return ~position; 2457 } 2458 return position + literal.length(); 2459 } 2460 2461 @Override toString()2462 public String toString() { 2463 String converted = literal.replace("'", "''"); 2464 return "'" + converted + "'"; 2465 } 2466 } 2467 2468 //----------------------------------------------------------------------- 2469 /** 2470 * Prints and parses a numeric date-time field with optional padding. 2471 */ 2472 static class NumberPrinterParser implements DateTimePrinterParser { 2473 2474 /** 2475 * Array of 10 to the power of n. 2476 */ 2477 static final long[] EXCEED_POINTS = new long[] { 2478 0L, 2479 10L, 2480 100L, 2481 1000L, 2482 10000L, 2483 100000L, 2484 1000000L, 2485 10000000L, 2486 100000000L, 2487 1000000000L, 2488 10000000000L, 2489 }; 2490 2491 final TemporalField field; 2492 final int minWidth; 2493 final int maxWidth; 2494 private final SignStyle signStyle; 2495 final int subsequentWidth; 2496 2497 /** 2498 * Constructor. 2499 * 2500 * @param field the field to format, not null 2501 * @param minWidth the minimum field width, from 1 to 19 2502 * @param maxWidth the maximum field width, from minWidth to 19 2503 * @param signStyle the positive/negative sign style, not null 2504 */ NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle)2505 NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) { 2506 // validated by caller 2507 this.field = field; 2508 this.minWidth = minWidth; 2509 this.maxWidth = maxWidth; 2510 this.signStyle = signStyle; 2511 this.subsequentWidth = 0; 2512 } 2513 2514 /** 2515 * Constructor. 2516 * 2517 * @param field the field to format, not null 2518 * @param minWidth the minimum field width, from 1 to 19 2519 * @param maxWidth the maximum field width, from minWidth to 19 2520 * @param signStyle the positive/negative sign style, not null 2521 * @param subsequentWidth the width of subsequent non-negative numbers, 0 or greater, 2522 * -1 if fixed width due to active adjacent parsing 2523 */ NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle, int subsequentWidth)2524 protected NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle, int subsequentWidth) { 2525 // validated by caller 2526 this.field = field; 2527 this.minWidth = minWidth; 2528 this.maxWidth = maxWidth; 2529 this.signStyle = signStyle; 2530 this.subsequentWidth = subsequentWidth; 2531 } 2532 2533 /** 2534 * Returns a new instance with fixed width flag set. 2535 * 2536 * @return a new updated printer-parser, not null 2537 */ withFixedWidth()2538 NumberPrinterParser withFixedWidth() { 2539 if (subsequentWidth == -1) { 2540 return this; 2541 } 2542 return new NumberPrinterParser(field, minWidth, maxWidth, signStyle, -1); 2543 } 2544 2545 /** 2546 * Returns a new instance with an updated subsequent width. 2547 * 2548 * @param subsequentWidth the width of subsequent non-negative numbers, 0 or greater 2549 * @return a new updated printer-parser, not null 2550 */ withSubsequentWidth(int subsequentWidth)2551 NumberPrinterParser withSubsequentWidth(int subsequentWidth) { 2552 return new NumberPrinterParser(field, minWidth, maxWidth, signStyle, this.subsequentWidth + subsequentWidth); 2553 } 2554 2555 @Override format(DateTimePrintContext context, StringBuilder buf)2556 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2557 Long valueLong = context.getValue(field); 2558 if (valueLong == null) { 2559 return false; 2560 } 2561 long value = getValue(context, valueLong); 2562 DecimalStyle decimalStyle = context.getDecimalStyle(); 2563 String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value))); 2564 if (str.length() > maxWidth) { 2565 throw new DateTimeException("Field " + field + 2566 " cannot be printed as the value " + value + 2567 " exceeds the maximum print width of " + maxWidth); 2568 } 2569 str = decimalStyle.convertNumberToI18N(str); 2570 2571 if (value >= 0) { 2572 switch (signStyle) { 2573 case EXCEEDS_PAD: 2574 if (minWidth < 19 && value >= EXCEED_POINTS[minWidth]) { 2575 buf.append(decimalStyle.getPositiveSign()); 2576 } 2577 break; 2578 case ALWAYS: 2579 buf.append(decimalStyle.getPositiveSign()); 2580 break; 2581 } 2582 } else { 2583 switch (signStyle) { 2584 case NORMAL: 2585 case EXCEEDS_PAD: 2586 case ALWAYS: 2587 buf.append(decimalStyle.getNegativeSign()); 2588 break; 2589 case NOT_NEGATIVE: 2590 throw new DateTimeException("Field " + field + 2591 " cannot be printed as the value " + value + 2592 " cannot be negative according to the SignStyle"); 2593 } 2594 } 2595 for (int i = 0; i < minWidth - str.length(); i++) { 2596 buf.append(decimalStyle.getZeroDigit()); 2597 } 2598 buf.append(str); 2599 return true; 2600 } 2601 2602 /** 2603 * Gets the value to output. 2604 * 2605 * @param context the context 2606 * @param value the value of the field, not null 2607 * @return the value 2608 */ getValue(DateTimePrintContext context, long value)2609 long getValue(DateTimePrintContext context, long value) { 2610 return value; 2611 } 2612 2613 /** 2614 * For NumberPrinterParser, the width is fixed depending on the 2615 * minWidth, maxWidth, signStyle and whether subsequent fields are fixed. 2616 * @param context the context 2617 * @return true if the field is fixed width 2618 * @see DateTimeFormatterBuilder#appendValue(java.time.temporal.TemporalField, int) 2619 */ isFixedWidth(DateTimeParseContext context)2620 boolean isFixedWidth(DateTimeParseContext context) { 2621 return subsequentWidth == -1 || 2622 (subsequentWidth > 0 && minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE); 2623 } 2624 2625 @Override parse(DateTimeParseContext context, CharSequence text, int position)2626 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2627 int length = text.length(); 2628 if (position == length) { 2629 return ~position; 2630 } 2631 char sign = text.charAt(position); // IOOBE if invalid position 2632 boolean negative = false; 2633 boolean positive = false; 2634 if (sign == context.getDecimalStyle().getPositiveSign()) { 2635 if (signStyle.parse(true, context.isStrict(), minWidth == maxWidth) == false) { 2636 return ~position; 2637 } 2638 positive = true; 2639 position++; 2640 } else if (sign == context.getDecimalStyle().getNegativeSign()) { 2641 if (signStyle.parse(false, context.isStrict(), minWidth == maxWidth) == false) { 2642 return ~position; 2643 } 2644 negative = true; 2645 position++; 2646 } else { 2647 if (signStyle == SignStyle.ALWAYS && context.isStrict()) { 2648 return ~position; 2649 } 2650 } 2651 int effMinWidth = (context.isStrict() || isFixedWidth(context) ? minWidth : 1); 2652 int minEndPos = position + effMinWidth; 2653 if (minEndPos > length) { 2654 return ~position; 2655 } 2656 int effMaxWidth = (context.isStrict() || isFixedWidth(context) ? maxWidth : 9) + Math.max(subsequentWidth, 0); 2657 long total = 0; 2658 BigInteger totalBig = null; 2659 int pos = position; 2660 for (int pass = 0; pass < 2; pass++) { 2661 int maxEndPos = Math.min(pos + effMaxWidth, length); 2662 while (pos < maxEndPos) { 2663 char ch = text.charAt(pos++); 2664 int digit = context.getDecimalStyle().convertToDigit(ch); 2665 if (digit < 0) { 2666 pos--; 2667 if (pos < minEndPos) { 2668 return ~position; // need at least min width digits 2669 } 2670 break; 2671 } 2672 if ((pos - position) > 18) { 2673 if (totalBig == null) { 2674 totalBig = BigInteger.valueOf(total); 2675 } 2676 totalBig = totalBig.multiply(BigInteger.TEN).add(BigInteger.valueOf(digit)); 2677 } else { 2678 total = total * 10 + digit; 2679 } 2680 } 2681 if (subsequentWidth > 0 && pass == 0) { 2682 // re-parse now we know the correct width 2683 int parseLen = pos - position; 2684 effMaxWidth = Math.max(effMinWidth, parseLen - subsequentWidth); 2685 pos = position; 2686 total = 0; 2687 totalBig = null; 2688 } else { 2689 break; 2690 } 2691 } 2692 if (negative) { 2693 if (totalBig != null) { 2694 if (totalBig.equals(BigInteger.ZERO) && context.isStrict()) { 2695 return ~(position - 1); // minus zero not allowed 2696 } 2697 totalBig = totalBig.negate(); 2698 } else { 2699 if (total == 0 && context.isStrict()) { 2700 return ~(position - 1); // minus zero not allowed 2701 } 2702 total = -total; 2703 } 2704 } else if (signStyle == SignStyle.EXCEEDS_PAD && context.isStrict()) { 2705 int parseLen = pos - position; 2706 if (positive) { 2707 if (parseLen <= minWidth) { 2708 return ~(position - 1); // '+' only parsed if minWidth exceeded 2709 } 2710 } else { 2711 if (parseLen > minWidth) { 2712 return ~position; // '+' must be parsed if minWidth exceeded 2713 } 2714 } 2715 } 2716 if (totalBig != null) { 2717 if (totalBig.bitLength() > 63) { 2718 // overflow, parse 1 less digit 2719 totalBig = totalBig.divide(BigInteger.TEN); 2720 pos--; 2721 } 2722 return setValue(context, totalBig.longValue(), position, pos); 2723 } 2724 return setValue(context, total, position, pos); 2725 } 2726 2727 /** 2728 * Stores the value. 2729 * 2730 * @param context the context to store into, not null 2731 * @param value the value 2732 * @param errorPos the position of the field being parsed 2733 * @param successPos the position after the field being parsed 2734 * @return the new position 2735 */ setValue(DateTimeParseContext context, long value, int errorPos, int successPos)2736 int setValue(DateTimeParseContext context, long value, int errorPos, int successPos) { 2737 return context.setParsedField(field, value, errorPos, successPos); 2738 } 2739 2740 @Override toString()2741 public String toString() { 2742 if (minWidth == 1 && maxWidth == 19 && signStyle == SignStyle.NORMAL) { 2743 return "Value(" + field + ")"; 2744 } 2745 if (minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE) { 2746 return "Value(" + field + "," + minWidth + ")"; 2747 } 2748 return "Value(" + field + "," + minWidth + "," + maxWidth + "," + signStyle + ")"; 2749 } 2750 } 2751 2752 //----------------------------------------------------------------------- 2753 /** 2754 * Prints and parses a reduced numeric date-time field. 2755 */ 2756 static final class ReducedPrinterParser extends NumberPrinterParser { 2757 /** 2758 * The base date for reduced value parsing. 2759 */ 2760 static final LocalDate BASE_DATE = LocalDate.of(2000, 1, 1); 2761 2762 private final int baseValue; 2763 private final ChronoLocalDate baseDate; 2764 2765 /** 2766 * Constructor. 2767 * 2768 * @param field the field to format, validated not null 2769 * @param minWidth the minimum field width, from 1 to 10 2770 * @param maxWidth the maximum field width, from 1 to 10 2771 * @param baseValue the base value 2772 * @param baseDate the base date 2773 */ ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, int baseValue, ChronoLocalDate baseDate)2774 ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, 2775 int baseValue, ChronoLocalDate baseDate) { 2776 this(field, minWidth, maxWidth, baseValue, baseDate, 0); 2777 if (minWidth < 1 || minWidth > 10) { 2778 throw new IllegalArgumentException("The minWidth must be from 1 to 10 inclusive but was " + minWidth); 2779 } 2780 if (maxWidth < 1 || maxWidth > 10) { 2781 throw new IllegalArgumentException("The maxWidth must be from 1 to 10 inclusive but was " + minWidth); 2782 } 2783 if (maxWidth < minWidth) { 2784 throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but " + 2785 maxWidth + " < " + minWidth); 2786 } 2787 if (baseDate == null) { 2788 if (field.range().isValidValue(baseValue) == false) { 2789 throw new IllegalArgumentException("The base value must be within the range of the field"); 2790 } 2791 if ((((long) baseValue) + EXCEED_POINTS[maxWidth]) > Integer.MAX_VALUE) { 2792 throw new DateTimeException("Unable to add printer-parser as the range exceeds the capacity of an int"); 2793 } 2794 } 2795 } 2796 2797 /** 2798 * Constructor. 2799 * The arguments have already been checked. 2800 * 2801 * @param field the field to format, validated not null 2802 * @param minWidth the minimum field width, from 1 to 10 2803 * @param maxWidth the maximum field width, from 1 to 10 2804 * @param baseValue the base value 2805 * @param baseDate the base date 2806 * @param subsequentWidth the subsequentWidth for this instance 2807 */ ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, int baseValue, ChronoLocalDate baseDate, int subsequentWidth)2808 private ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, 2809 int baseValue, ChronoLocalDate baseDate, int subsequentWidth) { 2810 super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth); 2811 this.baseValue = baseValue; 2812 this.baseDate = baseDate; 2813 } 2814 2815 @Override getValue(DateTimePrintContext context, long value)2816 long getValue(DateTimePrintContext context, long value) { 2817 long absValue = Math.abs(value); 2818 int baseValue = this.baseValue; 2819 if (baseDate != null) { 2820 Chronology chrono = Chronology.from(context.getTemporal()); 2821 baseValue = chrono.date(baseDate).get(field); 2822 } 2823 if (value >= baseValue && value < baseValue + EXCEED_POINTS[minWidth]) { 2824 // Use the reduced value if it fits in minWidth 2825 return absValue % EXCEED_POINTS[minWidth]; 2826 } 2827 // Otherwise truncate to fit in maxWidth 2828 return absValue % EXCEED_POINTS[maxWidth]; 2829 } 2830 2831 @Override setValue(DateTimeParseContext context, long value, int errorPos, int successPos)2832 int setValue(DateTimeParseContext context, long value, int errorPos, int successPos) { 2833 int baseValue = this.baseValue; 2834 if (baseDate != null) { 2835 Chronology chrono = context.getEffectiveChronology(); 2836 baseValue = chrono.date(baseDate).get(field); 2837 2838 // In case the Chronology is changed later, add a callback when/if it changes 2839 final long initialValue = value; 2840 context.addChronoChangedListener( 2841 (_unused) -> { 2842 /* Repeat the set of the field using the current Chronology 2843 * The success/error position is ignored because the value is 2844 * intentionally being overwritten. 2845 */ 2846 setValue(context, initialValue, errorPos, successPos); 2847 }); 2848 } 2849 int parseLen = successPos - errorPos; 2850 if (parseLen == minWidth && value >= 0) { 2851 long range = EXCEED_POINTS[minWidth]; 2852 long lastPart = baseValue % range; 2853 long basePart = baseValue - lastPart; 2854 if (baseValue > 0) { 2855 value = basePart + value; 2856 } else { 2857 value = basePart - value; 2858 } 2859 if (value < baseValue) { 2860 value += range; 2861 } 2862 } 2863 return context.setParsedField(field, value, errorPos, successPos); 2864 } 2865 2866 /** 2867 * Returns a new instance with fixed width flag set. 2868 * 2869 * @return a new updated printer-parser, not null 2870 */ 2871 @Override withFixedWidth()2872 ReducedPrinterParser withFixedWidth() { 2873 if (subsequentWidth == -1) { 2874 return this; 2875 } 2876 return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, baseDate, -1); 2877 } 2878 2879 /** 2880 * Returns a new instance with an updated subsequent width. 2881 * 2882 * @param subsequentWidth the width of subsequent non-negative numbers, 0 or greater 2883 * @return a new updated printer-parser, not null 2884 */ 2885 @Override withSubsequentWidth(int subsequentWidth)2886 ReducedPrinterParser withSubsequentWidth(int subsequentWidth) { 2887 return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, baseDate, 2888 this.subsequentWidth + subsequentWidth); 2889 } 2890 2891 /** 2892 * For a ReducedPrinterParser, fixed width is false if the mode is strict, 2893 * otherwise it is set as for NumberPrinterParser. 2894 * @param context the context 2895 * @return if the field is fixed width 2896 * @see DateTimeFormatterBuilder#appendValueReduced(java.time.temporal.TemporalField, int, int, int) 2897 */ 2898 @Override isFixedWidth(DateTimeParseContext context)2899 boolean isFixedWidth(DateTimeParseContext context) { 2900 if (context.isStrict() == false) { 2901 return false; 2902 } 2903 return super.isFixedWidth(context); 2904 } 2905 2906 @Override toString()2907 public String toString() { 2908 return "ReducedValue(" + field + "," + minWidth + "," + maxWidth + "," + (baseDate != null ? baseDate : baseValue) + ")"; 2909 } 2910 } 2911 2912 //----------------------------------------------------------------------- 2913 /** 2914 * Prints and parses a numeric date-time field with optional padding. 2915 */ 2916 static final class FractionPrinterParser implements DateTimePrinterParser { 2917 private final TemporalField field; 2918 private final int minWidth; 2919 private final int maxWidth; 2920 private final boolean decimalPoint; 2921 2922 /** 2923 * Constructor. 2924 * 2925 * @param field the field to output, not null 2926 * @param minWidth the minimum width to output, from 0 to 9 2927 * @param maxWidth the maximum width to output, from 0 to 9 2928 * @param decimalPoint whether to output the localized decimal point symbol 2929 */ FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint)2930 FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint) { 2931 Objects.requireNonNull(field, "field"); 2932 if (field.range().isFixed() == false) { 2933 throw new IllegalArgumentException("Field must have a fixed set of values: " + field); 2934 } 2935 if (minWidth < 0 || minWidth > 9) { 2936 throw new IllegalArgumentException("Minimum width must be from 0 to 9 inclusive but was " + minWidth); 2937 } 2938 if (maxWidth < 1 || maxWidth > 9) { 2939 throw new IllegalArgumentException("Maximum width must be from 1 to 9 inclusive but was " + maxWidth); 2940 } 2941 if (maxWidth < minWidth) { 2942 throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but " + 2943 maxWidth + " < " + minWidth); 2944 } 2945 this.field = field; 2946 this.minWidth = minWidth; 2947 this.maxWidth = maxWidth; 2948 this.decimalPoint = decimalPoint; 2949 } 2950 2951 @Override format(DateTimePrintContext context, StringBuilder buf)2952 public boolean format(DateTimePrintContext context, StringBuilder buf) { 2953 Long value = context.getValue(field); 2954 if (value == null) { 2955 return false; 2956 } 2957 DecimalStyle decimalStyle = context.getDecimalStyle(); 2958 BigDecimal fraction = convertToFraction(value); 2959 if (fraction.scale() == 0) { // scale is zero if value is zero 2960 if (minWidth > 0) { 2961 if (decimalPoint) { 2962 buf.append(decimalStyle.getDecimalSeparator()); 2963 } 2964 for (int i = 0; i < minWidth; i++) { 2965 buf.append(decimalStyle.getZeroDigit()); 2966 } 2967 } 2968 } else { 2969 int outputScale = Math.min(Math.max(fraction.scale(), minWidth), maxWidth); 2970 fraction = fraction.setScale(outputScale, RoundingMode.FLOOR); 2971 String str = fraction.toPlainString().substring(2); 2972 str = decimalStyle.convertNumberToI18N(str); 2973 if (decimalPoint) { 2974 buf.append(decimalStyle.getDecimalSeparator()); 2975 } 2976 buf.append(str); 2977 } 2978 return true; 2979 } 2980 2981 @Override parse(DateTimeParseContext context, CharSequence text, int position)2982 public int parse(DateTimeParseContext context, CharSequence text, int position) { 2983 int effectiveMin = (context.isStrict() ? minWidth : 0); 2984 int effectiveMax = (context.isStrict() ? maxWidth : 9); 2985 int length = text.length(); 2986 if (position == length) { 2987 // valid if whole field is optional, invalid if minimum width 2988 return (effectiveMin > 0 ? ~position : position); 2989 } 2990 if (decimalPoint) { 2991 if (text.charAt(position) != context.getDecimalStyle().getDecimalSeparator()) { 2992 // valid if whole field is optional, invalid if minimum width 2993 return (effectiveMin > 0 ? ~position : position); 2994 } 2995 position++; 2996 } 2997 int minEndPos = position + effectiveMin; 2998 if (minEndPos > length) { 2999 return ~position; // need at least min width digits 3000 } 3001 int maxEndPos = Math.min(position + effectiveMax, length); 3002 int total = 0; // can use int because we are only parsing up to 9 digits 3003 int pos = position; 3004 while (pos < maxEndPos) { 3005 char ch = text.charAt(pos++); 3006 int digit = context.getDecimalStyle().convertToDigit(ch); 3007 if (digit < 0) { 3008 if (pos < minEndPos) { 3009 return ~position; // need at least min width digits 3010 } 3011 pos--; 3012 break; 3013 } 3014 total = total * 10 + digit; 3015 } 3016 BigDecimal fraction = new BigDecimal(total).movePointLeft(pos - position); 3017 long value = convertFromFraction(fraction); 3018 return context.setParsedField(field, value, position, pos); 3019 } 3020 3021 /** 3022 * Converts a value for this field to a fraction between 0 and 1. 3023 * <p> 3024 * The fractional value is between 0 (inclusive) and 1 (exclusive). 3025 * It can only be returned if the {@link java.time.temporal.TemporalField#range() value range} is fixed. 3026 * The fraction is obtained by calculation from the field range using 9 decimal 3027 * places and a rounding mode of {@link RoundingMode#FLOOR FLOOR}. 3028 * The calculation is inaccurate if the values do not run continuously from smallest to largest. 3029 * <p> 3030 * For example, the second-of-minute value of 15 would be returned as 0.25, 3031 * assuming the standard definition of 60 seconds in a minute. 3032 * 3033 * @param value the value to convert, must be valid for this rule 3034 * @return the value as a fraction within the range, from 0 to 1, not null 3035 * @throws DateTimeException if the value cannot be converted to a fraction 3036 */ convertToFraction(long value)3037 private BigDecimal convertToFraction(long value) { 3038 ValueRange range = field.range(); 3039 range.checkValidValue(value, field); 3040 BigDecimal minBD = BigDecimal.valueOf(range.getMinimum()); 3041 BigDecimal rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE); 3042 BigDecimal valueBD = BigDecimal.valueOf(value).subtract(minBD); 3043 BigDecimal fraction = valueBD.divide(rangeBD, 9, RoundingMode.FLOOR); 3044 // stripTrailingZeros bug 3045 return fraction.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : fraction.stripTrailingZeros(); 3046 } 3047 3048 /** 3049 * Converts a fraction from 0 to 1 for this field to a value. 3050 * <p> 3051 * The fractional value must be between 0 (inclusive) and 1 (exclusive). 3052 * It can only be returned if the {@link java.time.temporal.TemporalField#range() value range} is fixed. 3053 * The value is obtained by calculation from the field range and a rounding 3054 * mode of {@link RoundingMode#FLOOR FLOOR}. 3055 * The calculation is inaccurate if the values do not run continuously from smallest to largest. 3056 * <p> 3057 * For example, the fractional second-of-minute of 0.25 would be converted to 15, 3058 * assuming the standard definition of 60 seconds in a minute. 3059 * 3060 * @param fraction the fraction to convert, not null 3061 * @return the value of the field, valid for this rule 3062 * @throws DateTimeException if the value cannot be converted 3063 */ convertFromFraction(BigDecimal fraction)3064 private long convertFromFraction(BigDecimal fraction) { 3065 ValueRange range = field.range(); 3066 BigDecimal minBD = BigDecimal.valueOf(range.getMinimum()); 3067 BigDecimal rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE); 3068 BigDecimal valueBD = fraction.multiply(rangeBD).setScale(0, RoundingMode.FLOOR).add(minBD); 3069 return valueBD.longValueExact(); 3070 } 3071 3072 @Override toString()3073 public String toString() { 3074 String decimal = (decimalPoint ? ",DecimalPoint" : ""); 3075 return "Fraction(" + field + "," + minWidth + "," + maxWidth + decimal + ")"; 3076 } 3077 } 3078 3079 //----------------------------------------------------------------------- 3080 /** 3081 * Prints or parses field text. 3082 */ 3083 static final class TextPrinterParser implements DateTimePrinterParser { 3084 private final TemporalField field; 3085 private final TextStyle textStyle; 3086 private final DateTimeTextProvider provider; 3087 /** 3088 * The cached number printer parser. 3089 * Immutable and volatile, so no synchronization needed. 3090 */ 3091 private volatile NumberPrinterParser numberPrinterParser; 3092 3093 /** 3094 * Constructor. 3095 * 3096 * @param field the field to output, not null 3097 * @param textStyle the text style, not null 3098 * @param provider the text provider, not null 3099 */ TextPrinterParser(TemporalField field, TextStyle textStyle, DateTimeTextProvider provider)3100 TextPrinterParser(TemporalField field, TextStyle textStyle, DateTimeTextProvider provider) { 3101 // validated by caller 3102 this.field = field; 3103 this.textStyle = textStyle; 3104 this.provider = provider; 3105 } 3106 3107 @Override format(DateTimePrintContext context, StringBuilder buf)3108 public boolean format(DateTimePrintContext context, StringBuilder buf) { 3109 Long value = context.getValue(field); 3110 if (value == null) { 3111 return false; 3112 } 3113 String text; 3114 Chronology chrono = context.getTemporal().query(TemporalQueries.chronology()); 3115 if (chrono == null || chrono == IsoChronology.INSTANCE) { 3116 text = provider.getText(field, value, textStyle, context.getLocale()); 3117 } else { 3118 text = provider.getText(chrono, field, value, textStyle, context.getLocale()); 3119 } 3120 if (text == null) { 3121 return numberPrinterParser().format(context, buf); 3122 } 3123 buf.append(text); 3124 return true; 3125 } 3126 3127 @Override parse(DateTimeParseContext context, CharSequence parseText, int position)3128 public int parse(DateTimeParseContext context, CharSequence parseText, int position) { 3129 int length = parseText.length(); 3130 if (position < 0 || position > length) { 3131 throw new IndexOutOfBoundsException(); 3132 } 3133 TextStyle style = (context.isStrict() ? textStyle : null); 3134 Chronology chrono = context.getEffectiveChronology(); 3135 Iterator<Entry<String, Long>> it; 3136 if (chrono == null || chrono == IsoChronology.INSTANCE) { 3137 it = provider.getTextIterator(field, style, context.getLocale()); 3138 } else { 3139 it = provider.getTextIterator(chrono, field, style, context.getLocale()); 3140 } 3141 if (it != null) { 3142 while (it.hasNext()) { 3143 Entry<String, Long> entry = it.next(); 3144 String itText = entry.getKey(); 3145 if (context.subSequenceEquals(itText, 0, parseText, position, itText.length())) { 3146 return context.setParsedField(field, entry.getValue(), position, position + itText.length()); 3147 } 3148 } 3149 if (context.isStrict()) { 3150 return ~position; 3151 } 3152 } 3153 return numberPrinterParser().parse(context, parseText, position); 3154 } 3155 3156 /** 3157 * Create and cache a number printer parser. 3158 * @return the number printer parser for this field, not null 3159 */ numberPrinterParser()3160 private NumberPrinterParser numberPrinterParser() { 3161 if (numberPrinterParser == null) { 3162 numberPrinterParser = new NumberPrinterParser(field, 1, 19, SignStyle.NORMAL); 3163 } 3164 return numberPrinterParser; 3165 } 3166 3167 @Override toString()3168 public String toString() { 3169 if (textStyle == TextStyle.FULL) { 3170 return "Text(" + field + ")"; 3171 } 3172 return "Text(" + field + "," + textStyle + ")"; 3173 } 3174 } 3175 3176 //----------------------------------------------------------------------- 3177 /** 3178 * Prints or parses an ISO-8601 instant. 3179 */ 3180 static final class InstantPrinterParser implements DateTimePrinterParser { 3181 // days in a 400 year cycle = 146097 3182 // days in a 10,000 year cycle = 146097 * 25 3183 // seconds per day = 86400 3184 private static final long SECONDS_PER_10000_YEARS = 146097L * 25L * 86400L; 3185 private static final long SECONDS_0000_TO_1970 = ((146097L * 5L) - (30L * 365L + 7L)) * 86400L; 3186 private final int fractionalDigits; 3187 InstantPrinterParser(int fractionalDigits)3188 InstantPrinterParser(int fractionalDigits) { 3189 this.fractionalDigits = fractionalDigits; 3190 } 3191 3192 @Override format(DateTimePrintContext context, StringBuilder buf)3193 public boolean format(DateTimePrintContext context, StringBuilder buf) { 3194 // use INSTANT_SECONDS, thus this code is not bound by Instant.MAX 3195 Long inSecs = context.getValue(INSTANT_SECONDS); 3196 Long inNanos = null; 3197 if (context.getTemporal().isSupported(NANO_OF_SECOND)) { 3198 inNanos = context.getTemporal().getLong(NANO_OF_SECOND); 3199 } 3200 if (inSecs == null) { 3201 return false; 3202 } 3203 long inSec = inSecs; 3204 int inNano = NANO_OF_SECOND.checkValidIntValue(inNanos != null ? inNanos : 0); 3205 // format mostly using LocalDateTime.toString 3206 if (inSec >= -SECONDS_0000_TO_1970) { 3207 // current era 3208 long zeroSecs = inSec - SECONDS_PER_10000_YEARS + SECONDS_0000_TO_1970; 3209 long hi = Math.floorDiv(zeroSecs, SECONDS_PER_10000_YEARS) + 1; 3210 long lo = Math.floorMod(zeroSecs, SECONDS_PER_10000_YEARS); 3211 LocalDateTime ldt = LocalDateTime.ofEpochSecond(lo - SECONDS_0000_TO_1970, 0, ZoneOffset.UTC); 3212 if (hi > 0) { 3213 buf.append('+').append(hi); 3214 } 3215 buf.append(ldt); 3216 if (ldt.getSecond() == 0) { 3217 buf.append(":00"); 3218 } 3219 } else { 3220 // before current era 3221 long zeroSecs = inSec + SECONDS_0000_TO_1970; 3222 long hi = zeroSecs / SECONDS_PER_10000_YEARS; 3223 long lo = zeroSecs % SECONDS_PER_10000_YEARS; 3224 LocalDateTime ldt = LocalDateTime.ofEpochSecond(lo - SECONDS_0000_TO_1970, 0, ZoneOffset.UTC); 3225 int pos = buf.length(); 3226 buf.append(ldt); 3227 if (ldt.getSecond() == 0) { 3228 buf.append(":00"); 3229 } 3230 if (hi < 0) { 3231 if (ldt.getYear() == -10_000) { 3232 buf.replace(pos, pos + 2, Long.toString(hi - 1)); 3233 } else if (lo == 0) { 3234 buf.insert(pos, hi); 3235 } else { 3236 buf.insert(pos + 1, Math.abs(hi)); 3237 } 3238 } 3239 } 3240 // add fraction 3241 if ((fractionalDigits < 0 && inNano > 0) || fractionalDigits > 0) { 3242 buf.append('.'); 3243 int div = 100_000_000; 3244 for (int i = 0; ((fractionalDigits == -1 && inNano > 0) || 3245 (fractionalDigits == -2 && (inNano > 0 || (i % 3) != 0)) || 3246 i < fractionalDigits); i++) { 3247 int digit = inNano / div; 3248 buf.append((char) (digit + '0')); 3249 inNano = inNano - (digit * div); 3250 div = div / 10; 3251 } 3252 } 3253 buf.append('Z'); 3254 return true; 3255 } 3256 3257 @Override parse(DateTimeParseContext context, CharSequence text, int position)3258 public int parse(DateTimeParseContext context, CharSequence text, int position) { 3259 // new context to avoid overwriting fields like year/month/day 3260 int minDigits = (fractionalDigits < 0 ? 0 : fractionalDigits); 3261 int maxDigits = (fractionalDigits < 0 ? 9 : fractionalDigits); 3262 CompositePrinterParser parser = new DateTimeFormatterBuilder() 3263 .append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral('T') 3264 .appendValue(HOUR_OF_DAY, 2).appendLiteral(':') 3265 .appendValue(MINUTE_OF_HOUR, 2).appendLiteral(':') 3266 .appendValue(SECOND_OF_MINUTE, 2) 3267 .appendFraction(NANO_OF_SECOND, minDigits, maxDigits, true) 3268 .appendLiteral('Z') 3269 .toFormatter().toPrinterParser(false); 3270 DateTimeParseContext newContext = context.copy(); 3271 int pos = parser.parse(newContext, text, position); 3272 if (pos < 0) { 3273 return pos; 3274 } 3275 // parser restricts most fields to 2 digits, so definitely int 3276 // correctly parsed nano is also guaranteed to be valid 3277 long yearParsed = newContext.getParsed(YEAR); 3278 int month = newContext.getParsed(MONTH_OF_YEAR).intValue(); 3279 int day = newContext.getParsed(DAY_OF_MONTH).intValue(); 3280 int hour = newContext.getParsed(HOUR_OF_DAY).intValue(); 3281 int min = newContext.getParsed(MINUTE_OF_HOUR).intValue(); 3282 Long secVal = newContext.getParsed(SECOND_OF_MINUTE); 3283 Long nanoVal = newContext.getParsed(NANO_OF_SECOND); 3284 int sec = (secVal != null ? secVal.intValue() : 0); 3285 int nano = (nanoVal != null ? nanoVal.intValue() : 0); 3286 int days = 0; 3287 if (hour == 24 && min == 0 && sec == 0 && nano == 0) { 3288 hour = 0; 3289 days = 1; 3290 } else if (hour == 23 && min == 59 && sec == 60) { 3291 context.setParsedLeapSecond(); 3292 sec = 59; 3293 } 3294 int year = (int) yearParsed % 10_000; 3295 long instantSecs; 3296 try { 3297 LocalDateTime ldt = LocalDateTime.of(year, month, day, hour, min, sec, 0).plusDays(days); 3298 instantSecs = ldt.toEpochSecond(ZoneOffset.UTC); 3299 instantSecs += Math.multiplyExact(yearParsed / 10_000L, SECONDS_PER_10000_YEARS); 3300 } catch (RuntimeException ex) { 3301 return ~position; 3302 } 3303 int successPos = pos; 3304 successPos = context.setParsedField(INSTANT_SECONDS, instantSecs, position, successPos); 3305 return context.setParsedField(NANO_OF_SECOND, nano, position, successPos); 3306 } 3307 3308 @Override toString()3309 public String toString() { 3310 return "Instant()"; 3311 } 3312 } 3313 3314 //----------------------------------------------------------------------- 3315 /** 3316 * Prints or parses an offset ID. 3317 */ 3318 static final class OffsetIdPrinterParser implements DateTimePrinterParser { 3319 static final String[] PATTERNS = new String[] { 3320 "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", 3321 }; // order used in pattern builder 3322 static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z"); 3323 static final OffsetIdPrinterParser INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0"); 3324 3325 private final String noOffsetText; 3326 private final int type; 3327 3328 /** 3329 * Constructor. 3330 * 3331 * @param pattern the pattern 3332 * @param noOffsetText the text to use for UTC, not null 3333 */ OffsetIdPrinterParser(String pattern, String noOffsetText)3334 OffsetIdPrinterParser(String pattern, String noOffsetText) { 3335 Objects.requireNonNull(pattern, "pattern"); 3336 Objects.requireNonNull(noOffsetText, "noOffsetText"); 3337 this.type = checkPattern(pattern); 3338 this.noOffsetText = noOffsetText; 3339 } 3340 checkPattern(String pattern)3341 private int checkPattern(String pattern) { 3342 for (int i = 0; i < PATTERNS.length; i++) { 3343 if (PATTERNS[i].equals(pattern)) { 3344 return i; 3345 } 3346 } 3347 throw new IllegalArgumentException("Invalid zone offset pattern: " + pattern); 3348 } 3349 3350 @Override format(DateTimePrintContext context, StringBuilder buf)3351 public boolean format(DateTimePrintContext context, StringBuilder buf) { 3352 Long offsetSecs = context.getValue(OFFSET_SECONDS); 3353 if (offsetSecs == null) { 3354 return false; 3355 } 3356 int totalSecs = Math.toIntExact(offsetSecs); 3357 if (totalSecs == 0) { 3358 buf.append(noOffsetText); 3359 } else { 3360 int absHours = Math.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped 3361 int absMinutes = Math.abs((totalSecs / 60) % 60); 3362 int absSeconds = Math.abs(totalSecs % 60); 3363 int bufPos = buf.length(); 3364 int output = absHours; 3365 buf.append(totalSecs < 0 ? "-" : "+") 3366 .append((char) (absHours / 10 + '0')).append((char) (absHours % 10 + '0')); 3367 if (type >= 3 || (type >= 1 && absMinutes > 0)) { 3368 buf.append((type % 2) == 0 ? ":" : "") 3369 .append((char) (absMinutes / 10 + '0')).append((char) (absMinutes % 10 + '0')); 3370 output += absMinutes; 3371 if (type >= 7 || (type >= 5 && absSeconds > 0)) { 3372 buf.append((type % 2) == 0 ? ":" : "") 3373 .append((char) (absSeconds / 10 + '0')).append((char) (absSeconds % 10 + '0')); 3374 output += absSeconds; 3375 } 3376 } 3377 if (output == 0) { 3378 buf.setLength(bufPos); 3379 buf.append(noOffsetText); 3380 } 3381 } 3382 return true; 3383 } 3384 3385 @Override parse(DateTimeParseContext context, CharSequence text, int position)3386 public int parse(DateTimeParseContext context, CharSequence text, int position) { 3387 int length = text.length(); 3388 int noOffsetLen = noOffsetText.length(); 3389 if (noOffsetLen == 0) { 3390 if (position == length) { 3391 return context.setParsedField(OFFSET_SECONDS, 0, position, position); 3392 } 3393 } else { 3394 if (position == length) { 3395 return ~position; 3396 } 3397 if (context.subSequenceEquals(text, position, noOffsetText, 0, noOffsetLen)) { 3398 return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen); 3399 } 3400 } 3401 3402 // parse normal plus/minus offset 3403 char sign = text.charAt(position); // IOOBE if invalid position 3404 if (sign == '+' || sign == '-') { 3405 // starts 3406 int negative = (sign == '-' ? -1 : 1); 3407 int[] array = new int[4]; 3408 array[0] = position + 1; 3409 if ((parseNumber(array, 1, text, true) || 3410 parseNumber(array, 2, text, type >=3) || 3411 parseNumber(array, 3, text, false)) == false) { 3412 // success 3413 long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]); 3414 return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, array[0]); 3415 } 3416 } 3417 // handle special case of empty no offset text 3418 if (noOffsetLen == 0) { 3419 return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen); 3420 } 3421 return ~position; 3422 } 3423 3424 /** 3425 * Parse a two digit zero-prefixed number. 3426 * 3427 * @param array the array of parsed data, 0=pos,1=hours,2=mins,3=secs, not null 3428 * @param arrayIndex the index to parse the value into 3429 * @param parseText the offset ID, not null 3430 * @param required whether this number is required 3431 * @return true if an error occurred 3432 */ parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required)3433 private boolean parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required) { 3434 if ((type + 3) / 2 < arrayIndex) { 3435 return false; // ignore seconds/minutes 3436 } 3437 int pos = array[0]; 3438 if ((type % 2) == 0 && arrayIndex > 1) { 3439 if (pos + 1 > parseText.length() || parseText.charAt(pos) != ':') { 3440 return required; 3441 } 3442 pos++; 3443 } 3444 if (pos + 2 > parseText.length()) { 3445 return required; 3446 } 3447 char ch1 = parseText.charAt(pos++); 3448 char ch2 = parseText.charAt(pos++); 3449 if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') { 3450 return required; 3451 } 3452 int value = (ch1 - 48) * 10 + (ch2 - 48); 3453 if (value < 0 || value > 59) { 3454 return required; 3455 } 3456 array[arrayIndex] = value; 3457 array[0] = pos; 3458 return false; 3459 } 3460 3461 @Override toString()3462 public String toString() { 3463 String converted = noOffsetText.replace("'", "''"); 3464 return "Offset(" + PATTERNS[type] + ",'" + converted + "')"; 3465 } 3466 } 3467 3468 //----------------------------------------------------------------------- 3469 /** 3470 * Prints or parses an offset ID. 3471 */ 3472 static final class LocalizedOffsetIdPrinterParser implements DateTimePrinterParser { 3473 private final TextStyle style; 3474 3475 /** 3476 * Constructor. 3477 * 3478 * @param style the style, not null 3479 */ LocalizedOffsetIdPrinterParser(TextStyle style)3480 LocalizedOffsetIdPrinterParser(TextStyle style) { 3481 this.style = style; 3482 } 3483 appendHMS(StringBuilder buf, int t)3484 private static StringBuilder appendHMS(StringBuilder buf, int t) { 3485 return buf.append((char)(t / 10 + '0')) 3486 .append((char)(t % 10 + '0')); 3487 } 3488 3489 @Override format(DateTimePrintContext context, StringBuilder buf)3490 public boolean format(DateTimePrintContext context, StringBuilder buf) { 3491 Long offsetSecs = context.getValue(OFFSET_SECONDS); 3492 if (offsetSecs == null) { 3493 return false; 3494 } 3495 String gmtText = "GMT"; // TODO: get localized version of 'GMT' 3496 if (gmtText != null) { 3497 buf.append(gmtText); 3498 } 3499 int totalSecs = Math.toIntExact(offsetSecs); 3500 if (totalSecs != 0) { 3501 int absHours = Math.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped 3502 int absMinutes = Math.abs((totalSecs / 60) % 60); 3503 int absSeconds = Math.abs(totalSecs % 60); 3504 buf.append(totalSecs < 0 ? "-" : "+"); 3505 if (style == TextStyle.FULL) { 3506 appendHMS(buf, absHours); 3507 buf.append(':'); 3508 appendHMS(buf, absMinutes); 3509 if (absSeconds != 0) { 3510 buf.append(':'); 3511 appendHMS(buf, absSeconds); 3512 } 3513 } else { 3514 if (absHours >= 10) { 3515 buf.append((char)(absHours / 10 + '0')); 3516 } 3517 buf.append((char)(absHours % 10 + '0')); 3518 if (absMinutes != 0 || absSeconds != 0) { 3519 buf.append(':'); 3520 appendHMS(buf, absMinutes); 3521 if (absSeconds != 0) { 3522 buf.append(':'); 3523 appendHMS(buf, absSeconds); 3524 } 3525 } 3526 } 3527 } 3528 return true; 3529 } 3530 getDigit(CharSequence text, int position)3531 int getDigit(CharSequence text, int position) { 3532 char c = text.charAt(position); 3533 if (c < '0' || c > '9') { 3534 return -1; 3535 } 3536 return c - '0'; 3537 } 3538 3539 @Override parse(DateTimeParseContext context, CharSequence text, int position)3540 public int parse(DateTimeParseContext context, CharSequence text, int position) { 3541 int pos = position; 3542 int end = pos + text.length(); 3543 String gmtText = "GMT"; // TODO: get localized version of 'GMT' 3544 if (gmtText != null) { 3545 if (!context.subSequenceEquals(text, pos, gmtText, 0, gmtText.length())) { 3546 return ~position; 3547 } 3548 pos += gmtText.length(); 3549 } 3550 // parse normal plus/minus offset 3551 int negative = 0; 3552 if (pos == end) { 3553 return context.setParsedField(OFFSET_SECONDS, 0, position, pos); 3554 } 3555 char sign = text.charAt(pos); // IOOBE if invalid position 3556 if (sign == '+') { 3557 negative = 1; 3558 } else if (sign == '-') { 3559 negative = -1; 3560 } else { 3561 return context.setParsedField(OFFSET_SECONDS, 0, position, pos); 3562 } 3563 pos++; 3564 int h = 0; 3565 int m = 0; 3566 int s = 0; 3567 if (style == TextStyle.FULL) { 3568 int h1 = getDigit(text, pos++); 3569 int h2 = getDigit(text, pos++); 3570 if (h1 < 0 || h2 < 0 || text.charAt(pos++) != ':') { 3571 return ~position; 3572 } 3573 h = h1 * 10 + h2; 3574 int m1 = getDigit(text, pos++); 3575 int m2 = getDigit(text, pos++); 3576 if (m1 < 0 || m2 < 0) { 3577 return ~position; 3578 } 3579 m = m1 * 10 + m2; 3580 if (pos + 2 < end && text.charAt(pos) == ':') { 3581 int s1 = getDigit(text, pos + 1); 3582 int s2 = getDigit(text, pos + 2); 3583 if (s1 >= 0 && s2 >= 0) { 3584 s = s1 * 10 + s2; 3585 pos += 3; 3586 } 3587 } 3588 } else { 3589 h = getDigit(text, pos++); 3590 if (h < 0) { 3591 return ~position; 3592 } 3593 if (pos < end) { 3594 int h2 = getDigit(text, pos); 3595 if (h2 >=0) { 3596 h = h * 10 + h2; 3597 pos++; 3598 } 3599 if (pos + 2 < end && text.charAt(pos) == ':') { 3600 if (pos + 2 < end && text.charAt(pos) == ':') { 3601 int m1 = getDigit(text, pos + 1); 3602 int m2 = getDigit(text, pos + 2); 3603 if (m1 >= 0 && m2 >= 0) { 3604 m = m1 * 10 + m2; 3605 pos += 3; 3606 if (pos + 2 < end && text.charAt(pos) == ':') { 3607 int s1 = getDigit(text, pos + 1); 3608 int s2 = getDigit(text, pos + 2); 3609 if (s1 >= 0 && s2 >= 0) { 3610 s = s1 * 10 + s2; 3611 pos += 3; 3612 } 3613 } 3614 } 3615 } 3616 } 3617 } 3618 } 3619 long offsetSecs = negative * (h * 3600L + m * 60L + s); 3620 return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, pos); 3621 } 3622 3623 @Override toString()3624 public String toString() { 3625 return "LocalizedOffset(" + style + ")"; 3626 } 3627 } 3628 3629 //----------------------------------------------------------------------- 3630 /** 3631 * Prints or parses a zone ID. 3632 */ 3633 static final class ZoneTextPrinterParser extends ZoneIdPrinterParser { 3634 3635 /** The text style to output. */ 3636 private final TextStyle textStyle; 3637 3638 /** The preferred zoneid map */ 3639 private Set<String> preferredZones; 3640 ZoneTextPrinterParser(TextStyle textStyle, Set<ZoneId> preferredZones)3641 ZoneTextPrinterParser(TextStyle textStyle, Set<ZoneId> preferredZones) { 3642 super(TemporalQueries.zone(), "ZoneText(" + textStyle + ")"); 3643 this.textStyle = Objects.requireNonNull(textStyle, "textStyle"); 3644 if (preferredZones != null && preferredZones.size() != 0) { 3645 this.preferredZones = new HashSet<>(); 3646 for (ZoneId id : preferredZones) { 3647 this.preferredZones.add(id.getId()); 3648 } 3649 } 3650 } 3651 3652 private static final int STD = 0; 3653 private static final int DST = 1; 3654 private static final int GENERIC = 2; 3655 3656 // BEGIN Android-added: Lists of types used by getDisplayName(). 3657 private static final TimeZoneNames.NameType[] TYPES = new TimeZoneNames.NameType[] { 3658 TimeZoneNames.NameType.LONG_STANDARD, 3659 TimeZoneNames.NameType.SHORT_STANDARD, 3660 TimeZoneNames.NameType.LONG_DAYLIGHT, 3661 TimeZoneNames.NameType.SHORT_DAYLIGHT, 3662 TimeZoneNames.NameType.LONG_GENERIC, 3663 TimeZoneNames.NameType.SHORT_GENERIC, 3664 }; 3665 3666 private static final TimeZoneNames.NameType[] FULL_TYPES = new TimeZoneNames.NameType[] { 3667 TimeZoneNames.NameType.LONG_STANDARD, 3668 TimeZoneNames.NameType.LONG_DAYLIGHT, 3669 TimeZoneNames.NameType.LONG_GENERIC, 3670 }; 3671 3672 private static final TimeZoneNames.NameType[] SHORT_TYPES = new TimeZoneNames.NameType[] { 3673 TimeZoneNames.NameType.SHORT_STANDARD, 3674 TimeZoneNames.NameType.SHORT_DAYLIGHT, 3675 TimeZoneNames.NameType.SHORT_GENERIC, 3676 }; 3677 // END Android-added: Lists of types used by getDisplayName(). 3678 3679 private static final Map<String, SoftReference<Map<Locale, String[]>>> cache = 3680 new ConcurrentHashMap<>(); 3681 getDisplayName(String id, int type, Locale locale)3682 private String getDisplayName(String id, int type, Locale locale) { 3683 if (textStyle == TextStyle.NARROW) { 3684 return null; 3685 } 3686 String[] names; 3687 SoftReference<Map<Locale, String[]>> ref = cache.get(id); 3688 Map<Locale, String[]> perLocale = null; 3689 if (ref == null || (perLocale = ref.get()) == null || 3690 (names = perLocale.get(locale)) == null) { 3691 // BEGIN Android-changed: use ICU TimeZoneNames instead of TimeZoneNameUtility. 3692 /* 3693 names = TimeZoneNameUtility.retrieveDisplayNames(id, locale); 3694 if (names == null) { 3695 return null; 3696 } 3697 names = Arrays.copyOfRange(names, 0, 7); 3698 names[5] = 3699 TimeZoneNameUtility.retrieveGenericDisplayName(id, TimeZone.LONG, locale); 3700 if (names[5] == null) { 3701 names[5] = names[0]; // use the id 3702 } 3703 names[6] = 3704 TimeZoneNameUtility.retrieveGenericDisplayName(id, TimeZone.SHORT, locale); 3705 */ 3706 TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale); 3707 names = new String[TYPES.length + 1]; 3708 // Zeroth index used for id, other indexes based on NameType constant + 1. 3709 names[0] = id; 3710 String canonicalId = ZoneName.getSystemCanonicalID(id); 3711 libcore.icu.TimeZoneNames.getDisplayNames(timeZoneNames, canonicalId, TYPES, 3712 System.currentTimeMillis(), /* dest */ names, /* destoffset */ 1); 3713 if (names == null) { 3714 return null; 3715 } 3716 if (names[1] == null || names[2] == null || names[3] == null || names[4] == null) { 3717 // Use "GMT+XX:XX" analogous to java.util.TimeZone.getDisplayName() 3718 TimeZone tz = TimeZone.getTimeZone(id); 3719 String stdString = TimeZone.createGmtOffsetString( 3720 /* includeGmt */ true, /* includeMinuteSeparator */ true, 3721 tz.getRawOffset()); 3722 String dstString = TimeZone.createGmtOffsetString( 3723 /* includeGmt */ true, /* includeMinuteSeparator */ true, 3724 tz.getRawOffset() + tz.getDSTSavings()); 3725 names[1] = names[1] != null ? names[1] : stdString; 3726 names[2] = names[2] != null ? names[2] : stdString; 3727 names[3] = names[3] != null ? names[3] : dstString; 3728 names[4] = names[4] != null ? names[4] : dstString; 3729 } 3730 if (names[5] == null) { 3731 names[5] = names[0]; // use the id 3732 } 3733 // END Android-changed: use ICU TimeZoneNames instead of TimeZoneNameUtility. 3734 if (names[6] == null) { 3735 names[6] = names[0]; 3736 } 3737 if (perLocale == null) { 3738 perLocale = new ConcurrentHashMap<>(); 3739 } 3740 perLocale.put(locale, names); 3741 cache.put(id, new SoftReference<>(perLocale)); 3742 } 3743 switch (type) { 3744 case STD: 3745 return names[textStyle.zoneNameStyleIndex() + 1]; 3746 case DST: 3747 return names[textStyle.zoneNameStyleIndex() + 3]; 3748 } 3749 return names[textStyle.zoneNameStyleIndex() + 5]; 3750 } 3751 3752 @Override format(DateTimePrintContext context, StringBuilder buf)3753 public boolean format(DateTimePrintContext context, StringBuilder buf) { 3754 ZoneId zone = context.getValue(TemporalQueries.zoneId()); 3755 if (zone == null) { 3756 return false; 3757 } 3758 String zname = zone.getId(); 3759 if (!(zone instanceof ZoneOffset)) { 3760 TemporalAccessor dt = context.getTemporal(); 3761 String name = getDisplayName(zname, 3762 dt.isSupported(ChronoField.INSTANT_SECONDS) 3763 ? (zone.getRules().isDaylightSavings(Instant.from(dt)) ? DST : STD) 3764 : GENERIC, 3765 context.getLocale()); 3766 if (name != null) { 3767 zname = name; 3768 } 3769 } 3770 buf.append(zname); 3771 return true; 3772 } 3773 3774 // cache per instance for now 3775 private final Map<Locale, Entry<Integer, SoftReference<PrefixTree>>> 3776 cachedTree = new HashMap<>(); 3777 private final Map<Locale, Entry<Integer, SoftReference<PrefixTree>>> 3778 cachedTreeCI = new HashMap<>(); 3779 3780 @Override getTree(DateTimeParseContext context)3781 protected PrefixTree getTree(DateTimeParseContext context) { 3782 if (textStyle == TextStyle.NARROW) { 3783 return super.getTree(context); 3784 } 3785 Locale locale = context.getLocale(); 3786 boolean isCaseSensitive = context.isCaseSensitive(); 3787 Set<String> regionIds = ZoneRulesProvider.getAvailableZoneIds(); 3788 int regionIdsSize = regionIds.size(); 3789 3790 Map<Locale, Entry<Integer, SoftReference<PrefixTree>>> cached = 3791 isCaseSensitive ? cachedTree : cachedTreeCI; 3792 3793 Entry<Integer, SoftReference<PrefixTree>> entry = null; 3794 PrefixTree tree = null; 3795 String[][] zoneStrings = null; 3796 if ((entry = cached.get(locale)) == null || 3797 (entry.getKey() != regionIdsSize || 3798 (tree = entry.getValue().get()) == null)) { 3799 tree = PrefixTree.newTree(context); 3800 // BEGIN Android-changed: use ICU TimeZoneNames to get Zone names. 3801 /* 3802 zoneStrings = TimeZoneNameUtility.getZoneStrings(locale); 3803 for (String[] names : zoneStrings) { 3804 String zid = names[0]; 3805 if (!regionIds.contains(zid)) { 3806 continue; 3807 } 3808 tree.add(zid, zid); // don't convert zid -> metazone 3809 zid = ZoneName.toZid(zid, locale); 3810 int i = textStyle == TextStyle.FULL ? 1 : 2; 3811 for (; i < names.length; i += 2) { 3812 tree.add(names[i], zid); 3813 } 3814 } 3815 // if we have a set of preferred zones, need a copy and 3816 // add the preferred zones again to overwrite 3817 if (preferredZones != null) { 3818 for (String[] names : zoneStrings) { 3819 String zid = names[0]; 3820 if (!preferredZones.contains(zid) || !regionIds.contains(zid)) { 3821 continue; 3822 } 3823 int i = textStyle == TextStyle.FULL ? 1 : 2; 3824 for (; i < names.length; i += 2) { 3825 tree.add(names[i], zid); 3826 } 3827 } 3828 } 3829 */ 3830 TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale); 3831 long now = System.currentTimeMillis(); 3832 TimeZoneNames.NameType[] types = 3833 textStyle == TextStyle.FULL ? FULL_TYPES : SHORT_TYPES; 3834 String[] names = new String[types.length]; 3835 for (String zid : regionIds) { 3836 tree.add(zid, zid); // don't convert zid -> metazone 3837 zid = ZoneName.toZid(zid, locale); 3838 libcore.icu.TimeZoneNames.getDisplayNames(timeZoneNames, zid, types, now, 3839 names, 0); 3840 for (int i = 0; i < names.length; i++) { 3841 if (names[i] != null) { 3842 tree.add(names[i], zid); 3843 } 3844 } 3845 } 3846 // if we have a set of preferred zones, need a copy and 3847 // add the preferred zones again to overwrite 3848 if (preferredZones != null) { 3849 for (String zid : regionIds) { 3850 if (!preferredZones.contains(zid)) { 3851 continue; 3852 } 3853 String canonicalId = ZoneName.toZid(zid, locale); 3854 libcore.icu.TimeZoneNames.getDisplayNames(timeZoneNames, canonicalId, types, 3855 now, names, 0); 3856 for (int i = 0; i < names.length; i++) { 3857 if (names[i] != null) { 3858 tree.add(names[i], zid); 3859 } 3860 } 3861 } 3862 } 3863 // END Android-changed: use ICU TimeZoneNames to get Zone names. 3864 cached.put(locale, new SimpleImmutableEntry<>(regionIdsSize, new SoftReference<>(tree))); 3865 } 3866 return tree; 3867 } 3868 } 3869 3870 //----------------------------------------------------------------------- 3871 /** 3872 * Prints or parses a zone ID. 3873 */ 3874 static class ZoneIdPrinterParser implements DateTimePrinterParser { 3875 private final TemporalQuery<ZoneId> query; 3876 private final String description; 3877 ZoneIdPrinterParser(TemporalQuery<ZoneId> query, String description)3878 ZoneIdPrinterParser(TemporalQuery<ZoneId> query, String description) { 3879 this.query = query; 3880 this.description = description; 3881 } 3882 3883 @Override format(DateTimePrintContext context, StringBuilder buf)3884 public boolean format(DateTimePrintContext context, StringBuilder buf) { 3885 ZoneId zone = context.getValue(query); 3886 if (zone == null) { 3887 return false; 3888 } 3889 buf.append(zone.getId()); 3890 return true; 3891 } 3892 3893 /** 3894 * The cached tree to speed up parsing. 3895 */ 3896 private static volatile Entry<Integer, PrefixTree> cachedPrefixTree; 3897 private static volatile Entry<Integer, PrefixTree> cachedPrefixTreeCI; 3898 getTree(DateTimeParseContext context)3899 protected PrefixTree getTree(DateTimeParseContext context) { 3900 // prepare parse tree 3901 Set<String> regionIds = ZoneRulesProvider.getAvailableZoneIds(); 3902 final int regionIdsSize = regionIds.size(); 3903 Entry<Integer, PrefixTree> cached = context.isCaseSensitive() 3904 ? cachedPrefixTree : cachedPrefixTreeCI; 3905 if (cached == null || cached.getKey() != regionIdsSize) { 3906 synchronized (this) { 3907 cached = context.isCaseSensitive() ? cachedPrefixTree : cachedPrefixTreeCI; 3908 if (cached == null || cached.getKey() != regionIdsSize) { 3909 cached = new SimpleImmutableEntry<>(regionIdsSize, PrefixTree.newTree(regionIds, context)); 3910 if (context.isCaseSensitive()) { 3911 cachedPrefixTree = cached; 3912 } else { 3913 cachedPrefixTreeCI = cached; 3914 } 3915 } 3916 } 3917 } 3918 return cached.getValue(); 3919 } 3920 3921 /** 3922 * This implementation looks for the longest matching string. 3923 * For example, parsing Etc/GMT-2 will return Etc/GMC-2 rather than just 3924 * Etc/GMC although both are valid. 3925 */ 3926 @Override parse(DateTimeParseContext context, CharSequence text, int position)3927 public int parse(DateTimeParseContext context, CharSequence text, int position) { 3928 int length = text.length(); 3929 if (position > length) { 3930 throw new IndexOutOfBoundsException(); 3931 } 3932 if (position == length) { 3933 return ~position; 3934 } 3935 3936 // handle fixed time-zone IDs 3937 char nextChar = text.charAt(position); 3938 if (nextChar == '+' || nextChar == '-') { 3939 return parseOffsetBased(context, text, position, position, OffsetIdPrinterParser.INSTANCE_ID_Z); 3940 } else if (length >= position + 2) { 3941 char nextNextChar = text.charAt(position + 1); 3942 if (context.charEquals(nextChar, 'U') && context.charEquals(nextNextChar, 'T')) { 3943 if (length >= position + 3 && context.charEquals(text.charAt(position + 2), 'C')) { 3944 return parseOffsetBased(context, text, position, position + 3, OffsetIdPrinterParser.INSTANCE_ID_ZERO); 3945 } 3946 return parseOffsetBased(context, text, position, position + 2, OffsetIdPrinterParser.INSTANCE_ID_ZERO); 3947 } else if (context.charEquals(nextChar, 'G') && length >= position + 3 && 3948 context.charEquals(nextNextChar, 'M') && context.charEquals(text.charAt(position + 2), 'T')) { 3949 return parseOffsetBased(context, text, position, position + 3, OffsetIdPrinterParser.INSTANCE_ID_ZERO); 3950 } 3951 } 3952 3953 // parse 3954 PrefixTree tree = getTree(context); 3955 ParsePosition ppos = new ParsePosition(position); 3956 String parsedZoneId = tree.match(text, ppos); 3957 if (parsedZoneId == null) { 3958 if (context.charEquals(nextChar, 'Z')) { 3959 context.setParsed(ZoneOffset.UTC); 3960 return position + 1; 3961 } 3962 return ~position; 3963 } 3964 context.setParsed(ZoneId.of(parsedZoneId)); 3965 return ppos.getIndex(); 3966 } 3967 3968 /** 3969 * Parse an offset following a prefix and set the ZoneId if it is valid. 3970 * To matching the parsing of ZoneId.of the values are not normalized 3971 * to ZoneOffsets. 3972 * 3973 * @param context the parse context 3974 * @param text the input text 3975 * @param prefixPos start of the prefix 3976 * @param position start of text after the prefix 3977 * @param parser parser for the value after the prefix 3978 * @return the position after the parse 3979 */ parseOffsetBased(DateTimeParseContext context, CharSequence text, int prefixPos, int position, OffsetIdPrinterParser parser)3980 private int parseOffsetBased(DateTimeParseContext context, CharSequence text, int prefixPos, int position, OffsetIdPrinterParser parser) { 3981 String prefix = text.toString().substring(prefixPos, position).toUpperCase(); 3982 if (position >= text.length()) { 3983 context.setParsed(ZoneId.of(prefix)); 3984 return position; 3985 } 3986 3987 // Android-added: "GMT0" is considered a valid ZoneId. 3988 if (text.charAt(position) == '0' && prefix.equals("GMT")) { 3989 context.setParsed(ZoneId.of("GMT0")); 3990 return position + 1; 3991 } 3992 3993 // '0' or 'Z' after prefix is not part of a valid ZoneId; use bare prefix 3994 if (text.charAt(position) == '0' || 3995 context.charEquals(text.charAt(position), 'Z')) { 3996 context.setParsed(ZoneId.of(prefix)); 3997 return position; 3998 } 3999 4000 DateTimeParseContext newContext = context.copy(); 4001 int endPos = parser.parse(newContext, text, position); 4002 try { 4003 if (endPos < 0) { 4004 if (parser == OffsetIdPrinterParser.INSTANCE_ID_Z) { 4005 return ~prefixPos; 4006 } 4007 context.setParsed(ZoneId.of(prefix)); 4008 return position; 4009 } 4010 int offset = (int) newContext.getParsed(OFFSET_SECONDS).longValue(); 4011 ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(offset); 4012 context.setParsed(ZoneId.ofOffset(prefix, zoneOffset)); 4013 return endPos; 4014 } catch (DateTimeException dte) { 4015 return ~prefixPos; 4016 } 4017 } 4018 4019 @Override toString()4020 public String toString() { 4021 return description; 4022 } 4023 } 4024 4025 //----------------------------------------------------------------------- 4026 /** 4027 * A String based prefix tree for parsing time-zone names. 4028 */ 4029 static class PrefixTree { 4030 protected String key; 4031 protected String value; 4032 protected char c0; // performance optimization to avoid the 4033 // boundary check cost of key.charat(0) 4034 protected PrefixTree child; 4035 protected PrefixTree sibling; 4036 PrefixTree(String k, String v, PrefixTree child)4037 private PrefixTree(String k, String v, PrefixTree child) { 4038 this.key = k; 4039 this.value = v; 4040 this.child = child; 4041 if (k.length() == 0){ 4042 c0 = 0xffff; 4043 } else { 4044 c0 = key.charAt(0); 4045 } 4046 } 4047 4048 /** 4049 * Creates a new prefix parsing tree based on parse context. 4050 * 4051 * @param context the parse context 4052 * @return the tree, not null 4053 */ newTree(DateTimeParseContext context)4054 public static PrefixTree newTree(DateTimeParseContext context) { 4055 //if (!context.isStrict()) { 4056 // return new LENIENT("", null, null); 4057 //} 4058 if (context.isCaseSensitive()) { 4059 return new PrefixTree("", null, null); 4060 } 4061 return new CI("", null, null); 4062 } 4063 4064 /** 4065 * Creates a new prefix parsing tree. 4066 * 4067 * @param keys a set of strings to build the prefix parsing tree, not null 4068 * @param context the parse context 4069 * @return the tree, not null 4070 */ newTree(Set<String> keys, DateTimeParseContext context)4071 public static PrefixTree newTree(Set<String> keys, DateTimeParseContext context) { 4072 PrefixTree tree = newTree(context); 4073 for (String k : keys) { 4074 tree.add0(k, k); 4075 } 4076 return tree; 4077 } 4078 4079 /** 4080 * Clone a copy of this tree 4081 */ copyTree()4082 public PrefixTree copyTree() { 4083 PrefixTree copy = new PrefixTree(key, value, null); 4084 if (child != null) { 4085 copy.child = child.copyTree(); 4086 } 4087 if (sibling != null) { 4088 copy.sibling = sibling.copyTree(); 4089 } 4090 return copy; 4091 } 4092 4093 4094 /** 4095 * Adds a pair of {key, value} into the prefix tree. 4096 * 4097 * @param k the key, not null 4098 * @param v the value, not null 4099 * @return true if the pair is added successfully 4100 */ add(String k, String v)4101 public boolean add(String k, String v) { 4102 return add0(k, v); 4103 } 4104 add0(String k, String v)4105 private boolean add0(String k, String v) { 4106 k = toKey(k); 4107 int prefixLen = prefixLength(k); 4108 if (prefixLen == key.length()) { 4109 if (prefixLen < k.length()) { // down the tree 4110 String subKey = k.substring(prefixLen); 4111 PrefixTree c = child; 4112 while (c != null) { 4113 if (isEqual(c.c0, subKey.charAt(0))) { 4114 return c.add0(subKey, v); 4115 } 4116 c = c.sibling; 4117 } 4118 // add the node as the child of the current node 4119 c = newNode(subKey, v, null); 4120 c.sibling = child; 4121 child = c; 4122 return true; 4123 } 4124 // have an existing <key, value> already, overwrite it 4125 // if (value != null) { 4126 // return false; 4127 //} 4128 value = v; 4129 return true; 4130 } 4131 // split the existing node 4132 PrefixTree n1 = newNode(key.substring(prefixLen), value, child); 4133 key = k.substring(0, prefixLen); 4134 child = n1; 4135 if (prefixLen < k.length()) { 4136 PrefixTree n2 = newNode(k.substring(prefixLen), v, null); 4137 child.sibling = n2; 4138 value = null; 4139 } else { 4140 value = v; 4141 } 4142 return true; 4143 } 4144 4145 /** 4146 * Match text with the prefix tree. 4147 * 4148 * @param text the input text to parse, not null 4149 * @param off the offset position to start parsing at 4150 * @param end the end position to stop parsing 4151 * @return the resulting string, or null if no match found. 4152 */ match(CharSequence text, int off, int end)4153 public String match(CharSequence text, int off, int end) { 4154 if (!prefixOf(text, off, end)){ 4155 return null; 4156 } 4157 if (child != null && (off += key.length()) != end) { 4158 PrefixTree c = child; 4159 do { 4160 if (isEqual(c.c0, text.charAt(off))) { 4161 String found = c.match(text, off, end); 4162 if (found != null) { 4163 return found; 4164 } 4165 return value; 4166 } 4167 c = c.sibling; 4168 } while (c != null); 4169 } 4170 return value; 4171 } 4172 4173 /** 4174 * Match text with the prefix tree. 4175 * 4176 * @param text the input text to parse, not null 4177 * @param pos the position to start parsing at, from 0 to the text 4178 * length. Upon return, position will be updated to the new parse 4179 * position, or unchanged, if no match found. 4180 * @return the resulting string, or null if no match found. 4181 */ match(CharSequence text, ParsePosition pos)4182 public String match(CharSequence text, ParsePosition pos) { 4183 int off = pos.getIndex(); 4184 int end = text.length(); 4185 if (!prefixOf(text, off, end)){ 4186 return null; 4187 } 4188 off += key.length(); 4189 if (child != null && off != end) { 4190 PrefixTree c = child; 4191 do { 4192 if (isEqual(c.c0, text.charAt(off))) { 4193 pos.setIndex(off); 4194 String found = c.match(text, pos); 4195 if (found != null) { 4196 return found; 4197 } 4198 break; 4199 } 4200 c = c.sibling; 4201 } while (c != null); 4202 } 4203 pos.setIndex(off); 4204 return value; 4205 } 4206 toKey(String k)4207 protected String toKey(String k) { 4208 return k; 4209 } 4210 newNode(String k, String v, PrefixTree child)4211 protected PrefixTree newNode(String k, String v, PrefixTree child) { 4212 return new PrefixTree(k, v, child); 4213 } 4214 isEqual(char c1, char c2)4215 protected boolean isEqual(char c1, char c2) { 4216 return c1 == c2; 4217 } 4218 prefixOf(CharSequence text, int off, int end)4219 protected boolean prefixOf(CharSequence text, int off, int end) { 4220 if (text instanceof String) { 4221 return ((String)text).startsWith(key, off); 4222 } 4223 int len = key.length(); 4224 if (len > end - off) { 4225 return false; 4226 } 4227 int off0 = 0; 4228 while (len-- > 0) { 4229 if (!isEqual(key.charAt(off0++), text.charAt(off++))) { 4230 return false; 4231 } 4232 } 4233 return true; 4234 } 4235 prefixLength(String k)4236 private int prefixLength(String k) { 4237 int off = 0; 4238 while (off < k.length() && off < key.length()) { 4239 if (!isEqual(k.charAt(off), key.charAt(off))) { 4240 return off; 4241 } 4242 off++; 4243 } 4244 return off; 4245 } 4246 4247 /** 4248 * Case Insensitive prefix tree. 4249 */ 4250 private static class CI extends PrefixTree { 4251 CI(String k, String v, PrefixTree child)4252 private CI(String k, String v, PrefixTree child) { 4253 super(k, v, child); 4254 } 4255 4256 @Override newNode(String k, String v, PrefixTree child)4257 protected CI newNode(String k, String v, PrefixTree child) { 4258 return new CI(k, v, child); 4259 } 4260 4261 @Override isEqual(char c1, char c2)4262 protected boolean isEqual(char c1, char c2) { 4263 return DateTimeParseContext.charEqualsIgnoreCase(c1, c2); 4264 } 4265 4266 @Override prefixOf(CharSequence text, int off, int end)4267 protected boolean prefixOf(CharSequence text, int off, int end) { 4268 int len = key.length(); 4269 if (len > end - off) { 4270 return false; 4271 } 4272 int off0 = 0; 4273 while (len-- > 0) { 4274 if (!isEqual(key.charAt(off0++), text.charAt(off++))) { 4275 return false; 4276 } 4277 } 4278 return true; 4279 } 4280 } 4281 4282 /** 4283 * Lenient prefix tree. Case insensitive and ignores characters 4284 * like space, underscore and slash. 4285 */ 4286 private static class LENIENT extends CI { 4287 LENIENT(String k, String v, PrefixTree child)4288 private LENIENT(String k, String v, PrefixTree child) { 4289 super(k, v, child); 4290 } 4291 4292 @Override newNode(String k, String v, PrefixTree child)4293 protected CI newNode(String k, String v, PrefixTree child) { 4294 return new LENIENT(k, v, child); 4295 } 4296 isLenientChar(char c)4297 private boolean isLenientChar(char c) { 4298 return c == ' ' || c == '_' || c == '/'; 4299 } 4300 toKey(String k)4301 protected String toKey(String k) { 4302 for (int i = 0; i < k.length(); i++) { 4303 if (isLenientChar(k.charAt(i))) { 4304 StringBuilder sb = new StringBuilder(k.length()); 4305 sb.append(k, 0, i); 4306 i++; 4307 while (i < k.length()) { 4308 if (!isLenientChar(k.charAt(i))) { 4309 sb.append(k.charAt(i)); 4310 } 4311 i++; 4312 } 4313 return sb.toString(); 4314 } 4315 } 4316 return k; 4317 } 4318 4319 @Override match(CharSequence text, ParsePosition pos)4320 public String match(CharSequence text, ParsePosition pos) { 4321 int off = pos.getIndex(); 4322 int end = text.length(); 4323 int len = key.length(); 4324 int koff = 0; 4325 while (koff < len && off < end) { 4326 if (isLenientChar(text.charAt(off))) { 4327 off++; 4328 continue; 4329 } 4330 if (!isEqual(key.charAt(koff++), text.charAt(off++))) { 4331 return null; 4332 } 4333 } 4334 if (koff != len) { 4335 return null; 4336 } 4337 if (child != null && off != end) { 4338 int off0 = off; 4339 while (off0 < end && isLenientChar(text.charAt(off0))) { 4340 off0++; 4341 } 4342 if (off0 < end) { 4343 PrefixTree c = child; 4344 do { 4345 if (isEqual(c.c0, text.charAt(off0))) { 4346 pos.setIndex(off0); 4347 String found = c.match(text, pos); 4348 if (found != null) { 4349 return found; 4350 } 4351 break; 4352 } 4353 c = c.sibling; 4354 } while (c != null); 4355 } 4356 } 4357 pos.setIndex(off); 4358 return value; 4359 } 4360 } 4361 } 4362 4363 //----------------------------------------------------------------------- 4364 /** 4365 * Prints or parses a chronology. 4366 */ 4367 static final class ChronoPrinterParser implements DateTimePrinterParser { 4368 /** The text style to output, null means the ID. */ 4369 private final TextStyle textStyle; 4370 ChronoPrinterParser(TextStyle textStyle)4371 ChronoPrinterParser(TextStyle textStyle) { 4372 // validated by caller 4373 this.textStyle = textStyle; 4374 } 4375 4376 @Override format(DateTimePrintContext context, StringBuilder buf)4377 public boolean format(DateTimePrintContext context, StringBuilder buf) { 4378 Chronology chrono = context.getValue(TemporalQueries.chronology()); 4379 if (chrono == null) { 4380 return false; 4381 } 4382 if (textStyle == null) { 4383 buf.append(chrono.getId()); 4384 } else { 4385 buf.append(getChronologyName(chrono, context.getLocale())); 4386 } 4387 return true; 4388 } 4389 4390 @Override parse(DateTimeParseContext context, CharSequence text, int position)4391 public int parse(DateTimeParseContext context, CharSequence text, int position) { 4392 // simple looping parser to find the chronology 4393 if (position < 0 || position > text.length()) { 4394 throw new IndexOutOfBoundsException(); 4395 } 4396 Set<Chronology> chronos = Chronology.getAvailableChronologies(); 4397 Chronology bestMatch = null; 4398 int matchLen = -1; 4399 for (Chronology chrono : chronos) { 4400 String name; 4401 if (textStyle == null) { 4402 name = chrono.getId(); 4403 } else { 4404 name = getChronologyName(chrono, context.getLocale()); 4405 } 4406 int nameLen = name.length(); 4407 if (nameLen > matchLen && context.subSequenceEquals(text, position, name, 0, nameLen)) { 4408 bestMatch = chrono; 4409 matchLen = nameLen; 4410 } 4411 } 4412 if (bestMatch == null) { 4413 return ~position; 4414 } 4415 context.setParsed(bestMatch); 4416 return position + matchLen; 4417 } 4418 4419 /** 4420 * Returns the chronology name of the given chrono in the given locale 4421 * if available, or the chronology Id otherwise. The regular ResourceBundle 4422 * search path is used for looking up the chronology name. 4423 * 4424 * @param chrono the chronology, not null 4425 * @param locale the locale, not null 4426 * @return the chronology name of chrono in locale, or the id if no name is available 4427 * @throws NullPointerException if chrono or locale is null 4428 */ getChronologyName(Chronology chrono, Locale locale)4429 private String getChronologyName(Chronology chrono, Locale locale) { 4430 // Android-changed: Use ICU LocaleDisplayNames. http://b/28832222 4431 // String key = "calendarname." + chrono.getCalendarType(); 4432 // String name = DateTimeTextProvider.getLocalizedResource(key, locale); 4433 LocaleDisplayNames displayNames = LocaleDisplayNames.getInstance(ULocale.forLocale(locale)); 4434 String name = displayNames.keyValueDisplayName("calendar", chrono.getCalendarType()); 4435 return name != null ? name : chrono.getId(); 4436 } 4437 } 4438 4439 //----------------------------------------------------------------------- 4440 /** 4441 * Prints or parses a localized pattern. 4442 */ 4443 static final class LocalizedPrinterParser implements DateTimePrinterParser { 4444 /** Cache of formatters. */ 4445 private static final ConcurrentMap<String, DateTimeFormatter> FORMATTER_CACHE = new ConcurrentHashMap<>(16, 0.75f, 2); 4446 4447 private final FormatStyle dateStyle; 4448 private final FormatStyle timeStyle; 4449 4450 /** 4451 * Constructor. 4452 * 4453 * @param dateStyle the date style to use, may be null 4454 * @param timeStyle the time style to use, may be null 4455 */ LocalizedPrinterParser(FormatStyle dateStyle, FormatStyle timeStyle)4456 LocalizedPrinterParser(FormatStyle dateStyle, FormatStyle timeStyle) { 4457 // validated by caller 4458 this.dateStyle = dateStyle; 4459 this.timeStyle = timeStyle; 4460 } 4461 4462 @Override format(DateTimePrintContext context, StringBuilder buf)4463 public boolean format(DateTimePrintContext context, StringBuilder buf) { 4464 Chronology chrono = Chronology.from(context.getTemporal()); 4465 return formatter(context.getLocale(), chrono).toPrinterParser(false).format(context, buf); 4466 } 4467 4468 @Override parse(DateTimeParseContext context, CharSequence text, int position)4469 public int parse(DateTimeParseContext context, CharSequence text, int position) { 4470 Chronology chrono = context.getEffectiveChronology(); 4471 return formatter(context.getLocale(), chrono).toPrinterParser(false).parse(context, text, position); 4472 } 4473 4474 /** 4475 * Gets the formatter to use. 4476 * <p> 4477 * The formatter will be the most appropriate to use for the date and time style in the locale. 4478 * For example, some locales will use the month name while others will use the number. 4479 * 4480 * @param locale the locale to use, not null 4481 * @param chrono the chronology to use, not null 4482 * @return the formatter, not null 4483 * @throws IllegalArgumentException if the formatter cannot be found 4484 */ formatter(Locale locale, Chronology chrono)4485 private DateTimeFormatter formatter(Locale locale, Chronology chrono) { 4486 String key = chrono.getId() + '|' + locale.toString() + '|' + dateStyle + timeStyle; 4487 DateTimeFormatter formatter = FORMATTER_CACHE.get(key); 4488 if (formatter == null) { 4489 String pattern = getLocalizedDateTimePattern(dateStyle, timeStyle, chrono, locale); 4490 formatter = new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale); 4491 DateTimeFormatter old = FORMATTER_CACHE.putIfAbsent(key, formatter); 4492 if (old != null) { 4493 formatter = old; 4494 } 4495 } 4496 return formatter; 4497 } 4498 4499 @Override toString()4500 public String toString() { 4501 return "Localized(" + (dateStyle != null ? dateStyle : "") + "," + 4502 (timeStyle != null ? timeStyle : "") + ")"; 4503 } 4504 } 4505 4506 //----------------------------------------------------------------------- 4507 /** 4508 * Prints or parses a localized pattern from a localized field. 4509 * The specific formatter and parameters is not selected until the 4510 * the field is to be printed or parsed. 4511 * The locale is needed to select the proper WeekFields from which 4512 * the field for day-of-week, week-of-month, or week-of-year is selected. 4513 */ 4514 static final class WeekBasedFieldPrinterParser implements DateTimePrinterParser { 4515 private char chr; 4516 private int count; 4517 4518 /** 4519 * Constructor. 4520 * 4521 * @param chr the pattern format letter that added this PrinterParser. 4522 * @param count the repeat count of the format letter 4523 */ WeekBasedFieldPrinterParser(char chr, int count)4524 WeekBasedFieldPrinterParser(char chr, int count) { 4525 this.chr = chr; 4526 this.count = count; 4527 } 4528 4529 @Override format(DateTimePrintContext context, StringBuilder buf)4530 public boolean format(DateTimePrintContext context, StringBuilder buf) { 4531 return printerParser(context.getLocale()).format(context, buf); 4532 } 4533 4534 @Override parse(DateTimeParseContext context, CharSequence text, int position)4535 public int parse(DateTimeParseContext context, CharSequence text, int position) { 4536 return printerParser(context.getLocale()).parse(context, text, position); 4537 } 4538 4539 /** 4540 * Gets the printerParser to use based on the field and the locale. 4541 * 4542 * @param locale the locale to use, not null 4543 * @return the formatter, not null 4544 * @throws IllegalArgumentException if the formatter cannot be found 4545 */ printerParser(Locale locale)4546 private DateTimePrinterParser printerParser(Locale locale) { 4547 WeekFields weekDef = WeekFields.of(locale); 4548 TemporalField field = null; 4549 switch (chr) { 4550 case 'Y': 4551 field = weekDef.weekBasedYear(); 4552 if (count == 2) { 4553 return new ReducedPrinterParser(field, 2, 2, 0, ReducedPrinterParser.BASE_DATE, 0); 4554 } else { 4555 return new NumberPrinterParser(field, count, 19, 4556 (count < 4) ? SignStyle.NORMAL : SignStyle.EXCEEDS_PAD, -1); 4557 } 4558 case 'e': 4559 case 'c': 4560 field = weekDef.dayOfWeek(); 4561 break; 4562 case 'w': 4563 field = weekDef.weekOfWeekBasedYear(); 4564 break; 4565 case 'W': 4566 field = weekDef.weekOfMonth(); 4567 break; 4568 default: 4569 throw new IllegalStateException("unreachable"); 4570 } 4571 return new NumberPrinterParser(field, (count == 2 ? 2 : 1), 2, SignStyle.NOT_NEGATIVE); 4572 } 4573 4574 @Override toString()4575 public String toString() { 4576 StringBuilder sb = new StringBuilder(30); 4577 sb.append("Localized("); 4578 if (chr == 'Y') { 4579 if (count == 1) { 4580 sb.append("WeekBasedYear"); 4581 } else if (count == 2) { 4582 sb.append("ReducedValue(WeekBasedYear,2,2,2000-01-01)"); 4583 } else { 4584 sb.append("WeekBasedYear,").append(count).append(",") 4585 .append(19).append(",") 4586 .append((count < 4) ? SignStyle.NORMAL : SignStyle.EXCEEDS_PAD); 4587 } 4588 } else { 4589 switch (chr) { 4590 case 'c': 4591 case 'e': 4592 sb.append("DayOfWeek"); 4593 break; 4594 case 'w': 4595 sb.append("WeekOfWeekBasedYear"); 4596 break; 4597 case 'W': 4598 sb.append("WeekOfMonth"); 4599 break; 4600 default: 4601 break; 4602 } 4603 sb.append(","); 4604 sb.append(count); 4605 } 4606 sb.append(")"); 4607 return sb.toString(); 4608 } 4609 } 4610 4611 //------------------------------------------------------------------------- 4612 /** 4613 * Length comparator. 4614 */ 4615 static final Comparator<String> LENGTH_SORT = new Comparator<String>() { 4616 @Override 4617 public int compare(String str1, String str2) { 4618 return str1.length() == str2.length() ? str1.compareTo(str2) : str1.length() - str2.length(); 4619 } 4620 }; 4621 } 4622