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