8247781: Day periods support

Reviewed-by: scolebourne, rriggs, joehw
This commit is contained in:
Naoto Sato 2020-11-16 23:12:45 +00:00
parent 0357db3581
commit bf84dac4cf
20 changed files with 1155 additions and 203 deletions

View file

@ -300,6 +300,7 @@ import sun.util.locale.provider.TimeZoneNameUtility;
* <tr><th scope="row">F</th> <td>day-of-week-in-month</td> <td>number</td> <td>3</td>
*
* <tr><th scope="row">a</th> <td>am-pm-of-day</td> <td>text</td> <td>PM</td>
* <tr><th scope="row">B</th> <td>period-of-day</td> <td>text</td> <td>in the morning</td>
* <tr><th scope="row">h</th> <td>clock-hour-of-am-pm (1-12)</td> <td>number</td> <td>12</td>
* <tr><th scope="row">K</th> <td>hour-of-am-pm (0-11)</td> <td>number</td> <td>0</td>
* <tr><th scope="row">k</th> <td>clock-hour-of-day (1-24)</td> <td>number</td> <td>24</td>

View file

@ -102,6 +102,7 @@ import java.time.zone.ZoneRulesProvider;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@ -117,10 +118,13 @@ import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import sun.text.spi.JavaTimeDateTimePatternProvider;
import sun.util.locale.provider.CalendarDataUtility;
import sun.util.locale.provider.LocaleProviderAdapter;
import sun.util.locale.provider.LocaleResources;
import sun.util.locale.provider.TimeZoneNameUtility;
/**
@ -218,10 +222,9 @@ public final class DateTimeFormatterBuilder {
}
LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(JavaTimeDateTimePatternProvider.class, locale);
JavaTimeDateTimePatternProvider provider = adapter.getJavaTimeDateTimePatternProvider();
String pattern = provider.getJavaTimeDateTimePattern(convertStyle(timeStyle),
return provider.getJavaTimeDateTimePattern(convertStyle(timeStyle),
convertStyle(dateStyle), chrono.getCalendarType(),
CalendarDataUtility.findRegionOverride(locale));
return pattern;
}
/**
@ -678,7 +681,7 @@ public final class DateTimeFormatterBuilder {
* the minimum and maximum width. In strict mode, if the minimum and maximum widths
* are equal and there is no decimal point then the parser will
* participate in adjacent value parsing, see
* {@link appendValue(java.time.temporal.TemporalField, int)}. When parsing in lenient mode,
* {@link #appendValue(java.time.temporal.TemporalField, int)}. When parsing in lenient mode,
* the minimum width is considered to be zero and the maximum is nine.
* <p>
* If the value cannot be obtained then an exception will be thrown.
@ -1447,6 +1450,55 @@ public final class DateTimeFormatterBuilder {
return this;
}
/**
* Appends the day period text to the formatter.
* <p>
* This appends an instruction to format/parse the textual name of the day period
* to the builder. Day periods are defined in LDML's
* <a href="https://unicode.org/reports/tr35/tr35-dates.html#dayPeriods">"day periods"
* </a> element.
* <p>
* During formatting, the day period is obtained from {@code HOUR_OF_DAY}, and
* optionally {@code MINUTE_OF_HOUR} if exist. It will be mapped to a day period
* type defined in LDML, such as "morning1" and then it will be translated into
* text. Mapping to a day period type and its translation both depend on the
* locale in the formatter.
* <p>
* During parsing, the text will be parsed into a day period type first. Then
* the parsed day period is combined with other fields to make a {@code LocalTime} in
* the resolving phase. If the {@code HOUR_OF_AMPM} field is present, it is combined
* with the day period to make {@code HOUR_OF_DAY} taking into account any
* {@code MINUTE_OF_HOUR} value. If {@code HOUR_OF_DAY} is present, it is validated
* against the day period taking into account any {@code MINUTE_OF_HOUR} value. If a
* day period is present without {@code HOUR_OF_DAY}, {@code MINUTE_OF_HOUR},
* {@code SECOND_OF_MINUTE} and {@code NANO_OF_SECOND} then the midpoint of the
* day period is set as the time in {@code SMART} and {@code LENIENT} mode.
* For example, if the parsed day period type is "night1" and the period defined
* for it in the formatter locale is from 21:00 to 06:00, then it results in
* the {@code LocalTime} of 01:30.
* If the resolved time conflicts with the day period, {@code DateTimeException} is
* thrown in {@code STRICT} and {@code SMART} mode. In {@code LENIENT} mode, no
* exception is thrown and the parsed day period is ignored.
* <p>
* The "midnight" type allows both "00:00" as the start-of-day and "24:00" as the
* end-of-day, as long as they are valid with the resolved hour field.
*
* @param style the text style to use, not null
* @return this, for chaining, not null
* @since 16
*/
public DateTimeFormatterBuilder appendDayPeriodText(TextStyle style) {
Objects.requireNonNull(style, "style");
switch (style) {
// Stand-alone is not applicable. Convert to standard text style
case FULL_STANDALONE -> style = TextStyle.FULL;
case SHORT_STANDALONE -> style = TextStyle.SHORT;
case NARROW_STANDALONE -> style = TextStyle.NARROW;
}
appendInternal(new DayPeriodPrinterParser(style));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends all the elements of a formatter to the builder.
@ -1510,6 +1562,7 @@ public final class DateTimeFormatterBuilder {
* F day-of-week-in-month number 3
*
* a am-pm-of-day text PM
* B period-of-day text in the morning
* h clock-hour-of-am-pm (1-12) number 12
* K hour-of-am-pm (0-11) number 0
* k clock-hour-of-day (1-24) number 24
@ -1640,6 +1693,15 @@ public final class DateTimeFormatterBuilder {
* N..N 1..n appendValue(ChronoField.NANO_OF_DAY, n, 19, SignStyle.NOT_NEGATIVE)
* </pre>
* <p>
* <b>Day periods</b>: Pattern letters to output a day period.
* <pre>
* Pattern Count Equivalent builder methods
* ------- ----- --------------------------
* B 1 appendDayPeriodText(TextStyle.SHORT)
* BBBB 4 appendDayPeriodText(TextStyle.FULL)
* BBBBB 5 appendDayPeriodText(TextStyle.NARROW)
* </pre>
* <p>
* <b>Zone ID</b>: Pattern letters to output {@code ZoneId}.
* <pre>
* Pattern Count Equivalent builder methods
@ -1759,7 +1821,7 @@ public final class DateTimeFormatterBuilder {
} else if (count == 4) {
appendGenericZoneText(TextStyle.FULL);
} else {
throw new IllegalArgumentException("Wrong number of pattern letters: " + cur);
throw new IllegalArgumentException("Wrong number of pattern letters: " + cur);
}
} else if (cur == 'Z') {
if (count < 4) {
@ -1809,6 +1871,13 @@ public final class DateTimeFormatterBuilder {
} else {
appendValue(new WeekBasedFieldPrinterParser(cur, count, count, 19));
}
} else if (cur == 'B') {
switch (count) {
case 1 -> appendDayPeriodText(TextStyle.SHORT);
case 4 -> appendDayPeriodText(TextStyle.FULL);
case 5 -> appendDayPeriodText(TextStyle.NARROW);
default -> throw new IllegalArgumentException("Wrong number of pattern letters: " + cur);
}
} else {
throw new IllegalArgumentException("Unknown pattern letter: " + cur);
}
@ -1920,19 +1989,10 @@ public final class DateTimeFormatterBuilder {
break;
case 'G':
switch (count) {
case 1:
case 2:
case 3:
appendText(field, TextStyle.SHORT);
break;
case 4:
appendText(field, TextStyle.FULL);
break;
case 5:
appendText(field, TextStyle.NARROW);
break;
default:
throw new IllegalArgumentException("Too many pattern letters: " + cur);
case 1, 2, 3 -> appendText(field, TextStyle.SHORT);
case 4 -> appendText(field, TextStyle.FULL);
case 5 -> appendText(field, TextStyle.NARROW);
default -> throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'S':
@ -2025,6 +2085,7 @@ public final class DateTimeFormatterBuilder {
// 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5
// 310 - x - matches LDML
// 310 - w, W, and Y are localized forms matching LDML
// LDML - B - day periods
// LDML - U - cycle year name, not supported by 310 yet
// LDML - l - deprecated
// LDML - j - not relevant
@ -2160,9 +2221,7 @@ public final class DateTimeFormatterBuilder {
private int appendInternal(DateTimePrinterParser pp) {
Objects.requireNonNull(pp, "pp");
if (active.padNextWidth > 0) {
if (pp != null) {
pp = new PadPrinterParserDecorator(pp, active.padNextWidth, active.padNextChar);
}
pp = new PadPrinterParserDecorator(pp, active.padNextWidth, active.padNextChar);
active.padNextWidth = 0;
active.padNextChar = 0;
}
@ -2311,7 +2370,7 @@ public final class DateTimeFormatterBuilder {
private final boolean optional;
CompositePrinterParser(List<DateTimePrinterParser> printerParsers, boolean optional) {
this(printerParsers.toArray(new DateTimePrinterParser[printerParsers.size()]), optional);
this(printerParsers.toArray(new DateTimePrinterParser[0]), optional);
}
CompositePrinterParser(DateTimePrinterParser[] printerParsers, boolean optional) {
@ -3129,11 +3188,11 @@ public final class DateTimeFormatterBuilder {
}
/**
* For FractionPrinterPrinterParser, the width is fixed if context is sttrict,
* For FractionPrinterPrinterParser, the width is fixed if context is strict,
* minWidth equal to maxWidth and decimalpoint is absent.
* @param context the context
* @return if the field is fixed width
* @see DateTimeFormatterBuilder#appendValueFraction(java.time.temporal.TemporalField, int, int, boolean)
* @see #appendFraction(java.time.temporal.TemporalField, int, int, boolean)
*/
@Override
boolean isFixedWidth(DateTimeParseContext context) {
@ -4975,14 +5034,296 @@ public final class DateTimeFormatterBuilder {
}
}
//-------------------------------------------------------------------------
//-----------------------------------------------------------------------
/**
* Length comparator.
* Prints or parses day periods.
*/
static final Comparator<String> LENGTH_SORT = new Comparator<String>() {
@Override
public int compare(String str1, String str2) {
return str1.length() == str2.length() ? str1.compareTo(str2) : str1.length() - str2.length();
static final class DayPeriodPrinterParser implements DateTimePrinterParser {
private final TextStyle textStyle;
private final static ConcurrentMap<Locale, LocaleStore> DAYPERIOD_LOCALESTORE = new ConcurrentHashMap<>();
/**
* Constructor.
*
* @param textStyle the text style, not null
*/
DayPeriodPrinterParser(TextStyle textStyle) {
// validated by caller
this.textStyle = textStyle;
}
};
@Override
public boolean format(DateTimePrintContext context, StringBuilder buf) {
Long hod = context.getValue(HOUR_OF_DAY);
if (hod == null) {
return false;
}
Long moh = context.getValue(MINUTE_OF_HOUR);
long value = Math.floorMod(hod, 24) * 60 + (moh != null ? Math.floorMod(moh, 60) : 0);
Locale locale = context.getLocale();
LocaleStore store = findDayPeriodStore(locale);
final long val = value;
final var map = DayPeriod.getDayPeriodMap(locale);
value = map.keySet().stream()
.filter(k -> k.includes(val))
.min(DayPeriod.DPCOMPARATOR)
.map(map::get)
.orElse(val / 720); // fall back to am/pm
String text = store.getText(value, textStyle);
buf.append(text);
return true;
}
@Override
public int parse(DateTimeParseContext context, CharSequence parseText, int position) {
int length = parseText.length();
if (position < 0 || position > length) {
throw new IndexOutOfBoundsException();
}
TextStyle style = (context.isStrict() ? textStyle : null);
Iterator<Entry<String, Long>> it;
LocaleStore store = findDayPeriodStore(context.getLocale());
it = store.getTextIterator(style);
if (it != null) {
while (it.hasNext()) {
Entry<String, Long> entry = it.next();
String itText = entry.getKey();
if (context.subSequenceEquals(itText, 0, parseText, position, itText.length())) {
context.setParsedDayPeriod(DayPeriod.ofLocale(context.getLocale(), entry.getValue()));
return position + itText.length();
}
}
}
return ~position;
}
@Override
public String toString() {
return "DayPeriod(" + textStyle + ")";
}
/**
* Returns the day period locale store for the locale
* @param locale locale to be examined
* @return locale store for the locale
*/
private static LocaleStore findDayPeriodStore(Locale locale) {
return DAYPERIOD_LOCALESTORE.computeIfAbsent(locale, loc -> {
Map<TextStyle, Map<Long, String>> styleMap = new HashMap<>();
for (TextStyle textStyle : TextStyle.values()) {
if (textStyle.isStandalone()) {
// Stand-alone isn't applicable to day period.
continue;
}
Map<Long, String> map = new HashMap<>();
int calStyle = textStyle.toCalendarStyle();
var periodMap = DayPeriod.getDayPeriodMap(loc);
periodMap.forEach((key, value) -> {
String displayName = CalendarDataUtility.retrieveJavaTimeFieldValueName(
"gregory", Calendar.AM_PM, value.intValue(), calStyle, loc);
if (displayName != null) {
map.put(value, displayName);
} else {
periodMap.remove(key);
}
});
if (!map.isEmpty()) {
styleMap.put(textStyle, map);
}
}
return new LocaleStore(styleMap);
});
}
}
/**
* DayPeriod class that represents a
* <a href="https://www.unicode.org/reports/tr35/tr35-dates.html#dayPeriods">DayPeriod</a> defined in CLDR.
* This is a value-based class.
*/
static final class DayPeriod {
/**
* DayPeriod cache
*/
private final static Map<Locale, Map<DayPeriod, Long>> DAYPERIOD_CACHE = new ConcurrentHashMap<>();
/**
* comparator based on the duration of the day period.
*/
private final static Comparator<DayPeriod> DPCOMPARATOR = (dp1, dp2) -> (int)(dp1.duration() - dp2.duration());
/**
* Pattern to parse day period rules
*/
private final static Pattern RULE = Pattern.compile("(?<type>[a-z12]+):(?<from>\\d{2}):00(-(?<to>\\d{2}))*");
/**
* minute-of-day of "at" or "from" attribute
*/
private final long from;
/**
* minute-of-day of "before" attribute (exclusive), or if it is
* the same value with "from", it indicates this day period
* designates "fixed" periods, i.e, "midnight" or "noon"
*/
private final long to;
/**
* day period type index. (cf. {@link #mapToIndex})
*/
private final long index;
/**
* Sole constructor
*
* @param from "from" in minute-of-day
* @param to "to" in minute-of-day
* @param index day period type index
*/
private DayPeriod(long from, long to, long index) {
this.from = from;
this.to = to;
this.index = index;
}
/**
* Gets the index of this day period
*
* @return index
*/
long getIndex() {
return index;
}
/**
* Returns the midpoint of this day period in minute-of-day
* @return midpoint
*/
long mid() {
return (from + duration() / 2) % 1_440;
}
/**
* Checks whether the passed minute-of-day is within this
* day period or not.
*
* @param mod minute-of-day to check
* @return true if {@code mod} is within this day period
*/
boolean includes(long mod) {
// special check for 24:00 for midnight in hour-of-day
if (from == 0 && to == 0 && mod == 1_440) {
return true;
}
return (from == mod && to == mod || // midnight/noon
from <= mod && mod < to || // contiguous from-to
from > to && (from <= mod || to > mod)); // beyond midnight
}
/**
* Calculates the duration of this day period
* @return the duration in minutes
*/
private long duration() {
return from > to ? 1_440 - from + to: to - from;
}
/**
* Maps the day period type defined in LDML to the index to the am/pm array
* returned from the Calendar resource bundle.
*
* @param type day period type defined in LDML
* @return the array index
*/
static long mapToIndex(String type) {
return switch (type) {
case "am" -> Calendar.AM;
case "pm" -> Calendar.PM;
case "midnight" -> 2;
case "noon" -> 3;
case "morning1" -> 4;
case "morning2" -> 5;
case "afternoon1" -> 6;
case "afternoon2" -> 7;
case "evening1" -> 8;
case "evening2" -> 9;
case "night1" -> 10;
case "night2" -> 11;
default -> throw new InternalError("invalid day period type");
};
}
/**
* Returns the DayPeriod to array index map for a locale.
*
* @param locale the locale, not null
* @return the DayPeriod to type index map
*/
static Map<DayPeriod, Long> getDayPeriodMap(Locale locale) {
return DAYPERIOD_CACHE.computeIfAbsent(locale, l -> {
LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased()
.getLocaleResources(CalendarDataUtility.findRegionOverride(l));
String dayPeriodRules = lr.getRules()[1];
final Map<DayPeriod, Long> periodMap = new ConcurrentHashMap<>();
Arrays.stream(dayPeriodRules.split(";"))
.forEach(rule -> {
Matcher m = RULE.matcher(rule);
if (m.find()) {
String from = m.group("from");
String to = m.group("to");
long index = DayPeriod.mapToIndex(m.group("type"));
if (to == null) {
to = from;
}
periodMap.putIfAbsent(
new DayPeriod(
Long.parseLong(from) * 60,
Long.parseLong(to) * 60,
index),
index);
}
});
// add am/pm
periodMap.putIfAbsent(new DayPeriod(0, 720, 0), 0L);
periodMap.putIfAbsent(new DayPeriod(720, 1_440, 1), 1L);
return periodMap;
});
}
/**
* Returns the DayPeriod singleton for the locale and index.
* @param locale desired locale
* @param index resource bundle array index
* @return a DayPeriod instance
*/
static DayPeriod ofLocale(Locale locale, long index) {
return getDayPeriodMap(locale).keySet().stream()
.filter(dp -> dp.getIndex() == index)
.findAny()
.orElseThrow(() -> new DateTimeException(
"DayPeriod could not be determined for the locale " +
locale + " at type index " + index));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DayPeriod dayPeriod = (DayPeriod) o;
return from == dayPeriod.from &&
to == dayPeriod.to &&
index == dayPeriod.index;
}
@Override
public int hashCode() {
return Objects.hash(from, to, index);
}
@Override
public String toString() {
return "DayPeriod(%02d:%02d".formatted(from / 60, from % 60) +
(from == to ? ")" : "-%02d:%02d)".formatted(to / 60, to % 60));
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -424,6 +424,15 @@ final class DateTimeParseContext {
currentParsed().leapSecond = true;
}
/**
* Stores the parsed day period.
*
* @param dayPeriod the parsed day period
*/
void setParsedDayPeriod(DateTimeFormatterBuilder.DayPeriod dayPeriod) {
currentParsed().dayPeriod = dayPeriod;
}
//-----------------------------------------------------------------------
/**
* Returns a string version of the context for debugging.

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -61,6 +61,7 @@
*/
package java.time.format;
import static java.time.format.DateTimeFormatterBuilder.DayPeriod;
import static java.time.temporal.ChronoField.AMPM_OF_DAY;
import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_AMPM;
import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_DAY;
@ -155,6 +156,10 @@ final class Parsed implements TemporalAccessor {
* The excess period from time-only parsing.
*/
Period excessDays = Period.ZERO;
/**
* The parsed day period.
*/
DayPeriod dayPeriod;
/**
* Creates an instance.
@ -172,6 +177,7 @@ final class Parsed implements TemporalAccessor {
cloned.zone = this.zone;
cloned.chrono = this.chrono;
cloned.leapSecond = this.leapSecond;
cloned.dayPeriod = this.dayPeriod;
return cloned;
}
@ -332,7 +338,8 @@ final class Parsed implements TemporalAccessor {
}
}
//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
private void resolveInstantFields() {
// resolve parsed instant seconds to date and time if zone available
if (fieldValues.containsKey(INSTANT_SECONDS)) {
@ -470,6 +477,19 @@ final class Parsed implements TemporalAccessor {
}
}
if (dayPeriod != null && fieldValues.containsKey(HOUR_OF_AMPM)) {
long hoap = fieldValues.remove(HOUR_OF_AMPM);
if (resolverStyle != ResolverStyle.LENIENT) {
HOUR_OF_AMPM.checkValidValue(hoap);
}
Long mohObj = fieldValues.get(MINUTE_OF_HOUR);
long moh = mohObj != null ? Math.floorMod(mohObj, 60) : 0;
long excessHours = dayPeriod.includes((Math.floorMod(hoap, 12) + 12) * 60 + moh) ? 12 : 0;
long hod = Math.addExact(hoap, excessHours);
updateCheckConflict(HOUR_OF_AMPM, HOUR_OF_DAY, hod);
dayPeriod = null;
}
// convert to time if all four fields available (optimization)
if (fieldValues.containsKey(HOUR_OF_DAY) && fieldValues.containsKey(MINUTE_OF_HOUR) &&
fieldValues.containsKey(SECOND_OF_MINUTE) && fieldValues.containsKey(NANO_OF_SECOND)) {
@ -506,6 +526,27 @@ final class Parsed implements TemporalAccessor {
fieldValues.put(NANO_OF_SECOND, cos * 1_000L);
}
// Set the hour-of-day, if not exist and not in STRICT, to the mid point of the day period or am/pm.
if (!fieldValues.containsKey(HOUR_OF_DAY) &&
!fieldValues.containsKey(MINUTE_OF_HOUR) &&
!fieldValues.containsKey(SECOND_OF_MINUTE) &&
!fieldValues.containsKey(NANO_OF_SECOND) &&
resolverStyle != ResolverStyle.STRICT) {
if (dayPeriod != null) {
long midpoint = dayPeriod.mid();
resolveTime(midpoint / 60, midpoint % 60, 0, 0);
dayPeriod = null;
} else if (fieldValues.containsKey(AMPM_OF_DAY)) {
long ap = fieldValues.remove(AMPM_OF_DAY);
if (resolverStyle == ResolverStyle.LENIENT) {
resolveTime(Math.addExact(Math.multiplyExact(ap, 12), 6), 0, 0, 0);
} else { // SMART
AMPM_OF_DAY.checkValidValue(ap);
resolveTime(ap * 12 + 6, 0, 0, 0);
}
}
}
// merge hour/minute/second/nano leniently
Long hod = fieldValues.get(HOUR_OF_DAY);
if (hod != null) {
@ -523,6 +564,15 @@ final class Parsed implements TemporalAccessor {
long mohVal = (moh != null ? moh : 0);
long somVal = (som != null ? som : 0);
long nosVal = (nos != null ? nos : 0);
if (dayPeriod != null && resolverStyle != ResolverStyle.LENIENT) {
// Check whether the hod/mohVal is within the day period
if (!dayPeriod.includes(hod * 60 + mohVal)) {
throw new DateTimeException("Conflict found: Resolved time %02d:%02d".formatted(hod, mohVal) +
" conflicts with " + dayPeriod);
}
}
resolveTime(hod, mohVal, somVal, nosVal);
fieldValues.remove(HOUR_OF_DAY);
fieldValues.remove(MINUTE_OF_HOUR);

View file

@ -333,8 +333,9 @@ public enum ChronoField implements TemporalField {
* When parsing this field it behaves equivalent to the following:
* The value is validated from 0 to 1 in strict and smart mode.
* In lenient mode the value is not validated. It is combined with
* {@code HOUR_OF_AMPM} to form {@code HOUR_OF_DAY} by multiplying
* the {@code AMPM_OF_DAY} value by 12.
* {@code HOUR_OF_AMPM} (if not present, it defaults to '6') to form
* {@code HOUR_OF_DAY} by multiplying the {@code AMPM_OF_DAY} value
* by 12.
*/
AMPM_OF_DAY("AmPmOfDay", HALF_DAYS, DAYS, ValueRange.of(0, 1), "dayperiod"),
/**