/* * ----------------------------------------------------------------------- * Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/> * ----------------------------------------------------------------------- * This file (DayPeriod.java) is part of project Time4J. * * Time4J is free software: You can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * Time4J is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Time4J. If not, see <http://www.gnu.org/licenses/>. * ----------------------------------------------------------------------- */ package net.time4j; import net.time4j.engine.AttributeKey; import net.time4j.engine.AttributeQuery; import net.time4j.engine.BasicElement; import net.time4j.engine.ChronoDisplay; import net.time4j.engine.ChronoElement; import net.time4j.engine.ChronoEntity; import net.time4j.engine.ChronoException; import net.time4j.engine.ChronoExtension; import net.time4j.engine.ChronoFunction; import net.time4j.engine.Chronology; import net.time4j.engine.ElementRule; import net.time4j.format.Attributes; import net.time4j.format.CalendarText; import net.time4j.format.OutputContext; import net.time4j.format.TextElement; import net.time4j.format.TextWidth; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; /** * <p>Represents a period or part of a day usually in minute precision * as formattable extension to {@code PlainTime}. </p> * * <p>The i18n-module is necessary to exploit the full functionality otherwise this class will just * fall back to AM/PM only. Most non-English speaking countries completely ignore the notation of AM/PM * but know day periods with finer granularity such as "in the morning", "evening" etc. * Users can get extensive format support for day periods via the CLDR-pattern symbols b or B. The combination * of half-day-hours with a day period is especially useful. Example of usage: </p> * * <pre> * ChronoFormatter<PlainTime> f = * ChronoFormatter.ofTimePattern("h:mm BBBB", PatternType.CLDR, Locale.ENGLISH); * System.out.println(f.format(PlainTime.of(17, 15))); // output => 5:15 in the afternoon * </pre> * * @author Meno Hochschild * @since 3.13/4.10 * @doctags.concurrency {immutable} */ /*[deutsch] * <p>Repräsentiert einen üblicherweise minutengenauen Tagesabschnitt * als formatierbare Erweiterung zu {@code PlainTime}. </p> * * <p>Das i18n-Modul ist notwendig, um die volle Funktionalität auszuschöpfen, sonst wird * diese Klasse nur auf AM/PM zurückfallen. Die meisten nicht Englisch sprechenden Länder * ignorieren vollständig die AM/PM-Schreibweise, kennen aber Tagesabschnitte mit feinerer * Detaillierung wie zum Beispiel "morgens", "Abend" usw. Anwender können * Formatunterstützung mit Hilfe der CLDR-Formatmustersymbole b oder B erhalten. Die Kombination * von Halbtagsstunden mit Tagesabschnitten ist besonders nützlich. Anwendungsbeispiel: </p> * * <pre> * ChronoFormatter<PlainTime> f = * ChronoFormatter.ofTimePattern("h:mm BBBB", PatternType.CLDR, Locale.ENGLISH); * System.out.println(f.format(PlainTime.of(17, 15))); // output => 5:15 in the afternoon * </pre> * * @author Meno Hochschild * @since 3.13/4.10 * @doctags.concurrency {immutable} */ public final class DayPeriod { //~ Statische Felder/Initialisierungen -------------------------------- private static final SortedMap<PlainTime, String> STD_RULES; static { SortedMap<PlainTime, String> rules = new TreeMap<>(); rules.put(PlainTime.midnightAtStartOfDay(), "am"); rules.put(PlainTime.of(12), "pm"); STD_RULES = Collections.unmodifiableSortedMap(rules); } private static DayPeriod FALLBACK = new DayPeriod(Locale.ROOT, CalendarText.ISO_CALENDAR_TYPE, STD_RULES); private static final AttributeKey<DayPeriod> CUSTOM = Attributes.createKey("CUSTOM_DAY_PERIOD", DayPeriod.class); //~ Instanzvariablen -------------------------------------------------- private transient final Locale locale; private transient final String calendarType; private transient final SortedMap<PlainTime, String> codeMap; //~ Konstruktoren ----------------------------------------------------- private DayPeriod( Locale locale, // optional String calendarType, SortedMap<PlainTime, String> codeMap ) { super(); this.locale = locale; this.calendarType = calendarType; this.codeMap = Collections.unmodifiableSortedMap(codeMap); } //~ Methoden ---------------------------------------------------------- /** * <p>Creates an instance based on locale-specific predefined data. </p> * * <p>If given locale does not point to any predefined data then Time4J will fall back to AM/PM. </p> * * @param locale contains the language setting * @return locale-specific instance * @since 3.13/4.10 */ /*[deutsch] * <p>Erzeugt eine Instanz, die auf sprachspezifischen vordefinierten Daten beruht. </p> * * <p>Wenn die angegebene Sprache nicht auf vordefinierte Daten verweist, wird Time4J auf AM/PM ausweichen. </p> * * @param locale contains the language setting * @return locale-specific instance * @since 3.13/4.10 */ public static DayPeriod of(Locale locale) { return DayPeriod.of(locale, CalendarText.ISO_CALENDAR_TYPE); } /** * <p>Creates an instance based on user-defined data. </p> * * @param timeToLabels map containing the day-periods where the keys represent starting points * and the values represent the associated labels intended for representation * @return user-specific instance * @throws IllegalArgumentException if given map is empty or contains empty values * @since 3.13/4.10 */ /*[deutsch] * <p>Erzeugt eine Instanz, die auf benutzerdefinierten Daten beruht. </p> * * @param timeToLabels map containing the day-periods where the keys represent starting points * and the values represent the associated labels intended for representation * @return user-specific instance * @throws IllegalArgumentException if given map is empty or contains empty values * @since 3.13/4.10 */ public static DayPeriod of(Map<PlainTime, String> timeToLabels) { if (timeToLabels.isEmpty()) { throw new IllegalArgumentException("Label map is empty."); } SortedMap<PlainTime, String> map = new TreeMap<>(timeToLabels); for (PlainTime key : timeToLabels.keySet()) { if (key.getHour() == 24) { map.put(PlainTime.midnightAtStartOfDay(), timeToLabels.get(key)); map.remove(key); } else if (timeToLabels.get(key).isEmpty()) { throw new IllegalArgumentException("Map has empty label: " + timeToLabels); } } return new DayPeriod(null, "", map); } /** * <p>Equivalent to {@code fixed(TextWidth.WIDE, OutputContext.FORMAT)}. </p> * * @return fixed textual representation of day period as function applicable on {@code PlainTime} etc. * @see #fixed(TextWidth, OutputContext) * @since 3.13/4.10 */ /*[deutsch] * <p>Äquivalent zu {@code fixed(TextWidth.WIDE, OutputContext.FORMAT)}. </p> * * @return fixed textual representation of day period as function applicable on {@code PlainTime} etc. * @see #fixed(TextWidth, OutputContext) * @since 3.13/4.10 */ public ChronoFunction<ChronoDisplay, String> fixed() { return this.fixed(TextWidth.WIDE, OutputContext.FORMAT); } /** * <p>Represents a fixed day period (am / pm / midnight / noon). </p> * * <p>The function returned can be applied on either {@code PlainTime} or {@code PlainTimestamp}. * Otherwise it throws a {@code ChronoException} if an instance of {@code PlainTime} cannot be found. * If this day period was not created for a locale then the function will just return one of the * literals "am", "pm", "midnight" or "noon". </p> * * @param width determines the text width * @param outputContext determines in which context to format * @return fixed textual representation of day period as function applicable on {@code PlainTime} etc. * @since 3.13/4.10 */ /*[deutsch] * <p>Repräsentiert einen fest definierten Tagesabschnitt (am / pm / midnight / noon). </p> * * <p>Die Funktion kann entweder auf {@code PlainTime} oder {@code PlainTimestamp} angewandt werden. * Sonst wirft sie eine {@code ChronoException}, wenn keine Instanz von {@code PlainTime} gefunden wird. * Wenn diese {@code DayPeriod} nicht für eine bestimmte Sprache erzeugt wurde, dann wird die * Funktion einfach nur eines der Literale "am", "pm", "midnight" oder * "noon" zurückgeben. </p> * * @param width determines the text width * @param outputContext determines in which context to format * @return fixed textual representation of day period as function applicable on {@code PlainTime} etc. * @since 3.13/4.10 */ public ChronoFunction<ChronoDisplay, String> fixed( TextWidth width, OutputContext outputContext ) { return new PeriodName(true, width, outputContext); } /** * <p>Equivalent to {@code approximate(TextWidth.WIDE, OutputContext.FORMAT)}. </p> * * @return approximate textual representation of day period as function applicable on {@code PlainTime} etc. * @see #approximate(TextWidth, OutputContext) * @since 3.13/4.10 */ /*[deutsch] * <p>Äquivalent zu {@code approximate(TextWidth.WIDE, OutputContext.FORMAT)}. </p> * * @return approximate textual representation of day period as function applicable on {@code PlainTime} etc. * @see #approximate(TextWidth, OutputContext) * @since 3.13/4.10 */ public ChronoFunction<ChronoDisplay, String> approximate() { return this.approximate(TextWidth.WIDE, OutputContext.FORMAT); } /** * <p>Represents a flexible day period (in the afternoon, at night etc). </p> * * <p>The function returned can be applied on either {@code PlainTime} or {@code PlainTimestamp}. * Otherwise it throws a {@code ChronoException} if an instance of {@code PlainTime} cannot be found. * If no suitable text can be determined then the function falls back to AM/PM. </p> * * @param width determines the text width * @param outputContext determines in which context to format * @return approximate textual representation of day period as function applicable on {@code PlainTime} etc. * @since 3.13/4.10 */ /*[deutsch] * <p>Repräsentiert einen flexiblen Tagesabschnitt (nachmittags, nachts usw). </p> * * <p>Die Funktion kann entweder auf {@code PlainTime} oder {@code PlainTimestamp} angewandt werden. * Sonst wirft sie eine {@code ChronoException}, wenn keine Instanz von {@code PlainTime} gefunden wird. * Wenn die Funktion keinen geeigneten Text findet, fällt sie auf AM/PM zurück. </p> * * @param width determines the text width * @param outputContext determines in which context to format * @return approximate textual representation of day period as function applicable on {@code PlainTime} etc. * @since 3.13/4.10 */ public ChronoFunction<ChronoDisplay, String> approximate( TextWidth width, OutputContext outputContext ) { return new PeriodName(false, width, outputContext); } /** * <p>Determines the start of the day period which covers given clock time. </p> * * @param context the clock time a day period is searched for * @return start of day period around given clock time, inclusive * @see #getEnd(PlainTime) * @since 3.13/4.10 */ /*[deutsch] * <p>Ermittelt den Start des Tagesabschnitts, der die angegebene Uhrzeit enthält. </p> * * @param context the clock time a day period is searched for * @return start of day period around given clock time, inclusive * @see #getEnd(PlainTime) * @since 3.13/4.10 */ public PlainTime getStart(PlainTime context) { PlainTime compare = ( (context.getHour() == 24) ? PlainTime.midnightAtStartOfDay() : context); PlainTime last = this.codeMap.lastKey(); for (PlainTime key : this.codeMap.keySet()) { if (compare.isSimultaneous(key)) { return key; } else if (compare.isBefore(key)) { break; } else { last = key; } } return last; } /** * <p>Determines the end of the day period which covers given clock time. </p> * * @param context the clock time a day period is searched for * @return end of day period around given clock time, exclusive * @see #getStart(PlainTime) * @since 3.13/4.10 */ /*[deutsch] * <p>Ermittelt das Ende des Tagesabschnitts, der die angegebene Uhrzeit enthält. </p> * * @param context the clock time a day period is searched for * @return end of day period around given clock time, exclusive * @see #getStart(PlainTime) * @since 3.13/4.10 */ public PlainTime getEnd(PlainTime context) { PlainTime compare = ( (context.getHour() == 24) ? PlainTime.midnightAtStartOfDay() : context); for (PlainTime key : this.codeMap.keySet()) { if (compare.isBefore(key)) { return key; } } return this.codeMap.firstKey(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof DayPeriod) { DayPeriod that = (DayPeriod) obj; if (this.locale == null) { if (that.locale != null) { return false; } } else if (!this.locale.equals(that.locale)) { return false; } return (this.codeMap.equals(that.codeMap) && this.calendarType.equals(that.calendarType)); } return false; } @Override public int hashCode() { return this.codeMap.hashCode(); } /** * For debugging purposes. * * @return String */ /*[deutsch] * Für Debugging-Zwecke. * * @return String */ @Override public String toString() { StringBuilder sb = new StringBuilder(64); sb.append("DayPeriod["); if (this.isPredefined()) { sb.append("locale="); sb.append(this.locale); sb.append(','); if (!this.calendarType.equals(CalendarText.ISO_CALENDAR_TYPE)) { sb.append(",calendar-type="); sb.append(this.calendarType); sb.append(','); } } sb.append(this.codeMap); sb.append(']'); return sb.toString(); } // package-private because used in deserialization static DayPeriod of( Locale locale, String calendarType ) { String lang = locale.getLanguage(); // NPE-check if (lang.equals("nn")) { locale = new Locale("nb"); // CLDR 29 contains no data for language nn } Map<String, String> resourceMap = loadTextForms(locale, calendarType); SortedMap<PlainTime, String> codeMap = Collections.emptySortedMap(); for (String key : resourceMap.keySet()) { if (accept(key)) { int hour = Integer.parseInt(key.substring(1, 3)); int minute = Integer.parseInt(key.substring(3, 5)); PlainTime time = PlainTime.midnightAtStartOfDay(); if (hour == 24) { if (minute != 0) { throw new IllegalStateException("Invalid time key: " + key); } } else if ((hour >= 0) && (hour < 24) && (minute >= 0) && (minute < 60)) { time = time.plus(hour * 60 + minute, ClockUnit.MINUTES); } else { throw new IllegalStateException("Invalid time key: " + key); } if (codeMap.isEmpty()) { codeMap = new TreeMap<>(); } codeMap.put(time, resourceMap.get(key)); } } if (codeMap.isEmpty() || lang.isEmpty()) { return FALLBACK; } Iterator<PlainTime> iter = codeMap.keySet().iterator(); String oldCode = ""; while (iter.hasNext()) { PlainTime time = iter.next(); String code = codeMap.get(time); if (code.equals(oldCode)) { iter.remove(); // lex colombia } else { oldCode = code; } } return new DayPeriod(locale, calendarType, codeMap); } private boolean isPredefined() { return (this.locale != null); } private static String getFixedCode(PlainTime time) { int minuteOfDay = time.get(PlainTime.MINUTE_OF_DAY).intValue(); if ((minuteOfDay == 0) || (minuteOfDay == 1440)) { return "midnight"; } else if (minuteOfDay < 720) { return "am"; } else if (minuteOfDay == 720) { return "noon"; } else { return "pm"; } } private static String createKey( Map<String, String> textForms, TextWidth tw, OutputContext oc, String code ) { if (tw == TextWidth.SHORT) { tw = TextWidth.ABBREVIATED; } String key = toPrefix(tw, oc) + code; if (!textForms.containsKey(key)) { if (oc == OutputContext.STANDALONE) { if (tw == TextWidth.ABBREVIATED) { key = createKey(textForms, tw, OutputContext.FORMAT, code); } else { key = createKey(textForms, TextWidth.ABBREVIATED, oc, code); } } else { if (tw != TextWidth.ABBREVIATED) { key = createKey(textForms, TextWidth.ABBREVIATED, oc, code); } } } return key; } private static String toPrefix( TextWidth tw, OutputContext oc ) { char c; switch (tw) { case WIDE: c = 'w'; break; case NARROW: c = 'n'; break; default: c = 'a'; } if (oc == OutputContext.STANDALONE) { c = Character.toUpperCase(c); } return "P(" + c + ")_"; } private static Map<String, String> loadTextForms( Locale locale, String calendarType ) { Map<String, String> map = CalendarText.getInstance(calendarType, locale).getTextForms(); if ( !calendarType.equals(CalendarText.ISO_CALENDAR_TYPE) && !"true".equals(map.get("hasDayPeriods")) ) { map = CalendarText.getIsoInstance(locale).getTextForms(); // fallback } return map; } private static boolean accept(String key) { return ((key.charAt(0) == 'T') && (key.length() == 5) && Character.isDigit(key.charAt(1))); } //~ Innere Klassen ---------------------------------------------------- static class Extension implements ChronoExtension { //~ Methoden ------------------------------------------------------ @Override public boolean accept(Class<?> chronoType) { return PlainTime.class.isAssignableFrom(chronoType); // not used } @Override public Set<ChronoElement<?>> getElements( Locale locale, AttributeQuery attributes ) { DayPeriod dp = from(locale, attributes); Set<ChronoElement<?>> set = new HashSet<>(); set.add(new Element(false, dp)); if (!attributes.contains(CUSTOM)) { set.add(new Element(true, dp)); // fixed } return Collections.unmodifiableSet(set); } @Override public ChronoEntity<?> resolve( ChronoEntity<?> entity, Locale locale, AttributeQuery attributes ) { if ( entity.contains(PlainTime.COMPONENT) || entity.contains(PlainTime.ISO_HOUR) || entity.contains(PlainTime.DIGITAL_HOUR_OF_DAY) || entity.contains(PlainTime.CLOCK_HOUR_OF_DAY) ) { return entity; // optimization } DayPeriod dp = from(locale, attributes); Element approximate = new Element(false, dp); if (entity.contains(approximate)) { String codes = entity.get(approximate); int index = 0; int count = 0; Meridiem meridiem = null; do { int nextIndex = codes.indexOf('|', index); String code; if (nextIndex == -1) { code = codes.substring(index); } else { code = codes.substring(index, nextIndex); } index = nextIndex + 1; count++; if (dp.isPredefined() && (meridiem == null)) { if (code.equals("midnight")) { meridiem = Meridiem.AM; continue; } else if (code.equals("noon")) { meridiem = Meridiem.PM; continue; } } for (PlainTime time : dp.codeMap.keySet()) { if (dp.codeMap.get(time).equals(code)) { Meridiem m = null; int hour12 = getHour12(entity); PlainTime end = dp.getEnd(time); // Optimistic assumption that hour12 is always within time range described by code. // However, the strict parser will detect any inconsistencies with day periods later. if (time.getHour() >= 12) { if (end.isAfter(time) || end.isSimultaneous(PlainTime.midnightAtStartOfDay())) { m = Meridiem.PM; } else if (hour12 != -1) { m = ((hour12 + 12 >= time.getHour()) ? Meridiem.PM : Meridiem.AM); } } else if (!end.isAfter(PlainTime.of(12))) { m = Meridiem.AM; } else if (hour12 != -1) { m = ((hour12 >= time.getHour()) ? Meridiem.AM : Meridiem.PM); } if (m != null) { if ((meridiem != null) && (meridiem != m)) { // ambivalent day period if (hour12 == -1) { meridiem = null; // no clock hour available for distinction } else if (code.startsWith("night")) { // night1 or night2 (ja) meridiem = ((hour12 < 6) ? Meridiem.AM : Meridiem.PM); } else if (code.startsWith("afternoon")) { // languages id or uz meridiem = ((hour12 < 6) ? Meridiem.PM : Meridiem.AM); } else { // cannot resolve other day period code duplicate to am/pm meridiem = null; } } else { meridiem = m; } } } } } while (index > 0); if (meridiem != null) { entity = entity.with(PlainTime.AM_PM_OF_DAY, meridiem); if (count > 1) { entity = entity.with(approximate, null); } // else don't remove element value here in order to help the strict parser to detect errors later } } else { Element fixed = new Element(true, dp); if (entity.contains(fixed)) { String code = entity.get(fixed); if (code.equals("am") || code.equals("midnight")) { entity = entity.with(PlainTime.AM_PM_OF_DAY, Meridiem.AM); } else { entity = entity.with(PlainTime.AM_PM_OF_DAY, Meridiem.PM); } entity = entity.with(fixed, null); } } return entity; } private static int getHour12(ChronoEntity<?> entity) { int hour12 = -1; if (entity.contains(PlainTime.CLOCK_HOUR_OF_AMPM)) { hour12 = entity.get(PlainTime.CLOCK_HOUR_OF_AMPM).intValue(); if (hour12 == 12) { hour12 = 0; } } else if (entity.contains(PlainTime.DIGITAL_HOUR_OF_AMPM)) { hour12 = entity.get(PlainTime.DIGITAL_HOUR_OF_AMPM).intValue(); } return hour12; } private static DayPeriod from( Locale locale, AttributeQuery attributes ) { if (attributes.contains(CUSTOM)) { return attributes.get(CUSTOM); } return DayPeriod.of(locale, attributes.get(Attributes.CALENDAR_TYPE, CalendarText.ISO_CALENDAR_TYPE)); } } static class Element extends BasicElement<String> implements TextElement<String>, ElementRule<ChronoEntity<?>, String> { //~ Statische Felder/Initialisierungen ---------------------------- private static final long serialVersionUID = 5589976208326940032L; //~ Instanzvariablen ---------------------------------------------- private transient final boolean fixed; private transient final DayPeriod dayPeriod; //~ Konstruktoren ------------------------------------------------- Element( boolean fixed, Locale locale, String calendarType ) { this(fixed, DayPeriod.of(locale, calendarType)); } Element( boolean fixed, DayPeriod dayPeriod ) { super(fixed ? "FIXED_DAY_PERIOD" : "APPROXIMATE_DAY_PERIOD"); this.fixed = fixed; this.dayPeriod = dayPeriod; } //~ Methoden ------------------------------------------------------ @Override public Class<String> getType() { return String.class; } @Override public char getSymbol() { return (this.fixed ? 'b' : 'B'); } @Override public String getDefaultMinimum() { if (this.fixed) { return "am"; } PlainTime key = this.dayPeriod.codeMap.firstKey(); return this.dayPeriod.codeMap.get(key); } @Override public String getDefaultMaximum() { if (this.fixed) { return "pm"; } PlainTime key = this.dayPeriod.codeMap.lastKey(); return this.dayPeriod.codeMap.get(key); } @Override public boolean isDateElement() { return false; } @Override public boolean isTimeElement() { return true; } @Override public String toString() { StringBuilder sb = new StringBuilder(32); sb.append(this.name()); sb.append('@'); sb.append(this.dayPeriod); return sb.toString(); } @Override @SuppressWarnings("unchecked") protected <T extends ChronoEntity<T>> ElementRule<T, String> derive(Chronology<T> chronology) { if (chronology.isRegistered(PlainTime.COMPONENT)) { return (ElementRule<T, String>) this; } return null; } @Override protected boolean doEquals(BasicElement<?> obj) { Element that = (Element) obj; return this.dayPeriod.equals(that.dayPeriod); } @Override public String getValue(ChronoEntity<?> context) { // used in consistency-check after strict parsing PlainTime time = context.get(PlainTime.COMPONENT); if (this.fixed) { return getFixedCode(time); } else { if (this.dayPeriod.isPredefined()) { Map<String, String> textForms = loadTextForms(this.getLocale(), this.getCalendarType()); String code = null; if (time.isMidnight()) { code = "midnight"; } else if (time.isSimultaneous(PlainTime.of(12))) { code = "noon"; } if (code != null) { String key = createKey(textForms, TextWidth.ABBREVIATED, OutputContext.FORMAT, code); if (textForms.containsKey(key)) { return code; } } } PlainTime key = this.dayPeriod.getStart(time); return this.dayPeriod.codeMap.get(key); } } @Override public String getMinimum(ChronoEntity<?> context) { return this.getDefaultMinimum(); } @Override public String getMaximum(ChronoEntity<?> context) { return this.getDefaultMaximum(); } @Override public boolean isValid( ChronoEntity<?> context, String value ) { return false; } @Override public ChronoEntity<?> withValue( ChronoEntity<?> context, String value, boolean lenient ) { throw new IllegalArgumentException("Day period element cannot be set."); } @Override public ChronoElement<?> getChildAtFloor(ChronoEntity<?> context) { return null; } @Override public ChronoElement<?> getChildAtCeiling(ChronoEntity<?> context) { return null; } boolean isFixed() { return this.fixed; } Locale getLocale() { return this.dayPeriod.locale; } String getCalendarType() { return this.dayPeriod.calendarType; } Object getCodeMap() { return this.dayPeriod.codeMap; } private Object writeReplace() { return new SPX(this, SPX.DAY_PERIOD_TYPE); } private void readObject(ObjectInputStream in) throws IOException { throw new InvalidObjectException("Serialization proxy required."); } @Override public void print( ChronoDisplay context, Appendable buffer, AttributeQuery attributes ) throws IOException, ChronoException { TextWidth width = attributes.get(Attributes.TEXT_WIDTH, TextWidth.WIDE); OutputContext oc = attributes.get(Attributes.OUTPUT_CONTEXT, OutputContext.FORMAT); String s; if (this.fixed) { s = this.dayPeriod.fixed(width, oc).apply(context); } else { s = this.dayPeriod.approximate(width, oc).apply(context); } buffer.append(s); } @Override public String parse( CharSequence text, ParsePosition status, AttributeQuery attributes ) { int index = status.getIndex(); OutputContext oc = attributes.get(Attributes.OUTPUT_CONTEXT, OutputContext.FORMAT); String result = this.parse(text, status, attributes, oc); if ((result == null) && attributes.get(Attributes.PARSE_MULTIPLE_CONTEXT, Boolean.TRUE)) { status.setErrorIndex(-1); status.setIndex(index); oc = ((oc == OutputContext.FORMAT) ? OutputContext.STANDALONE : OutputContext.FORMAT); result = this.parse(text, status, attributes, oc); } return result; } private String parse( CharSequence text, ParsePosition status, AttributeQuery attributes, OutputContext oc ) { List<String> codes = new ArrayList<>(); Map<String, String> textForms = null; if (this.fixed) { codes.add("am"); codes.add("pm"); codes.add("midnight"); codes.add("noon"); } else { Set<String> set = new LinkedHashSet<>(this.dayPeriod.codeMap.values()); codes.addAll(set); // no duplicates if (this.dayPeriod.isPredefined()) { codes.add("midnight"); codes.add("noon"); } } if (this.dayPeriod.isPredefined()) { textForms = loadTextForms(this.getLocale(), this.getCalendarType()); } TextWidth tw = attributes.get(Attributes.TEXT_WIDTH, TextWidth.WIDE); boolean caseInsensitive = attributes.get(Attributes.PARSE_CASE_INSENSITIVE, Boolean.TRUE).booleanValue(); boolean partialCompare = attributes.get(Attributes.PARSE_PARTIAL_COMPARE, Boolean.FALSE).booleanValue(); String candidate = null; int start = status.getIndex(); int end = text.length(); int maxEq = 0; for (String code : codes) { String test = null; if (this.dayPeriod.isPredefined()) { String key; if (this.fixed) { key = createKey(textForms, tw, oc, code); if (!textForms.containsKey(key)) { // use fallback am/pm if (code.equals("midnight")) { key = createKey(textForms, tw, oc, "am"); } else if (code.equals("noon")) { key = createKey(textForms, tw, oc, "pm"); } } } else { key = createKey(textForms, tw, oc, code); } if (textForms.containsKey(key)) { test = textForms.get(key); } } else { test = code; } if (test != null) { int pos = start; int n = test.length(); boolean eq = true; for (int j = 0; eq && (j < n); j++) { if (start + j >= end) { eq = false; } else { char c = text.charAt(start + j); char t = test.charAt(j); if (caseInsensitive) { eq = this.compareIgnoreCase(c, t); } else { eq = (c == t); } if (eq) { pos++; } } } if (partialCompare || (n == 1)) { if (maxEq < pos - start) { maxEq = pos - start; candidate = code; } else if ((candidate != null) && (maxEq == pos - start)) { if (this.fixed) { candidate = null; } else { candidate = candidate + "|" + code; // handles NARROW, fa, hu } } } else if (eq) { maxEq = n; if (candidate == null) { candidate = code; } else if (this.fixed) { candidate = null; } else { candidate = candidate + "|" + code; // handles NARROW, fa, hu } } } } if (candidate == null) { status.setErrorIndex(start); } else { status.setIndex(start + maxEq); } return candidate; } private boolean compareIgnoreCase(char c1, char c2) { if (c1 >= 'a' && c1 <= 'z') { c1 = (char) (c1 - 'a' + 'A'); } if (c2 >= 'a' && c2 <= 'z') { c2 = (char) (c2 - 'a' + 'A'); } if (c1 >= 'A' && c1 <= 'Z') { return (c1 == c2); } Locale locale = this.getLocale(); String s1 = String.valueOf(c1).toUpperCase(locale); String s2 = String.valueOf(c2).toUpperCase(locale); return s1.equals(s2); } } private class PeriodName implements ChronoFunction<ChronoDisplay, String> { //~ Instanzvariablen ---------------------------------------------- private final boolean fixed; private final TextWidth width; private final OutputContext outputContext; //~ Konstruktoren ------------------------------------------------- PeriodName( boolean fixed, TextWidth width, OutputContext outputContext ) { super(); if (width == null) { throw new NullPointerException("Missing text width."); } else if (outputContext == null) { throw new NullPointerException("Missing output context."); } this.fixed = fixed; this.width = width; this.outputContext = outputContext; } //~ Methoden ------------------------------------------------------ @Override public String apply(ChronoDisplay context) { PlainTime time = context.get(PlainTime.COMPONENT); DayPeriod dp = DayPeriod.this; Locale locale = dp.locale; if (this.fixed) { String code = getFixedCode(time); if (dp.isPredefined()) { Map<String, String> textForms = loadTextForms(locale, dp.calendarType); String key = createKey(textForms, this.width, this.outputContext, code); if (!textForms.containsKey(key)) { if (code.equals("midnight")) { key = createKey(textForms, this.width, this.outputContext, "am"); } else if (code.equals("noon")) { key = createKey(textForms, this.width, this.outputContext, "pm"); } } if (textForms.containsKey(key)) { return textForms.get(key); } } else { return code; } } else { if (dp.isPredefined()) { Map<String, String> textForms = loadTextForms(locale, dp.calendarType); if (time.isMidnight()) { String key = createKey(textForms, this.width, this.outputContext, "midnight"); if (textForms.containsKey(key)) { return textForms.get(key); } } else if (time.isSimultaneous(PlainTime.of(12))) { String key = createKey(textForms, this.width, this.outputContext, "noon"); if (textForms.containsKey(key)) { return textForms.get(key); } } String code = dp.codeMap.get(dp.getStart(time)); String key = createKey(textForms, this.width, this.outputContext, code); if (textForms.containsKey(key)) { return textForms.get(key); } } else { return dp.codeMap.get(dp.getStart(time)); } } return time.get(PlainTime.AM_PM_OF_DAY).getDisplayName((locale == null) ? Locale.ROOT : locale); // fallback } } }