net.time4j.format.CalendarText Maven / Gradle / Ivy
/*
* -----------------------------------------------------------------------
* Copyright © 2013-2015 Meno Hochschild,
* -----------------------------------------------------------------------
* This file (CalendarText.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 .
* -----------------------------------------------------------------------
*/
package net.time4j.format;
import net.time4j.base.ResourceLoader;
import net.time4j.engine.CalendarEra;
import net.time4j.engine.ChronoElement;
import net.time4j.engine.Chronology;
import net.time4j.format.internal.ExtendedPatterns;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.chrono.IsoEra;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.IsoFields;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Source for localized calendrical informations on enum basis like month
* or weekday names.
*
* This class is a facade for an underlying implementation of
* {@link TextProvider} which will be loaded as SPI-interface
* by help of a {@code ServiceLoader}. If no such SPI-interface can be
* found then this class will resort to the sources of JDK (usually
* as wrapper around {@code java.text.DateFormatSymbols}).
*
* Furthermore, an instance of {@code CalendarText} can also access
* the UTF-8 text resources in the folder "calendar" relative to
* the class path which are not based on JDK-defaults. In this case the
* presence of the i18n-module is required. In all ISO-systems the
* "iso8601_{locale}.properties"-files will override the
* JDK-defaults unless it is the ROOT-locale. Example:
*
* If you wish to use the name "Sonnabend" instead of the standard
* word "Samstag" in german locale (english: Saturday) then you can
* copy the existing file "calendar/iso8601_de.properties" from the
* content of "time4j-i18n-v{version}.jar"-file into a new directory
* with the same path. Then you can insert these lines extra (all seven entries
* must be inserted, not just the sixth line):
*
*
* DAY_OF_WEEK(WIDE)_1=Montag
* DAY_OF_WEEK(WIDE)_2=Dienstag
* DAY_OF_WEEK(WIDE)_3=Mittwoch
* DAY_OF_WEEK(WIDE)_4=Donnerstag
* DAY_OF_WEEK(WIDE)_5=Freitag
* DAY_OF_WEEK(WIDE)_6=Sonnabend
* DAY_OF_WEEK(WIDE)_7=Sonntag
*
*
* The general format of these lines is:
*
*
* {element-name}({text-width}[|STANDALONE])_{one-based-integer}={text}
*
*
* STANDALONE is optional. As element name in the context of ISO-8601
* following names are supported:
*
* - MONTH_OF_YEAR
* - QUARTER_OF_YEAR
* - DAY_OF_WEEK
* - ERA
* - AM_PM_OF_DAY
*
* @author Meno Hochschild
* @doctags.concurrency {immutable}
*/
/*[deutsch]
* Quelle für lokalisierte kalendarische Informationen auf Enum-Basis
* wie zum Beispiel Monats- oder Wochentagsnamen.
*
* Diese Klasse ist eine Fassade für eine dahinterstehende
* {@link TextProvider}-Implementierung, die als SPI-Interface
* über einen {@code ServiceLoader}-Mechanismus geladen wird. Gibt es
* keine solche Implementierung, wird intern auf die Quellen des JDK mittels
* der Schnittstelle {@code java.text.DateFormatSymbols} ausgewichen.
*
* Darüberhinaus kann eine Instanz von {@code CalendarText} auch
* auf UTF-8-Textressourcen im Verzeichnis "calendar" innerhalb des
* Klassenpfads zugreifen, die nicht auf JDK-Vorgaben beruhen. In diesem Fall
* ist das i18n-Modul notwendig. Für alle ISO-Systeme gilt, daß die
* Einträge in den Dateien "iso8601_{locale}.properties" die
* JDK-Vorgaben überschreiben, sofern es nicht die ROOT-locale ist.
* Beispiel:
*
* Wenn der Name "Sonnabend" anstatt der Standardvorgabe
* "Samstag" in der deutschen Variante verwendet werden soll,
* dann kann die existierende Datei "calendar/iso8601_de.properties"
* vom Inhalt der Bibliotheksdatei "time4j-i18n-v{version].jar"
* in ein neues Verzeichnis mit dem gleichen Pfad kopiert werden. Danach
* können alle folgenden Zeilen extra eingefügt werden (nicht nur
* die sechste Zeile allein):
*
*
* DAY_OF_WEEK(WIDE)_1=Montag
* DAY_OF_WEEK(WIDE)_2=Dienstag
* DAY_OF_WEEK(WIDE)_3=Mittwoch
* DAY_OF_WEEK(WIDE)_4=Donnerstag
* DAY_OF_WEEK(WIDE)_5=Freitag
* DAY_OF_WEEK(WIDE)_6=Sonnabend
* DAY_OF_WEEK(WIDE)_7=Sonntag
*
*
* Das allgemeine Format dieser Zeilen ist:
*
*
* {element-name}({text-width}[|STANDALONE])_{eins-basierter-integer}={text}
*
*
* STANDALONE ist optional. Als Elementname im Kontext von ISO-8601 werden
* folgende Namen unterstützt:
*
* - MONTH_OF_YEAR
* - QUARTER_OF_YEAR
* - DAY_OF_WEEK
* - ERA
* - AM_PM_OF_DAY
*
* @author Meno Hochschild
* @doctags.concurrency {immutable}
*/
public final class CalendarText {
//~ Statische Felder/Initialisierungen --------------------------------
private static final Set RTL;
static {
Set lang = new HashSet<>();
lang.add("ar");
lang.add("dv");
lang.add("fa");
lang.add("ha");
lang.add("he");
lang.add("iw");
lang.add("ji");
lang.add("ps");
lang.add("ur");
lang.add("yi");
RTL = Collections.unmodifiableSet(lang);
}
private static final FormatPatternProvider FORMAT_PATTERN_PROVIDER;
static {
FormatPatternProvider provider = new FormatPatterns(null);
for (FormatPatternProvider fpp : ResourceLoader.getInstance().services(FormatPatternProvider.class)) {
provider = new FormatPatterns(fpp);
if (!fpp.getClass().getName().startsWith("net.time4j.")) {
break;
}
}
FORMAT_PATTERN_PROVIDER = provider;
}
/**
* Default calendar type for all ISO systems.
*/
/*[deutsch]
* Standard-Kalendertyp für alle ISO-Systeme.
*/
public static final String ISO_CALENDAR_TYPE = "iso8601";
private static final TextProvider JDK_PROVIDER = new JDKTextProvider();
private static final TextProvider ROOT_PROVIDER = new FallbackProvider();
private static final ConcurrentMap CACHE = new ConcurrentHashMap<>();
//~ Instanzvariablen --------------------------------------------------
// Name des Provider
private final String provider;
// Standardtexte
private final Map> stdMonths;
private final Map> leapMonths;
private final Map> quarters;
private final Map> weekdays;
private final Map eras;
private final Map meridiems;
// Allgemeine Textformen spezifisch für eine Chronologie
private final Map textForms;
private final Locale locale;
private final MissingResourceException mre;
//~ Konstruktoren -----------------------------------------------------
private CalendarText(
String calendarType,
Locale locale,
TextProvider p
) {
super();
this.provider = p.toString();
// Monate, Quartale, Wochentage, Äras und AM/PM
this.stdMonths =
Collections.unmodifiableMap(
getMonths(calendarType, locale, p, false));
Map> tmpLeapMonths =
getMonths(calendarType, locale, p, true);
if (tmpLeapMonths == null) {
this.leapMonths = this.stdMonths;
} else {
this.leapMonths = Collections.unmodifiableMap(tmpLeapMonths);
}
Map> qt =
new EnumMap<>(TextWidth.class);
for (TextWidth tw : TextWidth.values()) {
Map qo =
new EnumMap<>(OutputContext.class);
for (OutputContext oc : OutputContext.values()) {
qo.put(
oc,
new TextAccessor(
p.quarters(calendarType, locale, tw, oc),
locale));
}
qt.put(tw, qo);
}
this.quarters = Collections.unmodifiableMap(qt);
Map> wt =
new EnumMap<>(TextWidth.class);
for (TextWidth tw : TextWidth.values()) {
Map wo =
new EnumMap<>(OutputContext.class);
for (OutputContext oc : OutputContext.values()) {
wo.put(
oc,
new TextAccessor(
p.weekdays(calendarType, locale, tw, oc),
locale));
}
wt.put(tw, wo);
}
this.weekdays = Collections.unmodifiableMap(wt);
Map et =
new EnumMap<>(TextWidth.class);
for (TextWidth tw : TextWidth.values()) {
et.put(
tw,
new TextAccessor(p.eras(calendarType, locale, tw), locale));
}
this.eras = Collections.unmodifiableMap(et);
Map mt =
new EnumMap<>(TextWidth.class);
for (TextWidth tw : TextWidth.values()) {
mt.put(
tw,
new TextAccessor(
p.meridiems(calendarType, locale, tw),
locale));
}
this.meridiems = Collections.unmodifiableMap(mt);
// Allgemeine Textformen als optionales Bundle vorbereiten
// Wichtig: Letzter Schritt im Konstruktor wg. Bundle-Cache
Map map = new HashMap<>();
MissingResourceException tmpMre = null;
try {
ResourceBundle rb =
ResourceBundle.getBundle(
"calendar/" + calendarType,
locale,
p.getControl());
for (String key : rb.keySet()) {
map.put(key, rb.getString(key));
}
} catch (MissingResourceException ex) {
tmpMre = ex;
}
this.textForms = Collections.unmodifiableMap(map);
this.locale = locale;
this.mre = tmpMre;
}
//~ Methoden ----------------------------------------------------------
/**
* Returns an instance of {@code CalendarText} for ISO calendar systems and given language.
*
* @param locale language
* @return {@code CalendarText} object maybe cached
* @since 3.13/4.10
*/
/*[deutsch]
* Gibt eine Instanz dieser Klasse für ISO-Kalendersysteme und die angegebene
* Sprache zurück.
*
* @param locale language
* @return {@code CalendarText} object maybe cached
* @since 3.13/4.10
*/
public static CalendarText getIsoInstance(Locale locale) {
return getInstance(CalendarText.ISO_CALENDAR_TYPE, locale);
}
/**
* Returns an instance of {@code CalendarText} for given chronology
* and language.
*
* @param chronology chronology (with calendar system)
* @param locale language
* @return {@code CalendarText} object maybe cached
*/
/*[deutsch]
* Gibt eine Instanz dieser Klasse für die angegebene Chronologie
* und Sprache zurück.
*
* @param chronology chronology (with calendar system)
* @param locale language
* @return {@code CalendarText} object maybe cached
*/
public static CalendarText getInstance(
Chronology> chronology,
Locale locale
) {
return getInstance(extractCalendarType(chronology), locale);
}
/**
* Returns an instance of {@code CalendarText} for given calendar type
* and language.
*
* @param calendarType name of calendar system
* @param locale language
* @return {@code CalendarText} object maybe cached
* @see CalendarType
*/
/*[deutsch]
* Gibt eine Instanz dieser Klasse für Kalendertyp
* und Sprache zurück.
*
* @param calendarType name of calendar system
* @param locale language
* @return {@code CalendarText} object maybe cached
* @see CalendarType
*/
public static CalendarText getInstance(
String calendarType,
Locale locale
) {
if (calendarType == null) {
throw new NullPointerException("Missing calendar type.");
}
StringBuilder sb = new StringBuilder();
sb.append(calendarType);
sb.append(':');
sb.append(locale.getLanguage());
String country = locale.getCountry();
if (!country.isEmpty()) {
sb.append('-');
sb.append(country);
}
String key = sb.toString();
CalendarText instance = CACHE.get(key);
if (instance == null) {
TextProvider p = null;
if (locale.getLanguage().isEmpty() && calendarType.equals(ISO_CALENDAR_TYPE)) {
p = ROOT_PROVIDER;
} else {
// ServiceLoader-Mechanismus (Suche nach externen Providern)
for (TextProvider tmp : ResourceLoader.getInstance().services(TextProvider.class)) {
if (
isCalendarTypeSupported(tmp, calendarType)
&& isLocaleSupported(tmp, locale)
) {
p = tmp;
break;
}
}
// Java-Ressourcen
if (p == null) {
TextProvider tmp = JDK_PROVIDER;
if (
isCalendarTypeSupported(tmp, calendarType)
&& isLocaleSupported(tmp, locale)
) {
p = tmp;
}
if (p == null) {
p = ROOT_PROVIDER; // keine-ISO-Ressource
}
}
}
instance = new CalendarText(calendarType, locale, p);
CalendarText old = CACHE.putIfAbsent(key, instance);
if (old != null) {
instance = old;
}
}
return instance;
}
/**
* Yields an {@code Accessor} for all standard months.
*
* The underlying list is sorted such that it will obey to the
* typical order of months in given calendar system. ISO-systems
* define January as first month and at whole 12 months. Other
* calendar systems can also define for example 13 months. The order
* of element value enums must be in agreement with the order of
* the text forms contained here.
*
* The default implementation handles SHORT as synonym for
* ABBREVIATED in the context of ISO-8601.
*
* @param textWidth text width of displayed month name
* @param outputContext output context (stand-alone?)
* @return accessor for standard month names
* @see net.time4j.Month
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle
* Standard-Monatsnamen.
*
* Die Liste ist so sortiert, daß die für das jeweilige
* Kalendersystem typische Reihenfolge der Monate eingehalten wird.
* ISO-Systeme definieren den Januar als den ersten Monat und insgesamt
* 12 Monate. Andere Kalendersysteme können auch 13 Monate definieren.
* Die Reihenfolge der Elementwert-Enums muß mit der Reihenfolge der
* hier enthaltenen Textformen übereinstimmen.
*
* Speziell für ISO-8601 behandelt die Standardimplementierung
* die Textbreiten SHORT und ABBREVIATED gleich.
*
* @param textWidth text width of displayed month name
* @param outputContext output context (stand-alone?)
* @return accessor for standard month names
* @see net.time4j.Month
*/
public TextAccessor getStdMonths(
TextWidth textWidth,
OutputContext outputContext
) {
return this.getMonths(textWidth, outputContext, false);
}
/**
* Yields an {@code Accessor} for all months if a leap month
* is relevant.
*
* Note: Leap months are defined in some calendar systems like the
* hebrew calendar ("Adar II") else there is no difference
* between standard and leap months escpecially not in ISO-8601.
*
* @param textWidth text width of displayed month name
* @param outputContext output context (stand-alone?)
* @return accessor for month names
* @see net.time4j.Month
* @see #getStdMonths(TextWidth, OutputContext)
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle
* Monatsnamen, wenn ein Schaltmonat relevant ist.
*
* Hinweis: Schaltmonate sind in einigen Kalendersystemen wie dem
* hebräischen Kalender definiert ("Adar II"). Ansonsten
* gibt es keinen Unterschied zwischen Standard- und Schaltmonaten,
* insbesondere nicht im ISO-8601-Standard.
*
* @param textWidth text width of displayed month name
* @param outputContext output context (stand-alone?)
* @return accessor for month names
* @see net.time4j.Month
* @see #getStdMonths(TextWidth, OutputContext)
*/
public TextAccessor getLeapMonths(
TextWidth textWidth,
OutputContext outputContext
) {
return this.getMonths(textWidth, outputContext, true);
}
/**
* Yields an {@code Accessor} for all quarter years.
*
* The underlying list of text forms is sorted in the same order
* as the enum {@code Quarter} and uses its ordinal index as list
* index. ISO systems define the range January-March as first quarter
* etc. and at whole four quarters per calendar year.
*
* The default implementation handles SHORT as synonym for
* ABBREVIATED in the context of ISO-8601.
*
* @param textWidth text width of displayed quarter name
* @param outputContext output context (stand-alone?)
* @return accessor for quarter names
* @see net.time4j.Quarter
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle
* Quartalsnamen.
*
* Die Liste ist wie das Enum {@code Quarter} sortiert und verwendet
* dessen Ordinalindex als Listenindex. ISO-Systeme definieren den
* Zeitraum Januar-März als erstes Quartal usw. und insgesamt
* 4 Quartale pro Kalenderjahr.
*
* Speziell für ISO-8601 behandelt die Standardimplementierung
* die Textbreiten SHORT und ABBREVIATED gleich.
*
* @param textWidth text width of displayed quarter name
* @param outputContext output context (stand-alone?)
* @return accessor for quarter names
* @see net.time4j.Quarter
*/
public TextAccessor getQuarters(
TextWidth textWidth,
OutputContext outputContext
) {
return this.quarters.get(textWidth).get(outputContext);
}
/**
* Yields an {@code Accessor} for all weekday names.
*
* The underlying list of text forms is sorted such that the
* typical order of weekdays is used in given calendar system.
* ISO systems define Monday as first day of week and at whole
* 7 weekdays. This order is also valid for US in the context of
* this class although in US Sunday is considered as start of a
* week. The order element value enums must be in agreement with
* the order of text forms contained here.
*
* @param textWidth text width of displayed weekday name
* @param outputContext output context (stand-alone?)
* @return accessor for weekday names
* @see net.time4j.Weekday
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle
* Wochentagsnamen.
*
* Die Liste ist so sortiert, daß die für das jeweilige
* Kalendersystem typische Reihenfolge der Wochentage eingehalten wird.
* ISO-Systeme definieren den Montag als den ersten Wochentag und insgesamt
* 7 Wochentage. Diese Sortierung gilt im Kontext dieser Klasse auch
* für die USA, in denen der Sonntag als erster Tag der Woche gilt.
* Die Reihenfolge der Elementwert-Enums muß mit der Reihenfolge
* der hier enthaltenen Textformen übereinstimmen.
*
* @param textWidth text width of displayed weekday name
* @param outputContext output context (stand-alone?)
* @return accessor for weekday names
* @see net.time4j.Weekday
*/
public TextAccessor getWeekdays(
TextWidth textWidth,
OutputContext outputContext
) {
return this.weekdays.get(textWidth).get(outputContext);
}
/**
* Yields an {@code Accessor} for all era names.
*
* The underlying list of text forms is sorted such that the
* typical order of eras is used in given calendar system. ISO systems
* define era names based on their historical extensions (eras of
* gregorian/historic calendar) because they themselves have no internal
* concept of eras. The order of element value enums must be in agreement
* with the text forms contained here. If an era is not defined on enum
* basis then the format API will not evaluate this class but the
* {@code CalendarSystem} to get the right text forms.
*
* @param textWidth text width of displayed era name
* @return accessor for era names
* @see net.time4j.engine.CalendarSystem#getEras()
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle
* Äranamen.
*
* Die Liste ist so sortiert, daß die für das jeweilige
* Kalendersystem typische Reihenfolge der Äranamen eingehalten wird.
* ISO-Systeme definieren Äranamen basierend auf ihren historischen
* Erweiterungen, da sie selbst keine kennen (also die des gregorianischen
* historischen Kalenders). Die Reihenfolge der Elementwert-Enums muß
* mit der Reihenfolge der hier enthaltenen Textformen übereinstimmen.
* Wenn eine Ära nicht auf Enum-Basis definiert ist, wertet das
* Format-API nicht diese Klasse, sondern das {@code CalendarSystem} zur
* Bestimmung der Textformen aus.
*
* @param textWidth text width of displayed era name
* @return accessor for era names
* @see net.time4j.engine.CalendarSystem#getEras()
*/
public TextAccessor getEras(TextWidth textWidth) {
return this.eras.get(textWidth);
}
/**
* Yields an {@code Accessor} for all am/pm-names.
*
* The underlying list of text forms is sorted in AM-PM-order.
* The order of element value enums must be the same.
*
* @param textWidth text width of displayed AM/PM name
* @return accessor for AM/PM names
* @see net.time4j.Meridiem
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle
* Tagesabschnittsnamen.
*
* Die Liste ist in AM/PM-Reihenfolge sortiert. Die Reihenfolge der
* Elementwert-Enums muß mit der Reihenfolge der hier enthaltenen
* Textformen übereinstimmen.
*
* @param textWidth text width of displayed AM/PM name
* @return accessor for AM/PM names
* @see net.time4j.Meridiem
*/
public TextAccessor getMeridiems(TextWidth textWidth) {
return this.meridiems.get(textWidth);
}
/**
* Yields all text forms in raw format.
*
* @return unmodifiable map of all text forms
* @since 3.12/4.9
*/
/*[deutsch]
* Liefert alle Textformen im Rohformat.
*
* @return unmodifiable map of all text forms
* @since 3.12/4.9
*/
public Map getTextForms() {
return this.textForms;
}
/**
* Yields an {@code Accessor} for all text forms of given
* chronological element.
*
* Text forms might exist in different variations. In case of
* enum-based variants the name of the enum (example "WIDE" in
* the variant {@code TextWidth}) is to be used, in case of boolean-based
* variants the literals "true" and "false" are to be
* used.
*
* While the methods {@code getStdMonths()}, {@code getWeekdays()}
* etc. are mainly based on JDK-defaults, this method is escpecially
* designed for querying chronological texts which are not contained in
* JDK. Text forms will be stored internally in the resource folder
* "calendar" relative to class path in properties-files using
* UTF-8 encoding. The basic name of these resources is the calendar type.
* The combination of element name and optionally variants in the form
* "(variant1|variant2|...|variantN)" and the underscore and
* finally a numerical suffix with base 1 (for era elements base 0) serves
* as resource text key. If there is no entry for given key in the resources
* then this method will simply yield the name of enum value associated
* with given element value.
*
* As example, the search for abbreviated historic era {@code HistoricEra.AD} of alternative form
* looks up keys in this order (using E if there is an entry "useShortKeys=true"):
*
*
* - value of "E(a|alt)_1"
* - value of "E(a)_1"
* - value of "E_1"
* - fallback=>AD
*
*
* @param generic type of element values based on enums
* @param element element text forms are searched for
* @param variants text form variants (optional)
* @return accessor for any text forms
* @throws MissingResourceException if for given calendar type there are no text resource files
*/
/*[deutsch]
* Liefert einen {@code Accessor} für alle Textformen des
* angegebenen chronologischen Elements.
*
* Textformen können unter Umständen in verschiedenen
* Varianten vorkommen. Als Variantenbezug dient bei enum-Varianten
* der Name der Enum-Ausprägung (Beispiel "WIDE" in
* der Variante {@code TextWidth}), im boolean-Fall sind die Literale
* "true" und "false" zu verwenden.
*
* Während {@code getStdMonths()}, {@code getWeekdays()} etc. in
* erster Linie auf JDK-Vorgaben beruhen, dient diese Methode dazu,
* chronologiespezifische Texte zu beschaffen, die nicht im JDK enthalten
* sind. Textformen werden intern im Verzeichnis "calendar"
* des Klassenpfads mit Hilfe von properties-Dateien im UTF-8-Format
* gespeichert. Der Basisname dieser Ressourcen ist der Kalendertyp. Als
* Textschluuml;ssel dient die Kombination aus Elementname, optional
* Varianten in der Form "(variant1|variant2|...|variantN)",
* dem Unterstrich und schließlich einem numerischen Suffix mit
* Basis 1 (für Ära-Elemente Basis 0). Wird in den Ressourcen zum
* angegebenen Schlüssel kein Eintrag gefunden, liefert diese Methode
* einfach den Namen des mit dem Element assoziierten enum-Werts.
*
* Zum Beispiel versucht die Suche nach der abgekürzten Form der historischen Ära
* {@code HistoricEra.AD} in der alternativen Form Schlüssel in dieser Reihenfolge zu finden
* (mit dem Präfix E, falls es einen Eintrag "useShortKeys=true" gibt):
*
*
* - value of "E(a|alt)_1"
* - value of "E(a)_1"
* - value of "E_1"
* - fallback=>AD
*
*
* @param generic type of element values based on enums
* @param element element text forms are searched for
* @param variants text form variants (optional)
* @return accessor for any text forms
* @throws MissingResourceException if for given calendar type there are no text resource files
*/
public > TextAccessor getTextForms(
ChronoElement element,
String... variants
) {
return this.getTextForms(element.name(), element.getType(), variants);
}
/**
* See {@link #getTextForms(ChronoElement, String...)}.
*
* @param generic type of element values based on enums
* @param name name of text entries in resource file
* @param type type of enum values
* @param variants text form variants (optional)
* @return accessor for any text forms
* @throws MissingResourceException if for given calendar type there are no text resource files
* @since 3.11/4.8
*/
/*[deutsch]
* Siehe {@link #getTextForms(ChronoElement, String...)}.
*
* @param generic type of element values based on enums
* @param name name of text entries in resource file
* @param type type of enum values
* @param variants text form variants (optional)
* @return accessor for any text forms
* @throws MissingResourceException if for given calendar type there are no text resource files
* @since 3.11/4.8
*/
public > TextAccessor getTextForms(
String name,
Class type,
String... variants
) {
if (this.mre != null) {
throw new MissingResourceException(
this.mre.getMessage(),
this.mre.getClassName(),
this.mre.getKey());
}
V[] enums = type.getEnumConstants();
int len = enums.length;
String[] tfs = new String[len];
String prefix = this.getKeyPrefix(name);
int baseIndex = (CalendarEra.class.isAssignableFrom(type) ? 0 : 1);
for (int i = 0; i < len; i++) {
int step = 0;
String raw = getKeyStart(prefix, 0, variants);
String key = null;
// sukzessives Reduzieren der Varianten, wenn nicht gefunden
while (raw != null) {
String test = toKey(raw, i, baseIndex);
if (this.textForms.containsKey(test)) {
key = test;
break;
}
step++;
raw = getKeyStart(prefix, step, variants);
}
if (key == null) {
tfs[i] = enums[i].name(); // fallback
} else {
tfs[i] = this.textForms.get(key);
}
}
return new TextAccessor(tfs, this.locale);
}
/**
* Yields the localized GMT-prefix which is used in the
* localized GMT format of CLDR.
*
* Note that using the GMT-notation is at least old-fashioned.
* Users should prefer the UTC-notation as combination of the
* "UTC"-literal and an ISO-8601-timezone-offset. This
* method is mainly a fit to the CLDR data.
*
* @param locale language and country configuration
* @return localized GMT-String defaults to "GMT"
* @deprecated Please avoid use of GMT-notation else you can use the expression
* {@code CalendarText.getIsoInstance(locale).getTextForms().get("prefixGMTOffset")}
*/
@Deprecated
public static String getGMTPrefix(Locale locale) {
CalendarText ct = CalendarText.getIsoInstance(locale);
if (ct.textForms.isEmpty()) {
return "GMT";
}
return ct.textForms.get("prefixGMTOffset");
}
/**
* Yields the best available format patterns.
*
* @return format pattern provider
* @since 3.10/4.7
* @deprecated Use one of methods {@code patternForXYZ} instead
*/
/*[deutsch]
* Liefert die am besten verfügbaren Formatmuster.
*
* @return format pattern provider
* @since 3.10/4.7
* @deprecated Use one of methods {@code patternForXYZ} instead
*/
@Deprecated
public static FormatPatternProvider getFormatPatterns() {
return FORMAT_PATTERN_PROVIDER;
}
/**
* Yields a format pattern without any timezone symbols for plain timestamps.
*
* @param dateMode display mode of date part
* @param timeMode display mode of time part
* @param locale language and country setting
* @return format pattern for plain timestamps without timezone symbols
* @since 3.10/4.7
* @deprecated Use {@code patternForTimestamp} instead
*/
/*[deutsch]
* Liefert ein Formatmuster ohne Zeitzonensymbole für reine Zeitstempel.
*
* @param dateMode display mode of date part
* @param timeMode display mode of time part
* @param locale language and country setting
* @return format pattern for plain timestamps without timezone symbols
* @since 3.10/4.7
* @deprecated Use {@code patternForTimestamp} instead
*/
@Deprecated
public static String getTimestampPattern(
DisplayMode dateMode,
DisplayMode timeMode,
Locale locale
) {
String pattern = FORMAT_PATTERN_PROVIDER.getDateTimePattern(dateMode, timeMode, locale);
return removeZones(pattern);
}
/**
* Returns the localized date pattern suitable for formatting of objects
* of type {@code PlainDate}.
*
* @param mode display mode
* @param locale language and country setting
* @return localized date pattern
* @see net.time4j.PlainDate
* @since 3.13/4.10
*/
/*[deutsch]
* Liefert das lokalisierte Datumsmuster geeignet für
* die Formatierung von Instanzen des Typs{@code PlainDate}.
*
* @param mode display mode
* @param locale language and country setting
* @return localized date pattern
* @see net.time4j.PlainDate
* @since 3.13/4.10
*/
public static String patternForDate(
DisplayMode mode,
Locale locale
) {
return FORMAT_PATTERN_PROVIDER.getDatePattern(mode, locale);
}
/**
* Returns the localized time pattern suitable for formatting of objects
* of type {@code PlainTime}.
*
* @param mode display mode
* @param locale language and country setting
* @return localized time pattern
* @see net.time4j.PlainTime
* @since 3.13/4.10
*/
/*[deutsch]
* Liefert das lokalisierte Uhrzeitmuster geeignet für die
* Formatierung von Instanzen des Typs {@code PlainTime}.
*
* @param mode display mode
* @param locale language and country setting
* @return localized time pattern
* @see net.time4j.PlainTime
* @since 3.13/4.10
*/
public static String patternForTime(
DisplayMode mode,
Locale locale
) {
return FORMAT_PATTERN_PROVIDER.getTimePattern(mode, locale);
}
/**
* Yields a format pattern without any timezone symbols for plain timestamps.
*
* @param dateMode display mode of date part
* @param timeMode display mode of time part
* @param locale language and country setting
* @return format pattern for plain timestamps without timezone symbols
* @see net.time4j.PlainTimestamp
* @since 3.13/4.10
*/
/*[deutsch]
* Liefert ein Formatmuster ohne Zeitzonensymbole für reine Zeitstempel.
*
* @param dateMode display mode of date part
* @param timeMode display mode of time part
* @param locale language and country setting
* @return format pattern for plain timestamps without timezone symbols
* @see net.time4j.PlainTimestamp
* @since 3.13/4.10
*/
public static String patternForTimestamp(
DisplayMode dateMode,
DisplayMode timeMode,
Locale locale
) {
String pattern = FORMAT_PATTERN_PROVIDER.getDateTimePattern(dateMode, timeMode, locale);
return removeZones(pattern);
}
/**
* Returns the localized date-time pattern suitable for formatting of objects
* of type {@code Moment}.
*
* @param dateMode display mode of date part
* @param timeMode display mode of time part
* @param locale language and country setting
* @return localized date-time pattern including timezone symbols
* @see net.time4j.Moment
* @since 3.13/4.10
*/
/*[deutsch]
* Liefert das lokalisierte Datums- und Uhrzeitmuster geeignet
* für die Formatierung von Instanzen des Typs {@code Moment}.
*
* @param dateMode display mode of date part
* @param timeMode display mode of time part
* @param locale language and country setting
* @return localized date-time pattern including timezone symbols
* @see net.time4j.Moment
* @since 3.13/4.10
*/
public static String patternForMoment(
DisplayMode dateMode,
DisplayMode timeMode,
Locale locale
) {
return FORMAT_PATTERN_PROVIDER.getDateTimePattern(dateMode, timeMode, locale);
}
/**
* Returns the localized interval pattern.
*
* Expressions of the form "{0}" will be interpreted as the start boundary format
* and expressions of the form "{1}" will be interpreted as the end boundary format.
* All other chars of the pattern will be treated as literals.
*
* @param locale language and country setting
* @return localized interval pattern
* @since 3.13/4.10
*/
/*[deutsch]
* Liefert das lokalisierte Intervallmuster.
*
* Die Ausdrücke "{0}" und "{1}" werden als Formathalter für die
* Start- und End-Intervallgrenzen interpretiert. Alle anderen Zeichen des Musters werden wie
* Literale behandelt.
*
* @param locale language and country setting
* @return localized interval pattern
* @since 3.13/4.10
*/
public static String patternForInterval(Locale locale) {
return FORMAT_PATTERN_PROVIDER.getIntervalPattern(locale);
}
/**
* Yields the name of the internal {@link TextProvider}.
*/
/*[deutsch]
* Liefert den Namen des internen {@link TextProvider}.
*/
@Override
public String toString() {
return this.provider;
}
/**
* Clears the internal cache.
*
* This method should be called if the internal text resources have
* changed and must be reloaded with a suitable {@code ClassLoader}.
*/
/*[deutsch]
* Löscht den internen Cache.
*
* Diese Methode sollte aufgerufen werden, wenn sich die internen
* Text-Ressourcen geändert haben und mit einem geeigneten
* {@code ClassLoader} neu geladen werden müssen.
*/
public static void clearCache() {
CACHE.clear();
}
/**
* Extrahiert den Kalendertyp aus der angegebenen Chronologie.
*
* Kann kein Kalendertyp ermittelt werden, wird {@code ISO_CALENDAR_TYPE}
* als Ausweichoption zurückgegeben.
*
* @param chronology chronology to be evaluated
* @return calendar type, never {@code null}
*/
static String extractCalendarType(Chronology> chronology) {
CalendarType ft =
chronology.getChronoType().getAnnotation(CalendarType.class);
return ((ft == null) ? ISO_CALENDAR_TYPE : ft.value());
}
private TextAccessor getMonths(
TextWidth textWidth,
OutputContext outputContext,
boolean leapForm
) {
if (leapForm) {
return this.leapMonths.get(textWidth).get(outputContext);
} else {
return this.stdMonths.get(textWidth).get(outputContext);
}
}
private static Map> getMonths(
String calendarType,
Locale locale,
TextProvider p,
boolean leapForm
) {
Map> mt = new EnumMap<>(TextWidth.class);
boolean usesDifferentLeapForm = false;
for (TextWidth tw : TextWidth.values()) {
Map mo =
new EnumMap<>(OutputContext.class);
for (OutputContext oc : OutputContext.values()) {
String[] ls =
p.months(calendarType, locale, tw, oc, leapForm);
if (leapForm && !usesDifferentLeapForm) {
String[] std =
p.months(calendarType, locale, tw, oc, false);
usesDifferentLeapForm = !Arrays.equals(std, ls);
}
mo.put(oc, new TextAccessor(ls, locale));
}
mt.put(tw, mo);
}
return ((!leapForm || usesDifferentLeapForm) ? mt : null);
}
private static boolean isCalendarTypeSupported(
TextProvider p,
String calendarType
) {
for (String c : p.getSupportedCalendarTypes()) {
if (c.equals(calendarType)) {
return true;
}
}
return false;
}
private static boolean isLocaleSupported(
TextProvider p,
Locale locale
) {
String lang = locale.getLanguage();
for (Locale l : p.getAvailableLocales()) {
if (lang.equals(l.getLanguage())) {
return true;
}
}
return false;
}
private String getKeyPrefix(String elementName) {
if (
this.textForms.containsKey("useShortKeys")
&& "true".equals(this.textForms.get("useShortKeys"))
) {
if (
(elementName.equals("MONTH_OF_YEAR") || elementName.equals("DAY_OF_WEEK")
|| elementName.equals("QUARTER_OF_YEAR") || elementName.equals("ERA"))
) {
return elementName.substring(0, 1);
} else if (elementName.equals("EVANGELIST")) { // special case
return "EV";
}
}
return elementName;
}
private static String getKeyStart(
String elementName,
int step,
String... variants
) {
if ((variants != null) && (variants.length > 0)) {
if (variants.length < step) {
return null;
}
StringBuilder sb = new StringBuilder(elementName);
boolean first = true;
for (int v = 0; v < variants.length - step; v++) {
if (first) {
sb.append('(');
first = false;
} else {
sb.append('|');
}
sb.append(variants[v]);
}
if (!first) {
sb.append(')');
}
return sb.toString();
} else {
return (step > 0 ? null : elementName);
}
}
private static String toKey(
String raw,
int counter,
int baseIndex
) {
StringBuilder keyBuilder = new StringBuilder(raw);
keyBuilder.append('_');
keyBuilder.append(counter + baseIndex);
return keyBuilder.toString();
}
// strip off any timezone symbols in clock time patterns,
// used by wrappers of FormatPatternProvider-objects
private static String removeZones(String pattern) {
boolean literal = false;
StringBuilder sb = new StringBuilder();
for (int i = 0, n = pattern.length(); i < n; i++) {
char c = pattern.charAt(i);
if (c == '\'') {
if (i + 1 < n && pattern.charAt(i + 1) == '\'') {
sb.append(c);
i++;
} else {
literal = !literal;
}
sb.append(c);
} else if (literal) {
sb.append(c);
} else if (c != 'z' && c != 'Z' && c != 'v' && c != 'V' && c != 'x' && c != 'X') {
sb.append(c);
}
}
for (int j = 0; j < sb.length(); j++) {
char c = sb.charAt(j);
if (c == ' ' && j + 1 < sb.length() && sb.charAt(j + 1) == ' ') {
sb.deleteCharAt(j);
j--;
} else if (c == '[' || c == ']' || c == '(' || c == ')') { // check locales es, fa, ps, uz
sb.deleteCharAt(j);
j--;
}
}
String result = sb.toString().trim();
if (result.endsWith(" '")) { // special case for de, fr_BE
result = result.substring(0, result.length() - 2) + "'";
} else if (result.endsWith(",")) { // special case for hy
result = result.substring(0, result.length() - 1);
}
return result;
}
//~ Innere Klassen ----------------------------------------------------
private static class JDKTextProvider
implements TextProvider {
//~ Methoden ------------------------------------------------------
@Override
public String[] getSupportedCalendarTypes() {
return new String[] { ISO_CALENDAR_TYPE };
}
@Override
public Locale[] getAvailableLocales() {
return DateFormatSymbols.getAvailableLocales();
}
@Override
public String[] months(
String calendarType,
Locale locale,
TextWidth tw,
OutputContext oc,
boolean leapForm
) {
TextStyle style = getStyle(tw, oc);
String[] months = new String[12];
int i = 0;
for (Month month : Month.values()) {
months[i] = month.getDisplayName(style, locale);
i++;
}
return months;
}
@Override
public String[] quarters(
String calendarType,
Locale locale,
TextWidth tw,
OutputContext oc
) {
TextStyle style = getStyle(tw, oc);
String[] quarters = new String[4];
for (int i = 0; i < 4; i++) {
LocalDate date = LocalDate.of(1970, i * 3 + 1, 1);
quarters[i] =
new DateTimeFormatterBuilder()
.appendText(IsoFields.QUARTER_OF_YEAR, style)
.toFormatter(locale)
.format(date);
}
return quarters;
}
@Override
public String[] weekdays(
String calendarType,
Locale locale,
TextWidth tw,
OutputContext oc
) {
TextStyle style = getStyle(tw, oc);
String[] weekdays = new String[7];
int i = 0;
for (DayOfWeek dow : DayOfWeek.values()) {
weekdays[i] = dow.getDisplayName(style, locale);
i++;
}
return weekdays;
}
@Override
public String[] eras(
String calendarType,
Locale locale,
TextWidth textWidth
) {
TextStyle style = getStyle(textWidth, OutputContext.FORMAT);
String[] eras = new String[2];
int i = 0;
for (IsoEra era : IsoEra.values()) {
eras[i] = era.getDisplayName(style, locale);
i++;
}
return eras;
}
@Override
public String[] meridiems(
String calendarType,
Locale locale,
TextWidth textWidth
) {
TextStyle style = getStyle(textWidth, OutputContext.FORMAT);
String[] meridiems = new String[2];
for (int i = 0; i < 2; i++) {
LocalTime time = LocalTime.of(i * 12, 0);
meridiems[i] =
new DateTimeFormatterBuilder()
.appendText(ChronoField.AMPM_OF_DAY, style)
.toFormatter(locale)
.format(time);
}
return meridiems;
}
@Override
public ResourceBundle.Control getControl() {
return ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT);
}
@Override
public String toString() {
return "JDKTextProvider";
}
private static TextStyle getStyle(
TextWidth tw,
OutputContext oc
) {
boolean standalone = (oc == OutputContext.STANDALONE);
switch (tw) {
case WIDE:
return (standalone ? TextStyle.FULL_STANDALONE : TextStyle.FULL);
case ABBREVIATED:
case SHORT:
return (standalone ? TextStyle.SHORT_STANDALONE : TextStyle.SHORT);
case NARROW:
return (standalone ? TextStyle.NARROW_STANDALONE : TextStyle.NARROW);
default:
throw new UnsupportedOperationException(tw.name());
}
}
}
private static class FallbackProvider
implements TextProvider {
//~ Methoden ------------------------------------------------------
@Override
public String[] getSupportedCalendarTypes() {
throw new UnsupportedOperationException("Never called.");
}
@Override
public Locale[] getAvailableLocales() {
throw new UnsupportedOperationException("Never called.");
}
@Override
public String[] months(
String calendarType,
Locale locale,
TextWidth textWidth,
OutputContext outputContext,
boolean leapForm
) {
if (textWidth == TextWidth.WIDE) {
return new String[] {
"01", "02", "03", "04", "05", "06",
"07", "08", "09", "10", "11", "12", "13"};
} else {
return new String[] {
"1", "2", "3", "4", "5", "6",
"7", "8", "9", "10", "11", "12", "13"};
}
}
@Override
public String[] quarters(
String calendarType,
Locale locale,
TextWidth textWidth,
OutputContext outputContext
) {
if (textWidth == TextWidth.NARROW) {
return new String[] {"1", "2", "3", "4"};
} else {
return new String[] {"Q1", "Q2", "Q3", "Q4"};
}
}
@Override
public String[] weekdays(
String calendarType,
Locale locale,
TextWidth textWidth,
OutputContext outputContext
) {
return new String[] {"1", "2", "3", "4", "5", "6", "7"};
}
@Override
public String[] eras(
String calendarType,
Locale locale,
TextWidth textWidth
) {
if (textWidth == TextWidth.NARROW) {
return new String[] {"B", "A"};
} else {
return new String[] {"BC", "AD"};
}
}
@Override
public String[] meridiems(
String calendarType,
Locale locale,
TextWidth textWidth
) {
if (textWidth == TextWidth.NARROW) {
return new String[] {"A", "P"};
} else {
return new String[] {"AM", "PM"};
}
}
@Override
public ResourceBundle.Control getControl() {
return ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT);
}
@Override
public String toString() {
return "FallbackProvider";
}
}
private static class FormatPatterns
implements FormatPatternProvider {
//~ Instanzvariablen ----------------------------------------------
private final FormatPatternProvider delegate;
//~ Konstruktoren -------------------------------------------------
FormatPatterns(FormatPatternProvider delegate) {
super();
this.delegate = delegate;
}
//~ Methoden ------------------------------------------------------
@Override
public String getDatePattern(
DisplayMode mode,
Locale locale
) {
if (this.delegate == null) {
int style = getFormatStyle(mode);
DateFormat df = DateFormat.getDateInstance(style, locale);
return getFormatPattern(df);
}
return this.delegate.getDatePattern(mode, locale);
}
@Override
public String getTimePattern(
DisplayMode mode,
Locale locale
) {
String pattern;
if (this.delegate == null) {
int style = getFormatStyle(mode);
DateFormat df = DateFormat.getTimeInstance(style, locale);
pattern = getFormatPattern(df);
} else if (this.delegate instanceof ExtendedPatterns) {
pattern = ExtendedPatterns.class.cast(this.delegate).getTimePattern(mode, locale, true);
} else {
pattern = this.delegate.getTimePattern(mode, locale);
}
return removeZones(pattern);
}
@Override
public String getDateTimePattern(
DisplayMode dateMode,
DisplayMode timeMode,
Locale locale
) {
if (this.delegate == null) {
int dateStyle = getFormatStyle(dateMode);
int timeStyle = getFormatStyle(timeMode);
DateFormat df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
return getFormatPattern(df);
}
String time = this.delegate.getTimePattern(timeMode, locale);
String date = this.delegate.getDatePattern(dateMode, locale);
String pattern = this.delegate.getDateTimePattern(dateMode, timeMode, locale);
return pattern.replace("{1}", date).replace("{0}", time);
}
@Override
public String getIntervalPattern(Locale locale) {
if (this.delegate == null) {
if (locale.getLanguage().isEmpty() && locale.getCountry().isEmpty()) {
return "{0}/{1}"; // ISO-8601-style
} else if (isTextRTL(locale)) {
return "{0} - {1}"; // based on analysis of CLDR-data
} else {
return "{0} - {1}"; // default
}
}
return this.delegate.getIntervalPattern(locale);
}
private static int getFormatStyle(DisplayMode mode) {
switch (mode) {
case FULL:
return DateFormat.FULL;
case LONG:
return DateFormat.LONG;
case MEDIUM:
return DateFormat.MEDIUM;
case SHORT:
return DateFormat.SHORT;
default:
throw new UnsupportedOperationException("Unknown: " + mode);
}
}
private static String getFormatPattern(DateFormat df) {
if (df instanceof SimpleDateFormat) {
return SimpleDateFormat.class.cast(df).toPattern();
}
throw new IllegalStateException("Cannot retrieve format pattern: " + df);
}
// helper method for text orientation
private static boolean isTextRTL(Locale locale) {
return RTL.contains(locale.getLanguage());
}
}
}