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