com.ibm.icu.util.GlobalizationPreferences Maven / Gradle / Ivy
Show all versions of icu4j Show documentation
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2004-2014, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.BreakIterator;
import com.ibm.icu.text.Collator;
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.SimpleDateFormat;
/**
* This convenience class provides a mechanism for bundling together different
* globalization preferences. It includes:
*
* - A list of locales/languages in preference order
* - A territory
* - A currency
* - A timezone
* - A calendar
* - A collator (for language-sensitive sorting, searching, and matching).
* - Explicit overrides for date/time formats, etc.
*
* The class will heuristically compute implicit, heuristic values for the above
* based on available data if explicit values are not supplied. These implicit
* values can be presented to users for confirmation, or replacement if the
* values are incorrect.
*
* To reset any explicit field so that it will get heuristic values, pass in
* null. For example, myPreferences.setLocale(null);
*
* All of the heuristics can be customized by subclasses, by overriding
* getTerritory(), guessCollator(), etc.
*
* The class also supplies display names for languages, scripts, territories,
* currencies, timezones, etc. These are computed according to the
* locale/language preference list. Thus, if the preference is Breton; French;
* English, then the display name for a language will be returned in Breton if
* available, otherwise in French if available, otherwise in English.
*
* The codes used to reference territory, currency, etc. are as defined elsewhere
* in ICU, and are taken from CLDR (which reflects RFC 3066bis usage, ISO 4217,
* and the TZ Timezone database identifiers).
*
* This is at a prototype stage, and has not incorporated all the design
* changes that we would like yet; further feedback is welcome.
* Note:
*
* - to get the display name for the first day of the week, use the calendar +
* display names.
* - to get the work days, ask the calendar (when that is available).
* - to get papersize / measurement system/bidi-orientation, ask the locale
* (when that is available there)
* - to get the field order in a date, and whether a time is 24hour or not,
* ask the DateFormat (when that is available there)
* - it will support HOST locale when it becomes available (it is a special
* locale that will ask the services to use the host platform's values).
*
*
* @draft ICU 3.6 (retainAll)
*/
//TODO:
// - Add Holidays
// - Add convenience to get/take Locale as well as ULocale.
// - Add Lenient datetime formatting when that is available.
// - Should this be serializable?
// - Other utilities?
public class GlobalizationPreferences implements Freezable {
/**
* Default constructor
* @draft ICU 3.6
*/
public GlobalizationPreferences(){}
/**
* Number Format type
* @draft ICU 3.6
*/
public static final int
NF_NUMBER = 0, // NumberFormat.NUMBERSTYLE
NF_CURRENCY = 1, // NumberFormat.CURRENCYSTYLE
NF_PERCENT = 2, // NumberFormat.PERCENTSTYLE
NF_SCIENTIFIC = 3, // NumberFormat.SCIENTIFICSTYLE
NF_INTEGER = 4; // NumberFormat.INTEGERSTYLE
private static final int NF_LIMIT = NF_INTEGER + 1;
/**
* Date Format type
* @draft ICU 3.6
*/
public static final int
DF_FULL = DateFormat.FULL, // 0
DF_LONG = DateFormat.LONG, // 1
DF_MEDIUM = DateFormat.MEDIUM, // 2
DF_SHORT = DateFormat.SHORT, // 3
DF_NONE = 4;
private static final int DF_LIMIT = DF_NONE + 1;
/**
* For selecting a choice of display names
* @draft ICU 3.6
*/
public static final int
ID_LOCALE = 0,
ID_LANGUAGE = 1,
ID_SCRIPT = 2,
ID_TERRITORY = 3,
ID_VARIANT = 4,
ID_KEYWORD = 5,
ID_KEYWORD_VALUE = 6,
ID_CURRENCY = 7,
ID_CURRENCY_SYMBOL = 8,
ID_TIMEZONE = 9;
//private static final int ID_LIMIT = ID_TIMEZONE + 1;
/**
* Break iterator type
* @draft ICU 3.6
*/
public static final int
BI_CHARACTER = BreakIterator.KIND_CHARACTER, // 0
BI_WORD = BreakIterator.KIND_WORD, // 1
BI_LINE = BreakIterator.KIND_LINE, // 2
BI_SENTENCE = BreakIterator.KIND_SENTENCE, // 3
BI_TITLE = BreakIterator.KIND_TITLE; // 4
private static final int BI_LIMIT = BI_TITLE + 1;
/**
* Sets the language/locale priority list. If other information is
* not (yet) available, this is used to to produce a default value
* for the appropriate territory, currency, timezone, etc. The
* user should be given the opportunity to correct those defaults
* in case they are incorrect.
*
* @param inputLocales list of locales in priority order, eg {"be", "fr"}
* for Breton first, then French if that fails.
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setLocales(List inputLocales) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
locales = processLocales(inputLocales);
return this;
}
/**
* Get a copy of the language/locale priority list
*
* @return a copy of the language/locale priority list.
* @draft ICU 3.6
*/
public List getLocales() {
List result;
if (locales == null) {
result = guessLocales();
} else {
result = new ArrayList<>();
result.addAll(locales);
}
return result;
}
/**
* Convenience function for getting the locales in priority order
* @param index The index (0..n) of the desired item.
* @return desired item. null if index is out of range
* @draft ICU 3.6
*/
public ULocale getLocale(int index) {
List lcls = locales;
if (lcls == null) {
lcls = guessLocales();
}
if (index >= 0 && index < lcls.size()) {
return lcls.get(index);
}
return null;
}
/**
* Convenience routine for setting the language/locale priority
* list from an array.
*
* @see #setLocales(List locales)
* @param uLocales list of locales in an array
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setLocales(ULocale[] uLocales) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
return setLocales(Arrays.asList(uLocales));
}
/**
* Convenience routine for setting the language/locale priority
* list from a single locale/language.
*
* @see #setLocales(List locales)
* @param uLocale single locale
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setLocale(ULocale uLocale) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
return setLocales(new ULocale[]{uLocale});
}
/**
* Convenience routine for setting the locale priority list from
* an Accept-Language string.
* @see #setLocales(List locales)
* @param acceptLanguageString Accept-Language list, as defined by
* Section 14.4 of the RFC 2616 (HTTP 1.1)
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setLocales(String acceptLanguageString) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
Set acceptSet = LocalePriorityList.add(acceptLanguageString).build().getULocales();
// processLocales() wants a List even though it only iterates front-to-back.
locales = processLocales(new ArrayList<>(acceptSet));
return this;
}
/**
* Convenience function to get a ResourceBundle instance using
* the specified base name based on the language/locale priority list
* stored in this object.
*
* @param baseName the base name of the resource bundle, a fully qualified
* class name
* @return a resource bundle for the given base name and locale based on the
* language/locale priority list stored in this object
* @draft ICU 3.6
*/
public ResourceBundle getResourceBundle(String baseName) {
return getResourceBundle(baseName, null);
}
/**
* Convenience function to get a ResourceBundle instance using
* the specified base name and class loader based on the language/locale
* priority list stored in this object.
*
* @param baseName the base name of the resource bundle, a fully qualified
* class name
* @param loader the class object from which to load the resource bundle
* @return a resource bundle for the given base name and locale based on the
* language/locale priority list stored in this object
* @draft ICU 3.6
*/
public ResourceBundle getResourceBundle(String baseName, ClassLoader loader) {
UResourceBundle urb = null;
UResourceBundle candidate = null;
String actualLocaleName = null;
List fallbacks = getLocales();
for (int i = 0; i < fallbacks.size(); i++) {
String localeName = (fallbacks.get(i)).toString();
if (actualLocaleName != null && localeName.equals(actualLocaleName)) {
// Actual locale name in the previous round may exactly matches
// with the next fallback locale
urb = candidate;
break;
}
try {
if (loader == null) {
candidate = UResourceBundle.getBundleInstance(baseName, localeName);
}
else {
candidate = UResourceBundle.getBundleInstance(baseName, localeName, loader);
}
if (candidate != null) {
actualLocaleName = candidate.getULocale().getName();
if (actualLocaleName.equals(localeName)) {
urb = candidate;
break;
}
if (urb == null) {
// Preserve the available bundle as the last resort
urb = candidate;
}
}
} catch (MissingResourceException mre) {
actualLocaleName = null;
continue;
}
}
if (urb == null) {
throw new MissingResourceException("Can't find bundle for base name "
+ baseName, baseName, "");
}
return urb;
}
/**
* Sets the territory, which is a valid territory according to for
* RFC 3066 (or successor). If not otherwise set, default
* currency and timezone values will be set from this. The user
* should be given the opportunity to correct those defaults in
* case they are incorrect.
*
* @param territory code
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setTerritory(String territory) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
this.territory = territory; // immutable, so don't need to clone
return this;
}
/**
* Gets the territory setting. If it wasn't explicitly set, it is
* computed from the general locale setting.
*
* @return territory code, explicit or implicit.
* @draft ICU 3.6
*/
public String getTerritory() {
if (territory == null) {
return guessTerritory();
}
return territory; // immutable, so don't need to clone
}
/**
* Sets the currency code. If this has not been set, uses default for territory.
*
* @param currency Valid ISO 4217 currency code.
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setCurrency(Currency currency) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
this.currency = currency; // immutable, so don't need to clone
return this;
}
/**
* Get a copy of the currency computed according to the settings.
*
* @return currency code, explicit or implicit.
* @draft ICU 3.6
*/
public Currency getCurrency() {
if (currency == null) {
return guessCurrency();
}
return currency; // immutable, so don't have to clone
}
/**
* Sets the calendar. If this has not been set, uses default for territory.
*
* @param calendar arbitrary calendar
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setCalendar(Calendar calendar) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
this.calendar = (Calendar) calendar.clone(); // clone for safety
return this;
}
/**
* Get a copy of the calendar according to the settings.
*
* @return calendar explicit or implicit.
* @draft ICU 3.6
*/
public Calendar getCalendar() {
if (calendar == null) {
return guessCalendar();
}
Calendar temp = (Calendar) calendar.clone(); // clone for safety
temp.setTimeZone(getTimeZone());
temp.setTimeInMillis(System.currentTimeMillis());
return temp;
}
/**
* Sets the timezone ID. If this has not been set, uses default for territory.
*
* @param timezone a valid TZID (see UTS#35).
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setTimeZone(TimeZone timezone) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
this.timezone = (TimeZone) timezone.clone(); // clone for safety;
return this;
}
/**
* Get the timezone. It was either explicitly set, or is
* heuristically computed from other settings.
*
* @return timezone, either implicitly or explicitly set
* @draft ICU 3.6
*/
public TimeZone getTimeZone() {
if (timezone == null) {
return guessTimeZone();
}
return timezone.cloneAsThawed(); // clone for safety
}
/**
* Get a copy of the collator according to the settings.
*
* @return collator explicit or implicit.
* @draft ICU 3.6
*/
public Collator getCollator() {
if (collator == null) {
return guessCollator();
}
try {
return (Collator) collator.clone(); // clone for safety
} catch (CloneNotSupportedException e) {
throw new ICUCloneNotSupportedException("Error in cloning collator", e);
}
}
/**
* Explicitly set the collator for this object.
* @param collator The collator object to be passed.
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setCollator(Collator collator) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
try {
this.collator = (Collator) collator.clone(); // clone for safety
} catch (CloneNotSupportedException e) {
throw new ICUCloneNotSupportedException("Error in cloning collator", e);
}
return this;
}
/**
* Get a copy of the break iterator for the specified type according to the
* settings.
*
* @param type break type - BI_CHARACTER or BI_WORD, BI_LINE, BI_SENTENCE, BI_TITLE
* @return break iterator explicit or implicit
* @draft ICU 3.6
*/
public BreakIterator getBreakIterator(int type) {
if (type < BI_CHARACTER || type >= BI_LIMIT) {
throw new IllegalArgumentException("Illegal break iterator type");
}
if (breakIterators == null || breakIterators[type] == null) {
return guessBreakIterator(type);
}
return (BreakIterator) breakIterators[type].clone(); // clone for safety
}
/**
* Explicitly set the break iterator for this object.
*
* @param type break type - BI_CHARACTER or BI_WORD, BI_LINE, BI_SENTENCE, BI_TITLE
* @param iterator a break iterator
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setBreakIterator(int type, BreakIterator iterator) {
if (type < BI_CHARACTER || type >= BI_LIMIT) {
throw new IllegalArgumentException("Illegal break iterator type");
}
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
if (breakIterators == null)
breakIterators = new BreakIterator[BI_LIMIT];
breakIterators[type] = (BreakIterator) iterator.clone(); // clone for safety
return this;
}
/**
* Get the display name for an ID: language, script, territory, currency, timezone...
* Uses the language priority list to do so.
*
* @param id language code, script code, ...
* @param type specifies the type of the ID: ID_LANGUAGE, etc.
* @return the display name
* @draft ICU 3.6
*/
public String getDisplayName(String id, int type) {
String result = id;
for (ULocale locale : getLocales()) {
if (!isAvailableLocale(locale, TYPE_GENERIC)) {
continue;
}
switch (type) {
case ID_LOCALE:
result = ULocale.getDisplayName(id, locale);
break;
case ID_LANGUAGE:
result = ULocale.getDisplayLanguage(id, locale);
break;
case ID_SCRIPT:
result = ULocale.getDisplayScript("und-" + id, locale);
break;
case ID_TERRITORY:
result = ULocale.getDisplayCountry("und-" + id, locale);
break;
case ID_VARIANT:
// TODO fix variant parsing
result = ULocale.getDisplayVariant("und-QQ-" + id, locale);
break;
case ID_KEYWORD:
result = ULocale.getDisplayKeyword(id, locale);
break;
case ID_KEYWORD_VALUE:
String[] parts = new String[2];
Utility.split(id,'=',parts);
result = ULocale.getDisplayKeywordValue("und@"+id, parts[0], locale);
// TODO fix to tell when successful
if (result.equals(parts[1])) {
continue;
}
break;
case ID_CURRENCY_SYMBOL:
case ID_CURRENCY:
Currency temp = new Currency(id);
result =temp.getName(locale, type==ID_CURRENCY
? Currency.LONG_NAME
: Currency.SYMBOL_NAME, null /* isChoiceFormat */);
// TODO: have method that doesn't take parameter. Add
// function to determine whether string is choice
// format.
// TODO: have method that doesn't require us
// to create a currency
break;
case ID_TIMEZONE:
SimpleDateFormat dtf = new SimpleDateFormat("vvvv",locale);
dtf.setTimeZone(TimeZone.getFrozenTimeZone(id));
result = dtf.format(new Date());
// TODO, have method that doesn't require us to create a timezone
// fix other hacks
// hack for couldn't match
boolean isBadStr = false;
// Matcher badTimeZone = Pattern.compile("[A-Z]{2}|.*\\s\\([A-Z]{2}\\)").matcher("");
// badtzstr = badTimeZone.reset(result).matches();
String teststr = result;
int sidx = result.indexOf('(');
int eidx = result.indexOf(')');
if (sidx != -1 && eidx != -1 && (eidx - sidx) == 3) {
teststr = result.substring(sidx+1, eidx);
}
if (teststr.length() == 2) {
isBadStr = true;
for (int i = 0; i < 2; i++) {
char c = teststr.charAt(i);
if (c < 'A' || 'Z' < c) {
isBadStr = false;
break;
}
}
}
if (isBadStr) {
continue;
}
break;
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
// TODO need better way of seeing if we fell back to root!!
// This will not work at all for lots of stuff
if (!id.equals(result)) {
return result;
}
}
return result;
}
/**
* Set an explicit date format. Overrides the locale priority list for
* a particular combination of dateStyle and timeStyle. DF_NONE should
* be used if for the style, where only the date or time format individually
* is being set.
*
* @param dateStyle DF_FULL, DF_LONG, DF_MEDIUM, DF_SHORT or DF_NONE
* @param timeStyle DF_FULL, DF_LONG, DF_MEDIUM, DF_SHORT or DF_NONE
* @param format The date format
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setDateFormat(int dateStyle, int timeStyle, DateFormat format) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
if (dateFormats == null) {
dateFormats = new DateFormat[DF_LIMIT][DF_LIMIT];
}
dateFormats[dateStyle][timeStyle] = (DateFormat) format.clone(); // for safety
return this;
}
/**
* Gets a date format according to the current settings. If there
* is an explicit (non-null) date/time format set, a copy of that
* is returned. Otherwise, the language priority list is used.
* DF_NONE should be used for the style, where only the date or
* time format individually is being gotten.
*
* @param dateStyle DF_FULL, DF_LONG, DF_MEDIUM, DF_SHORT or DF_NONE
* @param timeStyle DF_FULL, DF_LONG, DF_MEDIUM, DF_SHORT or DF_NONE
* @return a DateFormat, according to the above description
* @draft ICU 3.6
*/
public DateFormat getDateFormat(int dateStyle, int timeStyle) {
if (dateStyle == DF_NONE && timeStyle == DF_NONE
|| dateStyle < 0 || dateStyle >= DF_LIMIT
|| timeStyle < 0 || timeStyle >= DF_LIMIT) {
throw new IllegalArgumentException("Illegal date format style arguments");
}
DateFormat result = null;
if (dateFormats != null) {
result = dateFormats[dateStyle][timeStyle];
}
if (result != null) {
result = (DateFormat) result.clone(); // clone for safety
// Not sure overriding configuration is what we really want...
result.setTimeZone(getTimeZone());
} else {
result = guessDateFormat(dateStyle, timeStyle);
}
return result;
}
/**
* Gets a number format according to the current settings. If
* there is an explicit (non-null) number format set, a copy of
* that is returned. Otherwise, the language priority list is
* used.
*
* @param style NF_NUMBER, NF_CURRENCY, NF_PERCENT, NF_SCIENTIFIC, NF_INTEGER
* @draft ICU 3.6
*/
public NumberFormat getNumberFormat(int style) {
if (style < 0 || style >= NF_LIMIT) {
throw new IllegalArgumentException("Illegal number format type");
}
NumberFormat result = null;
if (numberFormats != null) {
result = numberFormats[style];
}
if (result != null) {
result = (NumberFormat) result.clone(); // clone for safety (later optimize)
} else {
result = guessNumberFormat(style);
}
return result;
}
/**
* Sets a number format explicitly. Overrides the general locale settings.
*
* @param style NF_NUMBER, NF_CURRENCY, NF_PERCENT, NF_SCIENTIFIC, NF_INTEGER
* @param format The number format
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences setNumberFormat(int style, NumberFormat format) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
if (numberFormats == null) {
numberFormats = new NumberFormat[NF_LIMIT];
}
numberFormats[style] = (NumberFormat) format.clone(); // for safety
return this;
}
/**
* Restore the object to the initial state.
*
* @return this, for chaining
* @draft ICU 3.6
*/
public GlobalizationPreferences reset() {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify immutable object");
}
locales = null;
territory = null;
calendar = null;
collator = null;
breakIterators = null;
timezone = null;
currency = null;
dateFormats = null;
numberFormats = null;
implicitLocales = null;
return this;
}
/**
* Process a language/locale priority list specified via setLocales
.
* The input locale list may be expanded or re-ordered to represent the prioritized
* language/locale order actually used by this object by the algorithm explained
* below.
*
*
* Step 1: Move later occurrence of more specific locale before earlier
* occurrence of less specific locale.
*
* Before: en, fr_FR, en_US, en_GB
*
* After: en_US, en_GB, en, fr_FR
*
*
* Step 2: Append a fallback locale to each locale.
*
* Before: en_US, en_GB, en, fr_FR
*
* After: en_US, en, en_GB, en, en, fr_FR, fr
*
*
* Step 3: Remove earlier occurrence of duplicated locale entries.
*
* Before: en_US, en, en_GB, en, en, fr_FR, fr
*
* After: en_US, en_GB, en, fr_FR, fr
*
*
* The final locale list is used to produce a default value for the appropriate territory,
* currency, timezone, etc. The list also represents the lookup order used in
* getResourceBundle
for this object. A subclass may override this method
* to customize the algorithm used for populating the locale list.
*
* @param inputLocales The list of input locales
* @draft ICU 3.6
*/
protected List processLocales(List inputLocales) {
// Note: Some of the callers, and non-ICU call sites, could be simpler/more efficient
// if this method took a Collection or even an Iterable.
// Maybe we can change it since this is still @draft and probably not widely overridden.
List result = new ArrayList<>();
/*
* Step 1: Relocate later occurrence of more specific locale
* before earlier occurrence of less specific locale.
*
* Example:
* Before - en_US, fr_FR, zh, en_US_Boston, zh_TW, zh_Hant, fr_CA
* After - en_US_Boston, en_US, fr_FR, zh_TW, zh_Hant, zh, fr_CA
*/
for (ULocale uloc : inputLocales) {
String language = uloc.getLanguage();
String script = uloc.getScript();
String country = uloc.getCountry();
String variant = uloc.getVariant();
boolean bInserted = false;
for (int j = 0; j < result.size(); j++) {
// Check if this locale is more specific
// than existing locale entries already inserted
// in the destination list
ULocale u = result.get(j);
if (!u.getLanguage().equals(language)) {
continue;
}
String s = u.getScript();
String c = u.getCountry();
String v = u.getVariant();
if (!s.equals(script)) {
if (s.length() == 0 && c.length() == 0 && v.length() == 0) {
result.add(j, uloc);
bInserted = true;
break;
} else if (s.length() == 0 && c.equals(country)) {
// We want to see zh_Hant_HK before zh_HK
result.add(j, uloc);
bInserted = true;
break;
} else if (script.length() == 0 && country.length() > 0 && c.length() == 0) {
// We want to see zh_HK before zh_Hant
result.add(j, uloc);
bInserted = true;
break;
}
continue;
}
if (!c.equals(country)) {
if (c.length() == 0 && v.length() == 0) {
result.add(j, uloc);
bInserted = true;
break;
}
}
if (!v.equals(variant) && v.length() == 0) {
result.add(j, uloc);
bInserted = true;
break;
}
}
if (!bInserted) {
// Add this locale at the end of the list
result.add(uloc);
}
}
// TODO: Locale aliases might be resolved here
// For example, zh_Hant_TW = zh_TW
/*
* Step 2: Append fallback locales for each entry
*
* Example:
* Before - en_US_Boston, en_US, fr_FR, zh_TW, zh_Hant, zh, fr_CA
* After - en_US_Boston, en_US, en, en_US, en, fr_FR, fr,
* zh_TW, zn, zh_Hant, zh, zh, fr_CA, fr
*/
int index = 0;
while (index < result.size()) {
ULocale uloc = result.get(index);
while ((uloc = uloc.getFallback()) != null) {
if (uloc.getLanguage().length() == 0) {
break;
}
index++;
result.add(index, uloc);
}
index++;
}
/*
* Step 3: Remove earlier occurrence of duplicated locales
*
* Example:
* Before - en_US_Boston, en_US, en, en_US, en, fr_FR, fr,
* zh_TW, zn, zh_Hant, zh, zh, fr_CA, fr
* After - en_US_Boston, en_US, en, fr_FR, zh_TW, zh_Hant,
* zh, fr_CA, fr
*/
index = 0;
while (index < result.size() - 1) {
ULocale uloc = result.get(index);
boolean bRemoved = false;
for (int i = index + 1; i < result.size(); i++) {
if (uloc.equals(result.get(i))) {
// Remove earlier one
result.remove(index);
bRemoved = true;
break;
}
}
if (!bRemoved) {
index++;
}
}
return result;
}
/**
* This function can be overridden by subclasses to use different heuristics.
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @param dateStyle
* @param timeStyle
* @draft ICU 3.6
*/
protected DateFormat guessDateFormat(int dateStyle, int timeStyle) {
DateFormat result;
ULocale dfLocale = getAvailableLocale(TYPE_DATEFORMAT);
if (dfLocale == null) {
dfLocale = ULocale.ROOT;
}
if (timeStyle == DF_NONE) {
result = DateFormat.getDateInstance(getCalendar(), dateStyle, dfLocale);
} else if (dateStyle == DF_NONE) {
result = DateFormat.getTimeInstance(getCalendar(), timeStyle, dfLocale);
} else {
result = DateFormat.getDateTimeInstance(getCalendar(), dateStyle, timeStyle, dfLocale);
}
return result;
}
/**
* This function can be overridden by subclasses to use different heuristics.
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @param style
* @draft ICU 3.6
*/
protected NumberFormat guessNumberFormat(int style) {
NumberFormat result;
ULocale nfLocale = getAvailableLocale(TYPE_NUMBERFORMAT);
if (nfLocale == null) {
nfLocale = ULocale.ROOT;
}
switch (style) {
case NF_NUMBER:
result = NumberFormat.getInstance(nfLocale);
break;
case NF_SCIENTIFIC:
result = NumberFormat.getScientificInstance(nfLocale);
break;
case NF_INTEGER:
result = NumberFormat.getIntegerInstance(nfLocale);
break;
case NF_PERCENT:
result = NumberFormat.getPercentInstance(nfLocale);
break;
case NF_CURRENCY:
result = NumberFormat.getCurrencyInstance(nfLocale);
result.setCurrency(getCurrency());
break;
default:
throw new IllegalArgumentException("Unknown number format style");
}
return result;
}
/**
* This function can be overridden by subclasses to use different heuristics.
*
* @draft ICU 3.6
*/
protected String guessTerritory() {
String result;
// pass through locales to see if there is a territory.
for (ULocale locale : getLocales()) {
result = locale.getCountry();
if (result.length() != 0) {
return result;
}
}
// if not, guess from the first language tag, or maybe from
// intersection of languages, eg nl + fr => BE
// TODO: fix using real data
// for now, just use fixed values
ULocale firstLocale = getLocale(0);
String language = firstLocale.getLanguage();
String script = firstLocale.getScript();
result = null;
if (script.length() != 0) {
result = language_territory_hack_map.get(language + "_" + script);
}
if (result == null) {
result = language_territory_hack_map.get(language);
}
if (result == null) {
result = "US"; // need *some* default
}
return result;
}
/**
* This function can be overridden by subclasses to use different heuristics
*
* @draft ICU 3.6
*/
protected Currency guessCurrency() {
return Currency.getInstance(new ULocale("und-" + getTerritory()));
}
/**
* This function can be overridden by subclasses to use different heuristics
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @draft ICU 3.6
*/
protected List guessLocales() {
if (implicitLocales == null) {
List result = new ArrayList<>(1);
result.add(ULocale.getDefault());
implicitLocales = processLocales(result);
}
return implicitLocales;
}
/**
* This function can be overridden by subclasses to use different heuristics.
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @draft ICU 3.6
*/
protected Collator guessCollator() {
ULocale collLocale = getAvailableLocale(TYPE_COLLATOR);
if (collLocale == null) {
collLocale = ULocale.ROOT;
}
return Collator.getInstance(collLocale);
}
/**
* This function can be overridden by subclasses to use different heuristics.
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @param type
* @draft ICU 3.6
*/
protected BreakIterator guessBreakIterator(int type) {
BreakIterator bitr = null;
ULocale brkLocale = getAvailableLocale(TYPE_BREAKITERATOR);
if (brkLocale == null) {
brkLocale = ULocale.ROOT;
}
switch (type) {
case BI_CHARACTER:
bitr = BreakIterator.getCharacterInstance(brkLocale);
break;
case BI_TITLE:
bitr = BreakIterator.getTitleInstance(brkLocale);
break;
case BI_WORD:
bitr = BreakIterator.getWordInstance(brkLocale);
break;
case BI_LINE:
bitr = BreakIterator.getLineInstance(brkLocale);
break;
case BI_SENTENCE:
bitr = BreakIterator.getSentenceInstance(brkLocale);
break;
default:
throw new IllegalArgumentException("Unknown break iterator type");
}
return bitr;
}
/**
* This function can be overridden by subclasses to use different heuristics.
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @draft ICU 3.6
*/
protected TimeZone guessTimeZone() {
// TODO fix using real data
// for single-zone countries, pick that zone
// for others, pick the most populous zone
// for now, just use fixed value
// NOTE: in a few cases can do better by looking at language.
// Eg haw+US should go to Pacific/Honolulu
// fr+CA should go to America/Montreal
String timezoneString = territory_tzid_hack_map.get(getTerritory());
if (timezoneString == null) {
String[] attempt = TimeZone.getAvailableIDs(getTerritory());
if (attempt.length == 0) {
timezoneString = "Etc/GMT"; // gotta do something
} else {
int i;
// this all needs to be fixed to use real data. But for now, do slightly better by skipping cruft
for (i = 0; i < attempt.length; ++i) {
if (attempt[i].indexOf("/") >= 0) break;
}
if (i > attempt.length) i = 0;
timezoneString = attempt[i];
}
}
return TimeZone.getTimeZone(timezoneString);
}
/**
* This function can be overridden by subclasses to use different heuristics.
* It MUST return a 'safe' value,
* one whose modification will not affect this object.
*
* @draft ICU 3.6
*/
protected Calendar guessCalendar() {
ULocale calLocale = getAvailableLocale(TYPE_CALENDAR);
if (calLocale == null) {
calLocale = ULocale.US;
}
return Calendar.getInstance(getTimeZone(), calLocale);
}
// PRIVATES
private List locales;
private String territory;
private Currency currency;
private TimeZone timezone;
private Calendar calendar;
private Collator collator;
private BreakIterator[] breakIterators;
private DateFormat[][] dateFormats;
private NumberFormat[] numberFormats;
private List implicitLocales;
{
reset();
}
private ULocale getAvailableLocale(int type) {
List locs = getLocales();
ULocale result = null;
for (int i = 0; i < locs.size(); i++) {
ULocale l = locs.get(i);
if (isAvailableLocale(l, type)) {
result = l;
break;
}
}
return result;
}
private boolean isAvailableLocale(ULocale loc, int type) {
BitSet bits = available_locales.get(loc);
if (bits != null && bits.get(type)) {
return true;
}
return false;
}
/*
* Available locales for service types
*/
private static final HashMap available_locales = new HashMap<>();
private static final int
TYPE_GENERIC = 0,
TYPE_CALENDAR = 1,
TYPE_DATEFORMAT= 2,
TYPE_NUMBERFORMAT = 3,
TYPE_COLLATOR = 4,
TYPE_BREAKITERATOR = 5,
TYPE_LIMIT = TYPE_BREAKITERATOR + 1;
static {
BitSet bits;
ULocale[] allLocales = ULocale.getAvailableLocales();
for (int i = 0; i < allLocales.length; i++) {
bits = new BitSet(TYPE_LIMIT);
available_locales.put(allLocales[i], bits);
bits.set(TYPE_GENERIC);
}
ULocale[] calLocales = Calendar.getAvailableULocales();
for (int i = 0; i < calLocales.length; i++) {
bits = available_locales.get(calLocales[i]);
if (bits == null) {
bits = new BitSet(TYPE_LIMIT);
available_locales.put(allLocales[i], bits);
}
bits.set(TYPE_CALENDAR);
}
ULocale[] dateLocales = DateFormat.getAvailableULocales();
for (int i = 0; i < dateLocales.length; i++) {
bits = available_locales.get(dateLocales[i]);
if (bits == null) {
bits = new BitSet(TYPE_LIMIT);
available_locales.put(allLocales[i], bits);
}
bits.set(TYPE_DATEFORMAT);
}
ULocale[] numLocales = NumberFormat.getAvailableULocales();
for (int i = 0; i < numLocales.length; i++) {
bits = available_locales.get(numLocales[i]);
if (bits == null) {
bits = new BitSet(TYPE_LIMIT);
available_locales.put(allLocales[i], bits);
}
bits.set(TYPE_NUMBERFORMAT);
}
ULocale[] collLocales = Collator.getAvailableULocales();
for (int i = 0; i < collLocales.length; i++) {
bits = available_locales.get(collLocales[i]);
if (bits == null) {
bits = new BitSet(TYPE_LIMIT);
available_locales.put(allLocales[i], bits);
}
bits.set(TYPE_COLLATOR);
}
ULocale[] brkLocales = BreakIterator.getAvailableULocales();
for (int i = 0; i < brkLocales.length; i++) {
bits = available_locales.get(brkLocales[i]);
bits.set(TYPE_BREAKITERATOR);
}
}
/** WARNING: All of this data is temporary, until we start importing from CLDR!!!
*
*/
private static final Map language_territory_hack_map = new HashMap<>();
private static final String[][] language_territory_hack = {
{"af", "ZA"},
{"am", "ET"},
{"ar", "SA"},
{"as", "IN"},
{"ay", "PE"},
{"az", "AZ"},
{"bal", "PK"},
{"be", "BY"},
{"bg", "BG"},
{"bn", "IN"},
{"bs", "BA"},
{"ca", "ES"},
{"ch", "MP"},
{"cpe", "SL"},
{"cs", "CZ"},
{"cy", "GB"},
{"da", "DK"},
{"de", "DE"},
{"dv", "MV"},
{"dz", "BT"},
{"el", "GR"},
{"en", "US"},
{"es", "ES"},
{"et", "EE"},
{"eu", "ES"},
{"fa", "IR"},
{"fi", "FI"},
{"fil", "PH"},
{"fj", "FJ"},
{"fo", "FO"},
{"fr", "FR"},
{"ga", "IE"},
{"gd", "GB"},
{"gl", "ES"},
{"gn", "PY"},
{"gu", "IN"},
{"gv", "GB"},
{"ha", "NG"},
{"he", "IL"},
{"hi", "IN"},
{"ho", "PG"},
{"hr", "HR"},
{"ht", "HT"},
{"hu", "HU"},
{"hy", "AM"},
{"id", "ID"},
{"is", "IS"},
{"it", "IT"},
{"ja", "JP"},
{"ka", "GE"},
{"kk", "KZ"},
{"kl", "GL"},
{"km", "KH"},
{"kn", "IN"},
{"ko", "KR"},
{"kok", "IN"},
{"ks", "IN"},
{"ku", "TR"},
{"ky", "KG"},
{"la", "VA"},
{"lb", "LU"},
{"ln", "CG"},
{"lo", "LA"},
{"lt", "LT"},
{"lv", "LV"},
{"mai", "IN"},
{"men", "GN"},
{"mg", "MG"},
{"mh", "MH"},
{"mk", "MK"},
{"ml", "IN"},
{"mn", "MN"},
{"mni", "IN"},
{"mo", "MD"},
{"mr", "IN"},
{"ms", "MY"},
{"mt", "MT"},
{"my", "MM"},
{"na", "NR"},
{"nb", "NO"},
{"nd", "ZA"},
{"ne", "NP"},
{"niu", "NU"},
{"nl", "NL"},
{"nn", "NO"},
{"no", "NO"},
{"nr", "ZA"},
{"nso", "ZA"},
{"ny", "MW"},
{"om", "KE"},
{"or", "IN"},
{"pa", "IN"},
{"pau", "PW"},
{"pl", "PL"},
{"ps", "PK"},
{"pt", "BR"},
{"qu", "PE"},
{"rn", "BI"},
{"ro", "RO"},
{"ru", "RU"},
{"rw", "RW"},
{"sd", "IN"},
{"sg", "CF"},
{"si", "LK"},
{"sk", "SK"},
{"sl", "SI"},
{"sm", "WS"},
{"so", "DJ"},
{"sq", "CS"},
{"sr", "CS"},
{"ss", "ZA"},
{"st", "ZA"},
{"sv", "SE"},
{"sw", "KE"},
{"ta", "IN"},
{"te", "IN"},
{"tem", "SL"},
{"tet", "TL"},
{"th", "TH"},
{"ti", "ET"},
{"tg", "TJ"},
{"tk", "TM"},
{"tkl", "TK"},
{"tvl", "TV"},
{"tl", "PH"},
{"tn", "ZA"},
{"to", "TO"},
{"tpi", "PG"},
{"tr", "TR"},
{"ts", "ZA"},
{"uk", "UA"},
{"ur", "IN"},
{"uz", "UZ"},
{"ve", "ZA"},
{"vi", "VN"},
{"wo", "SN"},
{"xh", "ZA"},
{"zh", "CN"},
{"zh_Hant", "TW"},
{"zu", "ZA"},
{"aa", "ET"},
{"byn", "ER"},
{"eo", "DE"},
{"gez", "ET"},
{"haw", "US"},
{"iu", "CA"},
{"kw", "GB"},
{"sa", "IN"},
{"sh", "HR"},
{"sid", "ET"},
{"syr", "SY"},
{"tig", "ER"},
{"tt", "RU"},
{"wal", "ET"}, };
static {
for (int i = 0; i < language_territory_hack.length; ++i) {
language_territory_hack_map.put(language_territory_hack[i][0],language_territory_hack[i][1]);
}
}
static final Map territory_tzid_hack_map = new HashMap<>();
static final String[][] territory_tzid_hack = {
{"AQ", "Antarctica/McMurdo"},
{"AR", "America/Buenos_Aires"},
{"AU", "Australia/Sydney"},
{"BR", "America/Sao_Paulo"},
{"CA", "America/Toronto"},
{"CD", "Africa/Kinshasa"},
{"CL", "America/Santiago"},
{"CN", "Asia/Shanghai"},
{"EC", "America/Guayaquil"},
{"ES", "Europe/Madrid"},
{"GB", "Europe/London"},
{"GL", "America/Godthab"},
{"ID", "Asia/Jakarta"},
{"ML", "Africa/Bamako"},
{"MX", "America/Mexico_City"},
{"MY", "Asia/Kuala_Lumpur"},
{"NZ", "Pacific/Auckland"},
{"PT", "Europe/Lisbon"},
{"RU", "Europe/Moscow"},
{"UA", "Europe/Kiev"},
{"US", "America/New_York"},
{"UZ", "Asia/Tashkent"},
{"PF", "Pacific/Tahiti"},
{"FM", "Pacific/Kosrae"},
{"KI", "Pacific/Tarawa"},
{"KZ", "Asia/Almaty"},
{"MH", "Pacific/Majuro"},
{"MN", "Asia/Ulaanbaatar"},
{"SJ", "Arctic/Longyearbyen"},
{"UM", "Pacific/Midway"},
};
static {
for (int i = 0; i < territory_tzid_hack.length; ++i) {
territory_tzid_hack_map.put(territory_tzid_hack[i][0],territory_tzid_hack[i][1]);
}
}
// Freezable implementation
private volatile boolean frozen;
/**
* @draft ICU 3.6
*/
@Override
public boolean isFrozen() {
return frozen;
}
/**
* @draft ICU 4.4
*/
@Override
public GlobalizationPreferences freeze() {
frozen = true;
return this;
}
/**
* @draft ICU 4.4
*/
@Override
public GlobalizationPreferences cloneAsThawed() {
try {
GlobalizationPreferences result = (GlobalizationPreferences) clone();
result.frozen = false;
return result;
} catch (CloneNotSupportedException e) {
// will always work
return null;
}
}
}