diff -r c588664d547e -r 55b829ca2334 jdk/src/share/classes/java/time/format/DateTimeFormatterBuilder.java --- a/jdk/src/share/classes/java/time/format/DateTimeFormatterBuilder.java Tue Feb 12 16:02:14 2013 +0400 +++ b/jdk/src/share/classes/java/time/format/DateTimeFormatterBuilder.java Tue Feb 12 09:25:43 2013 -0800 @@ -81,11 +81,12 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.chrono.Chronology; +import java.time.chrono.IsoChronology; +import java.time.chrono.JapaneseChronology; import java.time.format.DateTimeTextProvider.LocaleStore; -import java.time.temporal.Chrono; import java.time.temporal.ChronoField; -import java.time.temporal.ISOChrono; -import java.time.temporal.ISOFields; +import java.time.temporal.IsoFields; import java.time.temporal.Queries; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; @@ -99,6 +100,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -289,10 +291,10 @@ * Appends the value of a date-time field to the formatter using a normal * output style. *

- * The value of the field will be output during a print. + * The value of the field will be output during a format. * If the value cannot be obtained then an exception will be thrown. *

- * The value will be printed as per the normal print of an integer value. + * The value will be printed as per the normal format of an integer value. * Only negative numbers will be signed. No padding will be added. *

* The parser for a variable width value such as this normally behaves greedily, @@ -313,12 +315,12 @@ * Appends the value of a date-time field to the formatter using a fixed * width, zero-padded approach. *

- * The value of the field will be output during a print. + * The value of the field will be output during a format. * If the value cannot be obtained then an exception will be thrown. *

* The value will be zero-padded on the left. If the size of the value * means that it cannot be printed within the width then an exception is thrown. - * If the value of the field is negative then an exception is thrown during printing. + * If the value of the field is negative then an exception is thrown during formatting. *

* This method supports a special technique of parsing known as 'adjacent value parsing'. * This technique solves the problem where a variable length value is followed by one or more @@ -368,9 +370,9 @@ /** * Appends the value of a date-time field to the formatter providing full - * control over printing. + * control over formatting. *

- * The value of the field will be output during a print. + * The value of the field will be output during a format. * If the value cannot be obtained then an exception will be thrown. *

* This method provides full control of the numeric formatting, including @@ -386,7 +388,7 @@ *

* If this method is invoked with equal minimum and maximum widths and a sign style of * {@code NOT_NEGATIVE} then it delegates to {@code appendValue(TemporalField,int)}. - * In this scenario, the printing and parsing behavior described there occur. + * In this scenario, the formatting and parsing behavior described there occur. * * @param field the field to append, not null * @param minWidth the minimum field width of the printed field, from 1 to 19 @@ -425,11 +427,11 @@ /** * Appends the reduced value of a date-time field to the formatter. *

- * This is typically used for printing and parsing a two digit year. + * This is typically used for formatting and parsing a two digit year. * The {@code width} is the printed and parsed width. * The {@code baseValue} is used during parsing to determine the valid range. *

- * For printing, the width is used to determine the number of characters to print. + * For formatting, the width is used to determine the number of characters to format. * The rightmost characters are output to match the width, left padding with zero. *

* For parsing, exactly the number of characters specified by the width are parsed. @@ -525,12 +527,12 @@ * Appends the text of a date-time field to the formatter using the full * text style. *

- * The text of the field will be output during a print. + * The text of the field will be output during a format. * The value must be within the valid range of the field. * If the value cannot be obtained then an exception will be thrown. * If the field has no textual representation, then the numeric value will be used. *

- * The value will be printed as per the normal print of an integer value. + * The value will be printed as per the normal format of an integer value. * Only negative numbers will be signed. No padding will be added. * * @param field the field to append, not null @@ -543,12 +545,12 @@ /** * Appends the text of a date-time field to the formatter. *

- * The text of the field will be output during a print. + * The text of the field will be output during a format. * The value must be within the valid range of the field. * If the value cannot be obtained then an exception will be thrown. * If the field has no textual representation, then the numeric value will be used. *

- * The value will be printed as per the normal print of an integer value. + * The value will be printed as per the normal format of an integer value. * Only negative numbers will be signed. No padding will be added. * * @param field the field to append, not null @@ -568,10 +570,10 @@ *

* The standard text outputting methods use the localized text in the JDK. * This method allows that text to be specified directly. - * The supplied map is not validated by the builder to ensure that printing or + * The supplied map is not validated by the builder to ensure that formatting or * parsing is possible, thus an invalid map may throw an error during later use. *

- * Supplying the map of text provides considerable flexibility in printing and parsing. + * Supplying the map of text provides considerable flexibility in formatting and parsing. * For example, a legacy application might require or supply the months of the * year as "JNY", "FBY", "MCH" etc. These do not match the standard set of text * for localized month names. Using this method, a map can be created which @@ -588,7 +590,7 @@ * Other uses might be to output the value with a suffix, such as "1st", "2nd", "3rd", * or as Roman numerals "I", "II", "III", "IV". *

- * During printing, the value is obtained and checked that it is in the valid range. + * During formatting, the value is obtained and checked that it is in the valid range. * If text is not available for the value then it is output as a number. * During parsing, the parser will match against the map of text and numeric values. * @@ -624,7 +626,7 @@ * They are converted to a date-time with a zone-offset of UTC and printed * using the standard ISO-8601 format. *

- * An alternative to this method is to print/parse the instant as a single + * An alternative to this method is to format/parse the instant as a single * epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}. * * @return this, for chaining, not null @@ -637,22 +639,22 @@ /** * Appends the zone offset, such as '+01:00', to the formatter. *

- * This appends an instruction to print/parse the offset ID to the builder. + * This appends an instruction to format/parse the offset ID to the builder. * This is equivalent to calling {@code appendOffset("HH:MM:ss", "Z")}. * * @return this, for chaining, not null */ public DateTimeFormatterBuilder appendOffsetId() { - appendInternal(OffsetIdPrinterParser.INSTANCE_ID); + appendInternal(OffsetIdPrinterParser.INSTANCE_ID_Z); return this; } /** * Appends the zone offset, such as '+01:00', to the formatter. *

- * This appends an instruction to print/parse the offset ID to the builder. + * This appends an instruction to format/parse the offset ID to the builder. *

- * During printing, the offset is obtained using a mechanism equivalent + * During formatting, the offset is obtained using a mechanism equivalent * to querying the temporal with {@link Queries#offset()}. * It will be printed using the format defined below. * If the offset cannot be obtained then an exception is thrown unless the @@ -665,15 +667,18 @@ * The format of the offset is controlled by a pattern which must be one * of the following: *

- * The "no offset" text controls what text is printed when the offset is zero. + * The "no offset" text controls what text is printed when the total amount of + * the offset fields to be output is zero. * Example values would be 'Z', '+00:00', 'UTC' or 'GMT'. * Three formats are accepted for parsing UTC - the "no offset" text, and the * plus and minus versions of zero defined by the pattern. @@ -683,7 +688,7 @@ * @return this, for chaining, not null */ public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText) { - appendInternal(new OffsetIdPrinterParser(noOffsetText, pattern)); + appendInternal(new OffsetIdPrinterParser(pattern, noOffsetText)); return this; } @@ -691,20 +696,48 @@ /** * Appends the time-zone ID, such as 'Europe/Paris' or '+02:00', to the formatter. *

- * This appends an instruction to print/parse the zone ID to the builder. + * This appends an instruction to format/parse the zone ID to the builder. * The zone ID is obtained in a strict manner suitable for {@code ZonedDateTime}. * By contrast, {@code OffsetDateTime} does not have a zone ID suitable * for use with this method, see {@link #appendZoneOrOffsetId()}. *

- * During printing, the zone is obtained using a mechanism equivalent + * During formatting, the zone is obtained using a mechanism equivalent * to querying the temporal with {@link Queries#zoneId()}. * It will be printed using the result of {@link ZoneId#getId()}. * If the zone cannot be obtained then an exception is thrown unless the * section of the formatter is optional. *

- * During parsing, the zone is parsed and must match a known zone or offset. - * If the zone cannot be parsed then an exception is thrown unless the - * section of the formatter is optional. + * During parsing, the text must match a known zone or offset. + * There are two types of zone ID, offset-based, such as '+01:30' and + * region-based, such as 'Europe/London'. These are parsed differently. + * If the parse starts with '+', '-', 'UT', 'UTC' or 'GMT', then the parser + * expects an offset-based zone and will not match region-based zones. + * The offset ID, such as '+02:30', may be at the start of the parse, + * or prefixed by 'UT', 'UTC' or 'GMT'. The offset ID parsing is + * equivalent to using {@link #appendOffset(String, String)} using the + * arguments 'HH:MM:ss' and the no offset string '0'. + * If the parse starts with 'UT', 'UTC' or 'GMT', and the parser cannot + * match a following offset ID, then {@link ZoneOffset#UTC} is selected. + * In all other cases, the list of known region-based zones is used to + * find the longest available match. If no match is found, and the parse + * starts with 'Z', then {@code ZoneOffset.UTC} is selected. + * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. + *

+ * For example, the following will parse: + *

+     *   "Europe/London"           -> ZoneId.of("Europe/London")
+     *   "Z"                       -> ZoneOffset.UTC
+     *   "UT"                      -> ZoneOffset.UTC
+     *   "UTC"                     -> ZoneOffset.UTC
+     *   "GMT"                     -> ZoneOffset.UTC
+     *   "UT0"                     -> ZoneOffset.UTC
+     *   "UTC0"                    -> ZoneOffset.UTC
+     *   "GMT0"                    -> ZoneOffset.UTC
+     *   "+01:30"                  -> ZoneOffset.of("+01:30")
+     *   "UT+01:30"                -> ZoneOffset.of("+01:30")
+     *   "UTC+01:30"               -> ZoneOffset.of("+01:30")
+     *   "GMT+01:30"               -> ZoneOffset.of("+01:30")
+     * 
* * @return this, for chaining, not null * @see #appendZoneRegionId() @@ -718,21 +751,52 @@ * Appends the time-zone region ID, such as 'Europe/Paris', to the formatter, * rejecting the zone ID if it is a {@code ZoneOffset}. *

- * This appends an instruction to print/parse the zone ID to the builder + * This appends an instruction to format/parse the zone ID to the builder * only if it is a region-based ID. *

- * During printing, the zone is obtained using a mechanism equivalent + * During formatting, the zone is obtained using a mechanism equivalent * to querying the temporal with {@link Queries#zoneId()}. * If the zone is a {@code ZoneOffset} or it cannot be obtained then * an exception is thrown unless the section of the formatter is optional. * If the zone is not an offset, then the zone will be printed using * the zone ID from {@link ZoneId#getId()}. *

- * During parsing, the zone is parsed and must match a known zone or offset. - * If the zone cannot be parsed then an exception is thrown unless the - * section of the formatter is optional. - * Note that parsing accepts offsets, whereas printing will never produce - * one, thus parsing is equivalent to {@code appendZoneId}. + * During parsing, the text must match a known zone or offset. + * There are two types of zone ID, offset-based, such as '+01:30' and + * region-based, such as 'Europe/London'. These are parsed differently. + * If the parse starts with '+', '-', 'UT', 'UTC' or 'GMT', then the parser + * expects an offset-based zone and will not match region-based zones. + * The offset ID, such as '+02:30', may be at the start of the parse, + * or prefixed by 'UT', 'UTC' or 'GMT'. The offset ID parsing is + * equivalent to using {@link #appendOffset(String, String)} using the + * arguments 'HH:MM:ss' and the no offset string '0'. + * If the parse starts with 'UT', 'UTC' or 'GMT', and the parser cannot + * match a following offset ID, then {@link ZoneOffset#UTC} is selected. + * In all other cases, the list of known region-based zones is used to + * find the longest available match. If no match is found, and the parse + * starts with 'Z', then {@code ZoneOffset.UTC} is selected. + * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. + *

+ * For example, the following will parse: + *

+     *   "Europe/London"           -> ZoneId.of("Europe/London")
+     *   "Z"                       -> ZoneOffset.UTC
+     *   "UT"                      -> ZoneOffset.UTC
+     *   "UTC"                     -> ZoneOffset.UTC
+     *   "GMT"                     -> ZoneOffset.UTC
+     *   "UT0"                     -> ZoneOffset.UTC
+     *   "UTC0"                    -> ZoneOffset.UTC
+     *   "GMT0"                    -> ZoneOffset.UTC
+     *   "+01:30"                  -> ZoneOffset.of("+01:30")
+     *   "UT+01:30"                -> ZoneOffset.of("+01:30")
+     *   "UTC+01:30"               -> ZoneOffset.of("+01:30")
+     *   "GMT+01:30"               -> ZoneOffset.of("+01:30")
+     * 
+ *

+ * Note that this method is is identical to {@code appendZoneId()} except + * in the mechanism used to obtain the zone. + * Note also that parsing accepts offsets, whereas formatting will never + * produce one. * * @return this, for chaining, not null * @see #appendZoneId() @@ -746,24 +810,52 @@ * Appends the time-zone ID, such as 'Europe/Paris' or '+02:00', to * the formatter, using the best available zone ID. *

- * This appends an instruction to print/parse the best available + * This appends an instruction to format/parse the best available * zone or offset ID to the builder. * The zone ID is obtained in a lenient manner that first attempts to * find a true zone ID, such as that on {@code ZonedDateTime}, and * then attempts to find an offset, such as that on {@code OffsetDateTime}. *

- * During printing, the zone is obtained using a mechanism equivalent + * During formatting, the zone is obtained using a mechanism equivalent * to querying the temporal with {@link Queries#zone()}. * It will be printed using the result of {@link ZoneId#getId()}. * If the zone cannot be obtained then an exception is thrown unless the * section of the formatter is optional. *

- * During parsing, the zone is parsed and must match a known zone or offset. - * If the zone cannot be parsed then an exception is thrown unless the - * section of the formatter is optional. + * During parsing, the text must match a known zone or offset. + * There are two types of zone ID, offset-based, such as '+01:30' and + * region-based, such as 'Europe/London'. These are parsed differently. + * If the parse starts with '+', '-', 'UT', 'UTC' or 'GMT', then the parser + * expects an offset-based zone and will not match region-based zones. + * The offset ID, such as '+02:30', may be at the start of the parse, + * or prefixed by 'UT', 'UTC' or 'GMT'. The offset ID parsing is + * equivalent to using {@link #appendOffset(String, String)} using the + * arguments 'HH:MM:ss' and the no offset string '0'. + * If the parse starts with 'UT', 'UTC' or 'GMT', and the parser cannot + * match a following offset ID, then {@link ZoneOffset#UTC} is selected. + * In all other cases, the list of known region-based zones is used to + * find the longest available match. If no match is found, and the parse + * starts with 'Z', then {@code ZoneOffset.UTC} is selected. + * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. *

- * This method is is identical to {@code appendZoneId()} except in the - * mechanism used to obtain the zone. + * For example, the following will parse: + *

+     *   "Europe/London"           -> ZoneId.of("Europe/London")
+     *   "Z"                       -> ZoneOffset.UTC
+     *   "UT"                      -> ZoneOffset.UTC
+     *   "UTC"                     -> ZoneOffset.UTC
+     *   "GMT"                     -> ZoneOffset.UTC
+     *   "UT0"                     -> ZoneOffset.UTC
+     *   "UTC0"                    -> ZoneOffset.UTC
+     *   "GMT0"                    -> ZoneOffset.UTC
+     *   "+01:30"                  -> ZoneOffset.of("+01:30")
+     *   "UT+01:30"                -> ZoneOffset.of("+01:30")
+     *   "UTC+01:30"               -> ZoneOffset.of("+01:30")
+     *   "GMT+01:30"               -> ZoneOffset.of("+01:30")
+     * 
+ *

+ * Note that this method is is identical to {@code appendZoneId()} except + * in the mechanism used to obtain the zone. * * @return this, for chaining, not null * @see #appendZoneId() @@ -776,9 +868,46 @@ /** * Appends the time-zone name, such as 'British Summer Time', to the formatter. *

- * This appends an instruction to print the textual name of the zone to the builder. + * This appends an instruction to format/parse the textual name of the zone to + * the builder. + *

+ * During formatting, the zone is obtained using a mechanism equivalent + * to querying the temporal with {@link Queries#zoneId()}. + * If the zone is a {@code ZoneOffset} it will be printed using the + * result of {@link ZoneOffset#getId()}. + * If the zone is not an offset, the textual name will be looked up + * for the locale set in the {@link DateTimeFormatter}. + * If the temporal object being printed represents an instant, then the text + * will be the summer or winter time text as appropriate. + * If the lookup for text does not find any suitable reuslt, then the + * {@link ZoneId#getId() ID} will be printed instead. + * If the zone cannot be obtained then an exception is thrown unless the + * section of the formatter is optional. *

- * During printing, the zone is obtained using a mechanism equivalent + * During parsing, either the textual zone name, the zone ID or the offset + * is accepted. Many textual zone names are not unique, such as CST can be + * for both "Central Standard Time" and "China Standard Time". In this + * situation, the zone id will be determined by the region information from + * formatter's {@link DateTimeFormatter#getLocale() locale} and the standard + * zone id for that area, for example, America/New_York for the America Eastern + * zone. The {@link #appendZoneText(TextStyle, Set)} may be used + * to specify a set of preferred {@link ZoneId} in this situation. + * + * @param textStyle the text style to use, not null + * @return this, for chaining, not null + */ + public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle) { + appendInternal(new ZoneTextPrinterParser(textStyle, null)); + return this; + } + + /** + * Appends the time-zone name, such as 'British Summer Time', to the formatter. + *

+ * This appends an instruction to format/parse the textual name of the zone to + * the builder. + *

+ * During formatting, the zone is obtained using a mechanism equivalent * to querying the temporal with {@link Queries#zoneId()}. * If the zone is a {@code ZoneOffset} it will be printed using the * result of {@link ZoneOffset#getId()}. @@ -791,31 +920,51 @@ * If the zone cannot be obtained then an exception is thrown unless the * section of the formatter is optional. *

- * Parsing is not currently supported. + * During parsing, either the textual zone name, the zone ID or the offset + * is accepted. Many textual zone names are not unique, such as CST can be + * for both "Central Standard Time" and "China Standard Time". In this + * situation, the zone id will be determined by the region information from + * formatter's {@link DateTimeFormatter#getLocale() locale} and the standard + * zone id for that area, for example, America/New_York for the America Eastern + * zone. This method also allows a set of preferred {@link ZoneId} to be + * specified for parsing. The matched preferred zone id will be used if the + * textural zone name being parsed is not unique. + * + * If the zone cannot be parsed then an exception is thrown unless the + * section of the formatter is optional. * * @param textStyle the text style to use, not null + * @param preferredZones the set of preferred zone ids, not null * @return this, for chaining, not null */ - public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle) { - // TODO: parsing of zone text? -// * During parsing, either the textual zone name, the zone ID or the offset -// * is accepted. -// * If the zone cannot be parsed then an exception is thrown unless the -// * section of the formatter is optional. - appendInternal(new ZoneTextPrinterParser(textStyle)); + public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle, + Set preferredZones) { + Objects.requireNonNull(preferredZones, "preferredZones"); + appendInternal(new ZoneTextPrinterParser(textStyle, preferredZones)); return this; } //----------------------------------------------------------------------- /** - * Appends the chronology ID to the formatter. + * Appends the chronology ID, such as 'ISO' or 'ThaiBuddhist', to the formatter. + *

+ * This appends an instruction to format/parse the chronology ID to the builder. *

- * The chronology ID will be output during a print. - * If the chronology cannot be obtained then an exception will be thrown. + * During formatting, the chronology is obtained using a mechanism equivalent + * to querying the temporal with {@link Queries#chronology()}. + * It will be printed using the result of {@link Chronology#getId()}. + * If the chronology cannot be obtained then an exception is thrown unless the + * section of the formatter is optional. + *

+ * During parsing, the chronology is parsed and must match one of the chronologies + * in {@link Chronology#getAvailableChronologies()}. + * If the chronology cannot be parsed then an exception is thrown unless the + * section of the formatter is optional. + * The parser uses the {@linkplain #parseCaseInsensitive() case sensitive} setting. * * @return this, for chaining, not null */ - public DateTimeFormatterBuilder appendChronoId() { + public DateTimeFormatterBuilder appendChronologyId() { appendInternal(new ChronoPrinterParser(null)); return this; } @@ -823,14 +972,14 @@ /** * Appends the chronology name to the formatter. *

- * The calendar system name will be output during a print. + * The calendar system name will be output during a format. * If the chronology cannot be obtained then an exception will be thrown. * The calendar system name is obtained from the formatting symbols. * * @param textStyle the text style to use, not null * @return this, for chaining, not null */ - public DateTimeFormatterBuilder appendChronoText(TextStyle textStyle) { + public DateTimeFormatterBuilder appendChronologyText(TextStyle textStyle) { Objects.requireNonNull(textStyle, "textStyle"); appendInternal(new ChronoPrinterParser(textStyle)); return this; @@ -840,39 +989,36 @@ /** * Appends a localized date-time pattern to the formatter. *

- * The pattern is resolved lazily using the locale being used during the print/parse - * (stored in {@link DateTimeFormatter}. + * This appends a localized section to the builder, suitable for outputting + * a date, time or date-time combination. The format of the localized + * section is lazily looked up based on four items: + *

+ * During formatting, the chronology is obtained from the temporal object + * being formatted, which may have been overridden by + * {@link DateTimeFormatter#withChronology(Chronology)}. *

- * The pattern can vary by chronology, although typically it doesn't. - * This method uses the standard ISO chronology patterns. + * During parsing, if a chronology has already been parsed, then it is used. + * Otherwise the default from {@code DateTimeFormatter.withChronology(Chronology)} + * is used, with {@code IsoChronology} as the fallback. + *

+ * Note that this method provides similar functionality to methods on + * {@code DateFormat} such as {@link DateFormat#getDateTimeInstance(int, int)}. * * @param dateStyle the date style to use, null means no date required * @param timeStyle the time style to use, null means no time required * @return this, for chaining, not null + * @throws IllegalArgumentException if both the date and time styles are null */ public DateTimeFormatterBuilder appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle) { - return appendLocalized(dateStyle, timeStyle, ISOChrono.INSTANCE); - } - - /** - * Appends a localized date-time pattern to the formatter. - *

- * The pattern is resolved lazily using the locale being used during the print/parse, - * stored in {@link DateTimeFormatter}. - *

- * The pattern can vary by chronology, although typically it doesn't. - * This method allows the chronology to be specified. - * - * @param dateStyle the date style to use, null means no date required - * @param timeStyle the time style to use, null means no time required - * @param chrono the chronology to use, not null - * @return this, for chaining, not null - */ - public DateTimeFormatterBuilder appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle, Chrono chrono) { - Objects.requireNonNull(chrono, "chrono"); - if (dateStyle != null || timeStyle != null) { - appendInternal(new LocalizedPrinterParser(dateStyle, timeStyle, chrono)); + if (dateStyle == null && timeStyle == null) { + throw new IllegalArgumentException("Either the date or time style must be non-null"); } + appendInternal(new LocalizedPrinterParser(dateStyle, timeStyle)); return this; } @@ -880,7 +1026,7 @@ /** * Appends a character literal to the formatter. *

- * This character will be output during a print. + * This character will be output during a format. * * @param literal the literal to append, not null * @return this, for chaining, not null @@ -893,7 +1039,7 @@ /** * Appends a string literal to the formatter. *

- * This string will be output during a print. + * This string will be output during a format. *

* If the literal is empty, nothing is added to the formatter. * @@ -929,13 +1075,13 @@ } /** - * Appends a formatter to the builder which will optionally print/parse. + * Appends a formatter to the builder which will optionally format/parse. *

* This method has the same effect as appending each of the constituent * parts directly to this builder surrounded by an {@link #optionalStart()} and * {@link #optionalEnd()}. *

- * The formatter will print if data is available for all the fields contained within it. + * The formatter will format if data is available for all the fields contained within it. * The formatter will parse if the string matches, otherwise no error is returned. * * @param formatter the formatter to add, not null @@ -958,7 +1104,7 @@ *

      *  Symbol  Meaning                     Presentation      Examples
      *  ------  -------                     ------------      -------
-     *   G       era                         number/text       1; 01; AD; Anno Domini
+     *   G       era                         text              A; AD; Anno Domini
      *   y       year                        year              2004; 04
      *   D       day-of-year                 number            189
      *   M       month-of-year               number/text       7; 07; Jul; July; J
@@ -985,10 +1131,11 @@
      *   n       nano-of-second              number            987654321
      *   N       nano-of-day                 number            1234000000
      *
-     *   I       time-zone ID                zoneId            America/Los_Angeles
-     *   z       time-zone name              text              Pacific Standard Time; PST
+     *   V       time-zone ID                zone-id           America/Los_Angeles; Z; -08:30
+     *   z       time-zone name              zone-name         Pacific Standard Time; PST
+     *   X       zone-offset 'Z' for zero    offset-X          Z; -08; -0830; -08:30; -083015; -08:30:15;
+     *   x       zone-offset                 offset-x          +0000; -08; -0830; -08:30; -083015; -08:30:15;
      *   Z       zone-offset                 offset-Z          +0000; -0800; -08:00;
-     *   X       zone-offset 'Z' for zero    offset-X          Z; -08; -0830; -08:30; -083015; -08:30:15;
      *
      *   p       pad next                    pad modifier      1
      *
@@ -1023,26 +1170,66 @@
      * 

* Year: The count of letters determines the minimum field width below which padding is used. * If the count of letters is two, then a {@link #appendValueReduced reduced} two digit form is used. - * For printing, this outputs the rightmost two digits. For parsing, this will parse using the + * For formatting, this outputs the rightmost two digits. For parsing, this will parse using the * base value of 2000, resulting in a year within the range 2000 to 2099 inclusive. * If the count of letters is less than four (but not two), then the sign is only output for negative * years as per {@link SignStyle#NORMAL}. * Otherwise, the sign is output if the pad width is exceeded, as per {@link SignStyle#EXCEEDS_PAD} *

- * ZoneId: 'I' outputs the zone ID, such as 'Europe/Paris'. + * ZoneId: This outputs the time-zone ID, such as 'Europe/Paris'. + * If the count of letters is two, then the time-zone ID is output. + * Any other count of letters throws {@code IllegalArgumentException}. + *

+     *  Pattern     Equivalent builder methods
+     *   VV          appendZoneId()
+     * 
*

- * Offset X: This formats the offset using 'Z' when the offset is zero. - * One letter outputs just the hour', such as '+01' + * Zone names: This outputs the display name of the time-zone ID. + * If the count of letters is one, two or three, then the short name is output. + * If the count of letters is four, then the full name is output. + * Five or more letters throws {@code IllegalArgumentException}. + *

+     *  Pattern     Equivalent builder methods
+     *   z           appendZoneText(TextStyle.SHORT)
+     *   zz          appendZoneText(TextStyle.SHORT)
+     *   zzz         appendZoneText(TextStyle.SHORT)
+     *   zzzz        appendZoneText(TextStyle.FULL)
+     * 
+ *

+ * Offset X and x: This formats the offset based on the number of pattern letters. + * One letter outputs just the hour', such as '+01', unless the minute is non-zero + * in which case the minute is also output, such as '+0130'. * Two letters outputs the hour and minute, without a colon, such as '+0130'. * Three letters outputs the hour and minute, with a colon, such as '+01:30'. * Four letters outputs the hour and minute and optional second, without a colon, such as '+013015'. * Five letters outputs the hour and minute and optional second, with a colon, such as '+01:30:15'. + * Six or more letters throws {@code IllegalArgumentException}. + * Pattern letter 'X' (upper case) will output 'Z' when the offset to be output would be zero, + * whereas pattern letter 'x' (lower case) will output '+00', '+0000', or '+00:00'. + *

+     *  Pattern     Equivalent builder methods
+     *   X           appendOffset("+HHmm","Z")
+     *   XX          appendOffset("+HHMM","Z")
+     *   XXX         appendOffset("+HH:MM","Z")
+     *   XXXX        appendOffset("+HHMMss","Z")
+     *   XXXXX       appendOffset("+HH:MM:ss","Z")
+     *   x           appendOffset("+HHmm","+00")
+     *   xx          appendOffset("+HHMM","+0000")
+     *   xxx         appendOffset("+HH:MM","+00:00")
+     *   xxxx        appendOffset("+HHMMss","+0000")
+     *   xxxxx       appendOffset("+HH:MM:ss","+00:00")
+     * 
*

- * Offset Z: This formats the offset using '+0000' or '+00:00' when the offset is zero. - * One or two letters outputs the hour and minute, without a colon, such as '+0130'. - * Three letters outputs the hour and minute, with a colon, such as '+01:30'. - *

- * Zone names: Time zone names ('z') cannot be parsed. + * Offset Z: This formats the offset based on the number of pattern letters. + * One, two or three letters outputs the hour and minute, without a colon, such as '+0130'. + * Four or more letters throws {@code IllegalArgumentException}. + * The output will be '+0000' when the offset is zero. + *

+     *  Pattern     Equivalent builder methods
+     *   Z           appendOffset("+HHMM","+0000")
+     *   ZZ          appendOffset("+HHMM","+0000")
+     *   ZZZ         appendOffset("+HHMM","+0000")
+     * 
*

* Optional section: The optional section markers work exactly like calling {@link #optionalStart()} * and {@link #optionalEnd()}. @@ -1058,14 +1245,15 @@ * Despite this, it is recommended to use single quotes around all characters that you want to * output directly to ensure that future changes do not break your application. *

- * The pattern string is similar, but not identical, to {@link java.text.SimpleDateFormat SimpleDateFormat}. + * Note that the pattern string is similar, but not identical, to + * {@link java.text.SimpleDateFormat SimpleDateFormat}. + * The pattern string is also similar, but not identical, to that defined by the + * Unicode Common Locale Data Repository (CLDR/LDML). * Pattern letters 'E' and 'u' are merged, which changes the meaning of "E" and "EE" to be numeric. - * Pattern letters 'Z' and 'X' are extended. + * Pattern letters 'X' is aligned with Unicode CLDR/LDML, which affects pattern 'X'. * Pattern letter 'y' and 'Y' parse years of two digits and more than 4 digits differently. * Pattern letters 'n', 'A', 'N', 'I' and 'p' are added. * Number types will reject large numbers. - * The pattern string is also similar, but not identical, to that defined by the - * Unicode Common Locale Data Repository (CLDR). * * @param pattern the pattern to add, not null * @return this, for chaining, not null @@ -1107,27 +1295,34 @@ if (field != null) { parseField(cur, count, field); } else if (cur == 'z') { - if (count < 4) { - appendZoneText(TextStyle.SHORT); + if (count > 4) { + throw new IllegalArgumentException("Too many pattern letters: " + cur); + } else if (count == 4) { + appendZoneText(TextStyle.FULL); } else { - appendZoneText(TextStyle.FULL); + appendZoneText(TextStyle.SHORT); } - } else if (cur == 'I') { + } else if (cur == 'V') { + if (count != 2) { + throw new IllegalArgumentException("Pattern letter count must be 2: " + cur); + } appendZoneId(); } else if (cur == 'Z') { if (count > 3) { throw new IllegalArgumentException("Too many pattern letters: " + cur); } - if (count < 3) { - appendOffset("+HHMM", "+0000"); - } else { - appendOffset("+HH:MM", "+00:00"); - } + appendOffset("+HHMM", "+0000"); } else if (cur == 'X') { if (count > 5) { throw new IllegalArgumentException("Too many pattern letters: " + cur); } - appendOffset(OffsetIdPrinterParser.PATTERNS[count - 1], "Z"); + appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], "Z"); + } else if (cur == 'x') { + if (count > 5) { + throw new IllegalArgumentException("Too many pattern letters: " + cur); + } + String zero = (count == 1 ? "+00" : (count % 2 == 0 ? "+0000" : "+00:00")); + appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], zero); } else if (cur == 'w' || cur == 'e') { // Fields defined by Locale if (count > 1) { @@ -1196,7 +1391,6 @@ appendValue(field, count, 19, SignStyle.EXCEEDS_PAD); } break; - case 'G': case 'M': case 'Q': case 'E': @@ -1220,6 +1414,7 @@ throw new IllegalArgumentException("Too many pattern letters: " + cur); } break; + case 'G': case 'a': switch (count) { case 1: @@ -1253,44 +1448,44 @@ /** Map of letters to fields. */ private static final Map FIELD_MAP = new HashMap<>(); static { - FIELD_MAP.put('G', ChronoField.ERA); // Java, CLDR (different to both for 1/2 chars) - FIELD_MAP.put('y', ChronoField.YEAR); // CLDR - // FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // Java, CLDR // TODO redefine from above - // FIELD_MAP.put('u', ChronoField.YEAR); // CLDR // TODO - // FIELD_MAP.put('Y', ISODateTimeField.WEEK_BASED_YEAR); // Java7, CLDR (needs localized week number) // TODO - FIELD_MAP.put('Q', ISOFields.QUARTER_OF_YEAR); // CLDR (removed quarter from 310) - FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // Java, CLDR - // FIELD_MAP.put('w', WeekFields.weekOfYear()); // Java, CLDR (needs localized week number) - // FIELD_MAP.put('W', WeekFields.weekOfMonth()); // Java, CLDR (needs localized week number) - FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // Java, CLDR - FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // Java, CLDR - FIELD_MAP.put('F', ChronoField.ALIGNED_WEEK_OF_MONTH); // Java, CLDR - FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // Java, CLDR (different to both for 1/2 chars) - // FIELD_MAP.put('e', WeekFields.dayOfWeek()); // CLDR (needs localized week number) - FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // Java, CLDR - FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // Java, CLDR - FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // Java, CLDR - FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // Java, CLDR - FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // Java, CLDR - FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // Java, CLDR - FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // Java, CLDR - FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // CLDR (Java uses milli-of-second number) - FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // CLDR - FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 - FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 - // reserved - z,Z,X,I,p - // Java - X - compatible, but extended to 4 and 5 letters - // Java - u - clashes with CLDR, go with CLDR (year-proleptic) here - // CLDR - U - cycle year name, not supported by 310 yet - // CLDR - l - deprecated - // CLDR - j - not relevant - // CLDR - g - modified-julian-day - // CLDR - z - time-zone names // TODO properly - // CLDR - Z - different approach here // TODO bring 310 in line with CLDR - // CLDR - v,V - extended time-zone names - // CLDR - q/c/L - standalone quarter/day-of-week/month - // 310 - I - time-zone id - // 310 - p - prefix for padding + FIELD_MAP.put('G', ChronoField.ERA); // Java, LDML (different to both for 1/2 chars) + FIELD_MAP.put('y', ChronoField.YEAR); // LDML + // FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // Java, LDML // TODO redefine from above + // FIELD_MAP.put('u', ChronoField.YEAR); // LDML // TODO + // FIELD_MAP.put('Y', IsoFields.WEEK_BASED_YEAR); // Java7, LDML (needs localized week number) // TODO + FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310) + FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // Java, LDML + // FIELD_MAP.put('w', WeekFields.weekOfYear()); // Java, LDML (needs localized week number) + // FIELD_MAP.put('W', WeekFields.weekOfMonth()); // Java, LDML (needs localized week number) + FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // Java, LDML + FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // Java, LDML + FIELD_MAP.put('F', ChronoField.ALIGNED_WEEK_OF_MONTH); // Java, LDML + FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // Java, LDML (different to both for 1/2 chars) + // FIELD_MAP.put('e', WeekFields.dayOfWeek()); // LDML (needs localized week number) + FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // Java, LDML + FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // Java, LDML + FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // Java, LDML + FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // Java, LDML + FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // Java, LDML + FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // Java, LDML + FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // Java, LDML + FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (Java uses milli-of-second number) + FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML + FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML) + FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML) + // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 + // 310 - Z - matches SimpleDateFormat and LDML + // 310 - V - time-zone id, matches proposed LDML + // 310 - p - prefix for padding + // 310 - X - matches proposed LDML, almost matches JavaSDF for 1, exact match 2&3, extended 4&5 + // 310 - x - matches proposed LDML + // Java - u - clashes with LDML, go with LDML (year-proleptic) here + // LDML - U - cycle year name, not supported by 310 yet + // LDML - l - deprecated + // LDML - j - not relevant + // LDML - g - modified-julian-day + // LDML - v,V - extended time-zone names + // LDML - q/c/L - standalone quarter/day-of-week/month } //----------------------------------------------------------------------- @@ -1299,8 +1494,15 @@ *

* This padding will pad to a fixed width using spaces. *

- * An exception will be thrown during printing if the pad width - * is exceeded. + * During formatting, the decorated element will be output and then padded + * to the specified width. An exception will be thrown during formatting if + * the pad width is exceeded. + *

+ * During parsing, the padding and decorated element are parsed. + * If parsing is lenient, then the pad width is treated as a maximum. + * If parsing is case insensitive, then the pad character is matched ignoring case. + * The padding is parsed greedily. Thus, if the decorated element starts with + * the pad character, it will not be parsed. * * @param padWidth the pad width, 1 or greater * @return this, for chaining, not null @@ -1316,8 +1518,15 @@ * This padding is intended for padding other than zero-padding. * Zero-padding should be achieved using the appendValue methods. *

- * An exception will be thrown during printing if the pad width - * is exceeded. + * During formatting, the decorated element will be output and then padded + * to the specified width. An exception will be thrown during formatting if + * the pad width is exceeded. + *

+ * During parsing, the padding and decorated element are parsed. + * If parsing is lenient, then the pad width is treated as a maximum. + * If parsing is case insensitive, then the pad character is matched ignoring case. + * The padding is parsed greedily. Thus, if the decorated element starts with + * the pad character, it will not be parsed. * * @param padWidth the pad width, 1 or greater * @param padChar the pad character @@ -1338,19 +1547,19 @@ /** * Mark the start of an optional section. *

- * The output of printing can include optional sections, which may be nested. + * The output of formatting can include optional sections, which may be nested. * An optional section is started by calling this method and ended by calling * {@link #optionalEnd()} or by ending the build process. *

* All elements in the optional section are treated as optional. - * During printing, the section is only output if data is available in the + * During formatting, the section is only output if data is available in the * {@code TemporalAccessor} for all the elements in the section. * During parsing, the whole section may be missing from the parsed string. *

* For example, consider a builder setup as * {@code builder.appendValue(HOUR_OF_DAY,2).optionalStart().appendValue(MINUTE_OF_HOUR,2)}. * The optional section ends automatically at the end of the builder. - * During printing, the minute will only be output if its value can be obtained from the date-time. + * During formatting, the minute will only be output if its value can be obtained from the date-time. * During parsing, the input will be successfully parsed whether the minute is present or not. * * @return this, for chaining, not null @@ -1364,7 +1573,7 @@ /** * Ends an optional section. *

- * The output of printing can include optional sections, which may be nested. + * The output of formatting can include optional sections, which may be nested. * An optional section is started by calling {@link #optionalStart()} and ended * using this method (or at the end of the builder). *

@@ -1374,13 +1583,13 @@ * on the formatter other than ending the (empty) optional section. *

* All elements in the optional section are treated as optional. - * During printing, the section is only output if data is available in the + * During formatting, the section is only output if data is available in the * {@code TemporalAccessor} for all the elements in the section. * During parsing, the whole section may be missing from the parsed string. *

* For example, consider a builder setup as * {@code builder.appendValue(HOUR_OF_DAY,2).optionalStart().appendValue(MINUTE_OF_HOUR,2).optionalEnd()}. - * During printing, the minute will only be output if its value can be obtained from the date-time. + * During formatting, the minute will only be output if its value can be obtained from the date-time. * During parsing, the input will be successfully parsed whether the minute is present or not. * * @return this, for chaining, not null @@ -1466,18 +1675,17 @@ //----------------------------------------------------------------------- /** - * Strategy for printing/parsing date-time information. + * Strategy for formatting/parsing date-time information. *

- * The printer may print any part, or the whole, of the input date-time object. - * Typically, a complete print is constructed from a number of smaller + * The printer may format any part, or the whole, of the input date-time object. + * Typically, a complete format is constructed from a number of smaller * units, each outputting a single field. *

* The parser may parse any piece of text from the input, storing the result * in the context. Typically, each individual parser will just parse one * field, such as the day-of-month, storing the value in the context. - * Once the parse is complete, the caller will then convert the context - * to a {@link DateTimeBuilder} to merge the parsed values to create the - * desired object, such as a {@code LocalDate}. + * Once the parse is complete, the caller will then resolve the parsed values + * to create the desired object, such as a {@code LocalDate}. *

* The parse position will be updated during the parse. Parsing will start at * the specified index and the return value specifies the new parse position @@ -1489,7 +1697,7 @@ * All implementations that can be instantiated must be final, immutable and thread-safe. *

* The context is not a thread-safe object and a new instance will be created - * for each print that occurs. The context must not be stored in an instance + * for each format that occurs. The context must not be stored in an instance * variable or shared with any other threads. */ interface DateTimePrinterParser { @@ -1497,17 +1705,17 @@ /** * Prints the date-time object to the buffer. *

- * The context holds information to use during the print. + * The context holds information to use during the format. * It also contains the date-time information to be printed. *

* The buffer must not be mutated beyond the content controlled by the implementation. * - * @param context the context to print using, not null + * @param context the context to format using, not null * @param buf the buffer to append to, not null * @return false if unable to query the value from the date-time, true otherwise * @throws DateTimeException if the date-time cannot be printed successfully */ - boolean print(DateTimePrintContext context, StringBuilder buf); + boolean format(DateTimePrintContext context, StringBuilder buf); /** * Parses text into date-time information. @@ -1557,14 +1765,14 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { int length = buf.length(); if (optional) { context.startOptional(); } try { for (DateTimePrinterParser pp : printerParsers) { - if (pp.print(context, buf) == false) { + if (pp.format(context, buf) == false) { buf.setLength(length); // reset buffer return true; } @@ -1640,14 +1848,14 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { int preLen = buf.length(); - if (printerParser.print(context, buf) == false) { + if (printerParser.format(context, buf) == false) { return false; } int len = buf.length() - preLen; if (len > padWidth) { - throw new DateTimePrintException( + throw new DateTimeException( "Cannot print as output of " + len + " characters exceeds pad width of " + padWidth); } for (int i = 0; i < padWidth - len; i++) { @@ -1658,37 +1866,32 @@ @Override public int parse(DateTimeParseContext context, CharSequence text, int position) { + // cache context before changed by decorated parser + final boolean strict = context.isStrict(); + // parse if (position > text.length()) { throw new IndexOutOfBoundsException(); } + if (position == text.length()) { + return ~position; // no more characters in the string + } int endPos = position + padWidth; if (endPos > text.length()) { - return ~position; // not enough characters in the string to meet the parse width + if (strict) { + return ~position; // not enough characters in the string to meet the parse width + } + endPos = text.length(); } int pos = position; - while (pos < endPos && text.charAt(pos) == padChar) { + while (pos < endPos && context.charEquals(text.charAt(pos), padChar)) { pos++; } text = text.subSequence(0, endPos); - int firstError = 0; - while (pos >= position) { - int resultPos = printerParser.parse(context, text, pos); - if (resultPos < 0) { - // parse of decorated field had an error - if (firstError == 0) { - firstError = resultPos; - } - // loop around in case the decorated parser can handle the padChar at the start - pos--; - continue; - } - if (resultPos != endPos) { - return ~position; // parse of decorated field didn't parse to the end - } - return resultPos; + int resultPos = printerParser.parse(context, text, pos); + if (resultPos != endPos && strict) { + return ~(position + pos); // parse of decorated field didn't parse to the end } - // loop runs at least once, so firstError must be set by the time we get here - return firstError; // return error from first parse of decorated field + return resultPos; } @Override @@ -1708,7 +1911,7 @@ LENIENT; @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { return true; // nothing to do here } @@ -1749,7 +1952,7 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { buf.append(literal); return true; } @@ -1792,7 +1995,7 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { buf.append(literal); return true; } @@ -1847,7 +2050,7 @@ /** * Constructor. * - * @param field the field to print, not null + * @param field the field to format, not null * @param minWidth the minimum field width, from 1 to 19 * @param maxWidth the maximum field width, from minWidth to 19 * @param signStyle the positive/negative sign style, not null @@ -1864,7 +2067,7 @@ /** * Constructor. * - * @param field the field to print, not null + * @param field the field to format, not null * @param minWidth the minimum field width, from 1 to 19 * @param maxWidth the maximum field width, from minWidth to 19 * @param signStyle the positive/negative sign style, not null @@ -1900,8 +2103,14 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { - Long valueLong = context.getValue(field); + public boolean format(DateTimePrintContext context, StringBuilder buf) { + Chronology chrono = context.getTemporal().query(Queries.chronology()); + Long valueLong; + if (chrono == JapaneseChronology.INSTANCE && field == ChronoField.YEAR) { + valueLong = context.getValue(ChronoField.YEAR_OF_ERA); + } else { + valueLong = context.getValue(field); + } if (valueLong == null) { return false; } @@ -1909,7 +2118,7 @@ DateTimeFormatSymbols symbols = context.getSymbols(); String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value))); if (str.length() > maxWidth) { - throw new DateTimePrintException("Field " + field.getName() + + throw new DateTimeException("Field " + field.getName() + " cannot be printed as the value " + value + " exceeds the maximum print width of " + maxWidth); } @@ -1934,7 +2143,7 @@ buf.append(symbols.getNegativeSign()); break; case NOT_NEGATIVE: - throw new DateTimePrintException("Field " + field.getName() + + throw new DateTimeException("Field " + field.getName() + " cannot be printed as the value " + value + " cannot be negative according to the SignStyle"); } @@ -2057,11 +2266,9 @@ totalBig = totalBig.divide(BigInteger.TEN); pos--; } - setValue(context, totalBig.longValue()); - } else { - setValue(context, total); + return setValue(context, totalBig.longValue(), position, pos); } - return pos; + return setValue(context, total, position, pos); } /** @@ -2069,9 +2276,19 @@ * * @param context the context to store into, not null * @param value the value + * @param errorPos the position of the field being parsed + * @param successPos the position after the field being parsed + * @return the new position */ - void setValue(DateTimeParseContext context, long value) { - context.setParsedField(field, value); + int setValue(DateTimeParseContext context, long value, int errorPos, int successPos) { + TemporalField f = field; + if (field == ChronoField.YEAR) { + Chronology chrono = context.getEffectiveChronology(); + if (chrono == JapaneseChronology.INSTANCE) { + f = ChronoField.YEAR_OF_ERA; + } + } + return context.setParsedField(f, value, errorPos, successPos); } @Override @@ -2097,7 +2314,7 @@ /** * Constructor. * - * @param field the field to print, validated not null + * @param field the field to format, validated not null * @param width the field width, from 1 to 18 * @param baseValue the base value */ @@ -2122,7 +2339,7 @@ } @Override - void setValue(DateTimeParseContext context, long value) { + int setValue(DateTimeParseContext context, long value, int errorPos, int successPos) { int lastPart = baseValue % range; if (baseValue > 0) { value = baseValue - lastPart + value; @@ -2132,7 +2349,7 @@ if (value < baseValue) { value += range; } - context.setParsedField(field, value); + return context.setParsedField(field, value, errorPos, successPos); } @Override @@ -2191,7 +2408,7 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { Long value = context.getValue(field); if (value == null) { return false; @@ -2257,8 +2474,7 @@ } BigDecimal fraction = new BigDecimal(total).movePointLeft(pos - position); long value = convertFromFraction(fraction); - context.setParsedField(field, value); - return pos; + return context.setParsedField(field, value, position, pos); } /** @@ -2348,23 +2564,20 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { Long value = context.getValue(field); if (value == null) { return false; } - String text = null; - if (field == ChronoField.ERA) { - Chrono chrono = context.getTemporal().query(Queries.chrono()); - if (chrono == null) { - chrono = ISOChrono.INSTANCE; - } - text = provider.getEraText(chrono, value, textStyle, context.getLocale()); + String text; + Chronology chrono = context.getTemporal().query(Queries.chronology()); + if (chrono == null || chrono == IsoChronology.INSTANCE) { + text = provider.getText(field, value, textStyle, context.getLocale()); } else { - text = provider.getText(field, value, textStyle, context.getLocale()); + text = provider.getText(chrono, field, value, textStyle, context.getLocale()); } if (text == null) { - return numberPrinterParser().print(context, buf); + return numberPrinterParser().format(context, buf); } buf.append(text); return true; @@ -2377,14 +2590,19 @@ throw new IndexOutOfBoundsException(); } TextStyle style = (context.isStrict() ? textStyle : null); - Iterator> it = provider.getTextIterator(field, style, context.getLocale()); + Chronology chrono = context.getEffectiveChronology(); + Iterator> it; + if (chrono == null || chrono == IsoChronology.INSTANCE) { + it = provider.getTextIterator(field, style, context.getLocale()); + } else { + it = provider.getTextIterator(chrono, field, style, context.getLocale()); + } if (it != null) { while (it.hasNext()) { Entry entry = it.next(); String itText = entry.getKey(); if (context.subSequenceEquals(itText, 0, parseText, position, itText.length())) { - context.setParsedField(field, entry.getValue()); - return position + itText.length(); + return context.setParsedField(field, entry.getValue(), position, position + itText.length()); } } if (context.isStrict()) { @@ -2426,15 +2644,15 @@ private static final long SECONDS_0000_TO_1970 = ((146097L * 5L) - (30L * 365L + 7L)) * 86400L; private static final CompositePrinterParser PARSER = new DateTimeFormatterBuilder() .parseCaseInsensitive() - .append(DateTimeFormatters.isoLocalDate()).appendLiteral('T') - .append(DateTimeFormatters.isoLocalTime()).appendLiteral('Z') + .append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral('T') + .append(DateTimeFormatter.ISO_LOCAL_TIME).appendLiteral('Z') .toFormatter().toPrinterParser(false); InstantPrinterParser() { } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { // use INSTANT_SECONDS, thus this code is not bound by Instant.MAX Long inSecs = context.getValue(INSTANT_SECONDS); Long inNanos = context.getValue(NANO_OF_SECOND); @@ -2502,9 +2720,9 @@ } catch (RuntimeException ex) { return ~position; } - context.setParsedField(INSTANT_SECONDS, instantSecs); - context.setParsedField(NANO_OF_SECOND, nano); - return text.length(); + int successPos = text.length(); + successPos = context.setParsedField(INSTANT_SECONDS, instantSecs, position, successPos); + return context.setParsedField(NANO_OF_SECOND, nano, position, successPos); } @Override @@ -2519,9 +2737,10 @@ */ static final class OffsetIdPrinterParser implements DateTimePrinterParser { static final String[] PATTERNS = new String[] { - "+HH", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", + "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", }; // order used in pattern builder - static final OffsetIdPrinterParser INSTANCE_ID = new OffsetIdPrinterParser("Z", "+HH:MM:ss"); + static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z"); + static final OffsetIdPrinterParser INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0"); private final String noOffsetText; private final int type; @@ -2529,14 +2748,14 @@ /** * Constructor. * + * @param pattern the pattern * @param noOffsetText the text to use for UTC, not null - * @param pattern the pattern */ - OffsetIdPrinterParser(String noOffsetText, String pattern) { - Objects.requireNonNull(noOffsetText, "noOffsetText"); + OffsetIdPrinterParser(String pattern, String noOffsetText) { Objects.requireNonNull(pattern, "pattern"); + Objects.requireNonNull(noOffsetText, "noOffsetText"); + this.type = checkPattern(pattern); this.noOffsetText = noOffsetText; - this.type = checkPattern(pattern); } private int checkPattern(String pattern) { @@ -2549,7 +2768,7 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { Long offsetSecs = context.getValue(OFFSET_SECONDS); if (offsetSecs == null) { return false; @@ -2561,16 +2780,24 @@ int absHours = Math.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped int absMinutes = Math.abs((totalSecs / 60) % 60); int absSeconds = Math.abs(totalSecs % 60); + int bufPos = buf.length(); + int output = absHours; buf.append(totalSecs < 0 ? "-" : "+") .append((char) (absHours / 10 + '0')).append((char) (absHours % 10 + '0')); - if (type >= 1) { + if (type >= 3 || (type >= 1 && absMinutes > 0)) { buf.append((type % 2) == 0 ? ":" : "") .append((char) (absMinutes / 10 + '0')).append((char) (absMinutes % 10 + '0')); - if (type >= 5 || (type >= 3 && absSeconds > 0)) { + output += absMinutes; + if (type >= 7 || (type >= 5 && absSeconds > 0)) { buf.append((type % 2) == 0 ? ":" : "") .append((char) (absSeconds / 10 + '0')).append((char) (absSeconds % 10 + '0')); + output += absSeconds; } } + if (output == 0) { + buf.setLength(bufPos); + buf.append(noOffsetText); + } } return true; } @@ -2581,16 +2808,14 @@ int noOffsetLen = noOffsetText.length(); if (noOffsetLen == 0) { if (position == length) { - context.setParsedField(OFFSET_SECONDS, 0); - return position; + return context.setParsedField(OFFSET_SECONDS, 0, position, position); } } else { if (position == length) { return ~position; } if (context.subSequenceEquals(text, position, noOffsetText, 0, noOffsetLen)) { - context.setParsedField(OFFSET_SECONDS, 0); - return position + noOffsetLen; + return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen); } } @@ -2601,22 +2826,19 @@ int negative = (sign == '-' ? -1 : 1); int[] array = new int[4]; array[0] = position + 1; - if (parseNumber(array, 1, text, true) || - parseNumber(array, 2, text, type > 0) || - parseNumber(array, 3, text, false)) { - return ~position; + if ((parseNumber(array, 1, text, true) || + parseNumber(array, 2, text, type >=3) || + parseNumber(array, 3, text, false)) == false) { + // success + long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]); + return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, array[0]); } - long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]); - context.setParsedField(OFFSET_SECONDS, offsetSecs); - return array[0]; - } else { - // handle special case of empty no offset text - if (noOffsetLen == 0) { - context.setParsedField(OFFSET_SECONDS, 0); - return position + noOffsetLen; - } - return ~position; } + // handle special case of empty no offset text + if (noOffsetLen == 0) { + return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen); + } + return ~position; } /** @@ -2659,7 +2881,7 @@ @Override public String toString() { String converted = noOffsetText.replace("'", "''"); - return "Offset('" + converted + "'," + PATTERNS[type] + ")"; + return "Offset(" + PATTERNS[type] + ",'" + converted + "')"; } } @@ -2667,29 +2889,38 @@ /** * Prints or parses a zone ID. */ - static final class ZoneTextPrinterParser implements DateTimePrinterParser { + static final class ZoneTextPrinterParser extends ZoneIdPrinterParser { /** The text style to output. */ private final TextStyle textStyle; - ZoneTextPrinterParser(TextStyle textStyle) { + /** The preferred zoneid map */ + private Set preferredZones; + + ZoneTextPrinterParser(TextStyle textStyle, Set preferredZones) { + super(Queries.zone(), "ZoneText(" + textStyle + ")"); this.textStyle = Objects.requireNonNull(textStyle, "textStyle"); + if (preferredZones != null && preferredZones.size() != 0) { + this.preferredZones = new HashSet<>(); + for (ZoneId id : preferredZones) { + this.preferredZones.add(id.getId()); + } + } } private static final int STD = 0; private static final int DST = 1; private static final int GENERIC = 2; - private static final Map>> cache = new ConcurrentHashMap<>(); - private static String getDisplayName(String id, int type, TextStyle style, Locale locale) { - if (style == TextStyle.NARROW) { + private String getDisplayName(String id, int type, Locale locale) { + if (textStyle == TextStyle.NARROW) { return null; } String[] names; SoftReference> ref = cache.get(id); - Map perLocale; + Map perLocale = null; if (ref == null || (perLocale = ref.get()) == null || (names = perLocale.get(locale)) == null) { names = TimeZoneNameUtility.retrieveDisplayNames(id, locale); @@ -2707,55 +2938,99 @@ if (names[6] == null) { names[6] = names[0]; } - perLocale = new ConcurrentHashMap<>(); + if (perLocale == null) { + perLocale = new ConcurrentHashMap<>(); + } perLocale.put(locale, names); - ref = new SoftReference<>(perLocale); - cache.put(id, ref); + cache.put(id, new SoftReference<>(perLocale)); } switch (type) { case STD: - return names[style.ordinal() + 1]; + return names[textStyle.ordinal() + 1]; case DST: - return names[style.ordinal() + 3]; + return names[textStyle.ordinal() + 3]; } - return names[style.ordinal() + 5]; + return names[textStyle.ordinal() + 5]; } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { ZoneId zone = context.getValue(Queries.zoneId()); if (zone == null) { return false; } - if (zone instanceof ZoneOffset) { - buf.append(zone.getId()); - } else { + String zname = zone.getId(); + if (!(zone instanceof ZoneOffset)) { TemporalAccessor dt = context.getTemporal(); - Instant instant = null; - if (dt.isSupported(ChronoField.INSTANT_SECONDS)) { - instant = Instant.from(dt); - } - String name = getDisplayName(zone.getId(), - instant == null ? GENERIC - : (zone.getRules().isDaylightSavings(instant) ? DST : STD), - textStyle, context.getLocale()); + String name = getDisplayName(zname, + dt.isSupported(ChronoField.INSTANT_SECONDS) + ? (zone.getRules().isDaylightSavings(Instant.from(dt)) ? DST : STD) + : GENERIC, + context.getLocale()); if (name != null) { - buf.append(name); - } else { - buf.append(zone.getId()); + zname = name; } } + buf.append(zname); return true; } - @Override - public int parse(DateTimeParseContext context, CharSequence text, int position) { - throw new UnsupportedOperationException(); - } + // cache per instance for now + private final Map>> + cachedTree = new HashMap<>(); + private final Map>> + cachedTreeCI = new HashMap<>(); @Override - public String toString() { - return "ZoneText(" + textStyle + ")"; + protected PrefixTree getTree(DateTimeParseContext context) { + if (textStyle == TextStyle.NARROW) { + return super.getTree(context); + } + Locale locale = context.getLocale(); + boolean isCaseSensitive = context.isCaseSensitive(); + Set regionIds = ZoneRulesProvider.getAvailableZoneIds(); + int regionIdsSize = regionIds.size(); + + Map>> cached = + isCaseSensitive ? cachedTree : cachedTreeCI; + + Entry> entry = null; + PrefixTree tree = null; + String[][] zoneStrings = null; + if ((entry = cached.get(locale)) == null || + (entry.getKey() != regionIdsSize || + (tree = entry.getValue().get()) == null)) { + tree = PrefixTree.newTree(context); + zoneStrings = TimeZoneNameUtility.getZoneStrings(locale); + for (String[] names : zoneStrings) { + String zid = names[0]; + if (!regionIds.contains(zid)) { + continue; + } + tree.add(zid, zid); // don't convert zid -> metazone + zid = ZoneName.toZid(zid, locale); + int i = textStyle == TextStyle.FULL ? 1 : 2; + for (; i < names.length; i += 2) { + tree.add(names[i], zid); + } + } + // if we have a set of preferred zones, need a copy and + // add the preferred zones again to overwrite + if (preferredZones != null) { + for (String[] names : zoneStrings) { + String zid = names[0]; + if (!preferredZones.contains(zid) || !regionIds.contains(zid)) { + continue; + } + int i = textStyle == TextStyle.FULL ? 1 : 2; + for (; i < names.length; i += 2) { + tree.add(names[i], zid); + } + } + } + cached.put(locale, new SimpleImmutableEntry<>(regionIdsSize, new SoftReference<>(tree))); + } + return tree; } } @@ -2763,7 +3038,7 @@ /** * Prints or parses a zone ID. */ - static final class ZoneIdPrinterParser implements DateTimePrinterParser { + static class ZoneIdPrinterParser implements DateTimePrinterParser { private final TemporalQuery query; private final String description; @@ -2772,9 +3047,8 @@ this.description = description; } - //----------------------------------------------------------------------- @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf) { ZoneId zone = context.getValue(query); if (zone == null) { return false; @@ -2783,13 +3057,34 @@ return true; } - //----------------------------------------------------------------------- /** * The cached tree to speed up parsing. */ private static volatile Entry cachedPrefixTree; private static volatile Entry cachedPrefixTreeCI; + protected PrefixTree getTree(DateTimeParseContext context) { + // prepare parse tree + Set regionIds = ZoneRulesProvider.getAvailableZoneIds(); + final int regionIdsSize = regionIds.size(); + Entry cached = context.isCaseSensitive() + ? cachedPrefixTree : cachedPrefixTreeCI; + if (cached == null || cached.getKey() != regionIdsSize) { + synchronized (this) { + cached = context.isCaseSensitive() ? cachedPrefixTree : cachedPrefixTreeCI; + if (cached == null || cached.getKey() != regionIdsSize) { + cached = new SimpleImmutableEntry<>(regionIdsSize, PrefixTree.newTree(regionIds, context)); + if (context.isCaseSensitive()) { + cachedPrefixTree = cached; + } else { + cachedPrefixTreeCI = cached; + } + } + } + } + return cached.getValue(); + } + /** * This implementation looks for the longest matching string. * For example, parsing Etc/GMT-2 will return Etc/GMC-2 rather than just @@ -2801,58 +3096,57 @@ if (position > length) { throw new IndexOutOfBoundsException(); } + if (position == length) { + return ~position; + } // handle fixed time-zone IDs - if ((text.length() - position) >= 1) { - char nextChar = text.charAt(position); - if (nextChar == '+' || nextChar == '-') { - DateTimeParseContext newContext = context.copy(); - int endPos = OffsetIdPrinterParser.INSTANCE_ID.parse(newContext, text, position); - if (endPos < 0) { - return endPos; + char nextChar = text.charAt(position); + if (nextChar == '+' || nextChar == '-') { + return parseOffsetBased(context, text, position, OffsetIdPrinterParser.INSTANCE_ID_Z); + } else if (length >= position + 2) { + char nextNextChar = text.charAt(position + 1); + if (context.charEquals(nextChar, 'U') && context.charEquals(nextNextChar, 'T')) { + if (length >= position + 3 && context.charEquals(text.charAt(position + 2), 'C')) { + return parseOffsetBased(context, text, position + 3, OffsetIdPrinterParser.INSTANCE_ID_ZERO); } - int offset = (int) (long) newContext.getParsed(OFFSET_SECONDS); - ZoneId zone = ZoneOffset.ofTotalSeconds(offset); - context.setParsed(zone); - return endPos; + return parseOffsetBased(context, text, position + 2, OffsetIdPrinterParser.INSTANCE_ID_ZERO); + } else if (context.charEquals(nextChar, 'G') && length >= position + 3 && + context.charEquals(nextNextChar, 'M') && context.charEquals(text.charAt(position + 2), 'T')) { + return parseOffsetBased(context, text, position + 3, OffsetIdPrinterParser.INSTANCE_ID_ZERO); } } - // prepare parse tree - Set regionIds = ZoneRulesProvider.getAvailableZoneIds(); - final int regionIdsSize = regionIds.size(); - Entry cached = context.isCaseSensitive() - ? cachedPrefixTree : cachedPrefixTreeCI; - if (cached == null || cached.getKey() != regionIdsSize) { - synchronized (this) { - cached = context.isCaseSensitive() ? cachedPrefixTree : cachedPrefixTreeCI; - if (cached == null || cached.getKey() != regionIdsSize) { - cached = new SimpleImmutableEntry<>(regionIdsSize, - PrefixTree.newTree(regionIds, context.isCaseSensitive() - ? PrefixTree.STRICT : PrefixTree.CASE_INSENSITIVE)); - if (context.isCaseSensitive()) { - cachedPrefixTree = cached; - } else { - cachedPrefixTreeCI = cached; - } - } - } - } - PrefixTree tree = cached.getValue(); - // parse - String parsedZoneId = tree.match(text, position, length); - if (parsedZoneId == null || regionIds.contains(parsedZoneId) == false) { - if (text.charAt(position) == 'Z') { + PrefixTree tree = getTree(context); + ParsePosition ppos = new ParsePosition(position); + String parsedZoneId = tree.match(text, ppos); + if (parsedZoneId == null) { + if (context.charEquals(nextChar, 'Z')) { context.setParsed(ZoneOffset.UTC); return position + 1; } return ~position; } context.setParsed(ZoneId.of(parsedZoneId)); - return position + parsedZoneId.length(); + return ppos.getIndex(); } + private int parseOffsetBased(DateTimeParseContext context, CharSequence text, int position, OffsetIdPrinterParser parser) { + DateTimeParseContext newContext = context.copy(); + int endPos = parser.parse(newContext, text, position); + if (endPos < 0) { + if (parser == OffsetIdPrinterParser.INSTANCE_ID_Z) { + return ~position; + } + context.setParsed(ZoneOffset.UTC); + return position; + } + int offset = (int) newContext.getParsed(OFFSET_SECONDS).longValue(); + ZoneId zone = ZoneOffset.ofTotalSeconds(offset); + context.setParsed(zone); + return endPos; + } @Override public String toString() { @@ -2872,10 +3166,6 @@ protected PrefixTree child; protected PrefixTree sibling; - static final int STRICT = 1; - static final int CASE_INSENSITIVE = 2; - static final int LENIENT = 3; - private PrefixTree(String k, String v, PrefixTree child) { this.key = k; this.value = v; @@ -2888,40 +3178,30 @@ } /** - * Creates a new prefix parsing tree. + * Creates a new prefix parsing tree based on parse context. * - * @param type the type of the prefix tree. One of the three supported - * types, STRICT, CASE_INSENSITIVE and LENIENT + * @param context the parse context * @return the tree, not null */ - public static PrefixTree newTree(int type) { - PrefixTree tree; - switch(type) { - case STRICT: - tree = new PrefixTree("", null, null); - break; - case CASE_INSENSITIVE: - tree = new CI("", null, null); - break; - case LENIENT: - tree = new LENIENT("", null, null); - break; - default: - throw new IllegalArgumentException("Unknown type"); + public static PrefixTree newTree(DateTimeParseContext context) { + //if (!context.isStrict()) { + // return new LENIENT("", null, null); + //} + if (context.isCaseSensitive()) { + return new PrefixTree("", null, null); } - return tree; + return new CI("", null, null); } /** * Creates a new prefix parsing tree. * * @param keys a set of strings to build the prefix parsing tree, not null - * @param type the type of the prefix tree. One of the three supported - * types, STRICT, CASE_INSENSITIVE and LENIENT + * @param context the parse context * @return the tree, not null */ - public static PrefixTree newTree(Set keys, int type) { - PrefixTree tree = newTree(type); + public static PrefixTree newTree(Set keys, DateTimeParseContext context) { + PrefixTree tree = newTree(context); for (String k : keys) { tree.add0(k, k); } @@ -2929,6 +3209,21 @@ } /** + * Clone a copy of this tree + */ + public PrefixTree copyTree() { + PrefixTree copy = new PrefixTree(key, value, null); + if (child != null) { + copy.child = child.copyTree(); + } + if (sibling != null) { + copy.sibling = sibling.copyTree(); + } + return copy; + } + + + /** * Adds a pair of {key, value} into the prefix tree. * * @param k the key, not null @@ -2958,10 +3253,10 @@ child = c; return true; } - // have an existing already, keep it. - if (value != null) { - return false; - } + // have an existing already, overwrite it + // if (value != null) { + // return false; + //} value = v; return true; } @@ -3097,9 +3392,7 @@ @Override protected boolean isEqual(char c1, char c2) { - return c1 == c2 || - Character.toUpperCase(c1) == Character.toUpperCase(c2) || - Character.toLowerCase(c1) == Character.toLowerCase(c2); + return DateTimeParseContext.charEqualsIgnoreCase(c1, c2); } @Override @@ -3213,8 +3506,8 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { - Chrono chrono = context.getValue(Queries.chrono()); + public boolean format(DateTimePrintContext context, StringBuilder buf) { + Chronology chrono = context.getValue(Queries.chronology()); if (chrono == null) { return false; } @@ -3228,7 +3521,26 @@ @Override public int parse(DateTimeParseContext context, CharSequence text, int position) { - return ~position; // TODO, including case insensitive + // simple looping parser to find the chronology + if (position < 0 || position > text.length()) { + throw new IndexOutOfBoundsException(); + } + Set chronos = Chronology.getAvailableChronologies(); + Chronology bestMatch = null; + int matchLen = -1; + for (Chronology chrono : chronos) { + String id = chrono.getId(); + int idLen = id.length(); + if (idLen > matchLen && context.subSequenceEquals(text, position, id, 0, idLen)) { + bestMatch = chrono; + matchLen = idLen; + } + } + if (bestMatch == null) { + return ~position; + } + context.setParsed(bestMatch); + return position + matchLen; } } @@ -3239,40 +3551,40 @@ static final class LocalizedPrinterParser implements DateTimePrinterParser { private final FormatStyle dateStyle; private final FormatStyle timeStyle; - private final Chrono chrono; /** * Constructor. * * @param dateStyle the date style to use, may be null * @param timeStyle the time style to use, may be null - * @param chrono the chronology to use, not null */ - LocalizedPrinterParser(FormatStyle dateStyle, FormatStyle timeStyle, Chrono chrono) { + LocalizedPrinterParser(FormatStyle dateStyle, FormatStyle timeStyle) { // validated by caller this.dateStyle = dateStyle; this.timeStyle = timeStyle; - this.chrono = chrono; } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { - return formatter(context.getLocale()).toPrinterParser(false).print(context, buf); + public boolean format(DateTimePrintContext context, StringBuilder buf) { + Chronology chrono = Chronology.from(context.getTemporal()); + return formatter(context.getLocale(), chrono).toPrinterParser(false).format(context, buf); } @Override public int parse(DateTimeParseContext context, CharSequence text, int position) { - return formatter(context.getLocale()).toPrinterParser(false).parse(context, text, position); + Chronology chrono = context.getEffectiveChronology(); + return formatter(context.getLocale(), chrono).toPrinterParser(false).parse(context, text, position); } /** * Gets the formatter to use. * * @param locale the locale to use, not null + * @param chrono the chronology to use, not null * @return the formatter, not null * @throws IllegalArgumentException if the formatter cannot be found */ - private DateTimeFormatter formatter(Locale locale) { + private DateTimeFormatter formatter(Locale locale, Chronology chrono) { return DateTimeFormatStyleProvider.getInstance() .getFormatter(dateStyle, timeStyle, chrono, locale); } @@ -3280,7 +3592,7 @@ @Override public String toString() { return "Localized(" + (dateStyle != null ? dateStyle : "") + "," + - (timeStyle != null ? timeStyle : "") + "," + chrono.getId() + ")"; + (timeStyle != null ? timeStyle : "") + ")"; } } @@ -3309,8 +3621,8 @@ } @Override - public boolean print(DateTimePrintContext context, StringBuilder buf) { - return printerParser(context.getLocale()).print(context, buf); + public boolean format(DateTimePrintContext context, StringBuilder buf) { + return printerParser(context.getLocale()).format(context, buf); } @Override