blob: 5e6b574bf3cf79ef1c40862aef0528ddf5ae1e0a [file] [log] [blame]
//##header 1132615047000
/*
*******************************************************************************
* Copyright (C) 2004-2005, 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.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
//#ifndef FOUNDATION
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//#endif
import com.ibm.icu.impl.Utility;
import com.ibm.icu.impl.ZoneMeta;
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:
* <ul>
* <li>A list of locales/languages in preference order</li>
* <li>A territory</li>
* <li>A currency</li>
* <li>A timezone</li>
* <li>A calendar</li>
* <li>A collator (for language-sensitive sorting, searching, and matching).</li>
* <li>And explicit overrides for date/time formats, etc.</li></ul>
* 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.
* <p>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.
* <p><b>This is at a prototype stage, and has not incorporated all the design changes
* that we would like yet; further feedback is welcome.
*
* @internal
* @deprecated ICU 3.4.2
*/
public class GlobalizationPreferences {
/**
* Number Format types
*/
public static final int CURRENCY = 0, NUMBER = 1, INTEGER = 2, SCIENTIFIC = 3, PERCENT = 4, NUMBER_LIMIT = 5;
/**
* Supplement to DateFormat.FULL, LONG, MEDIUM, SHORT. Indicates that no value for one of date or time is to be used.
*/
public static final int NONE = 4;
/**
* For selecting a choice of display names
*/
public static final int
LOCALEID = 0, LANGUAGEID = 1, SCRIPTID = 2, TERRITORYID = 3, VARIANTID = 4,
KEYWORDID = 5, KEYWORD_VALUEID = 6,
CURRENCYID = 7, CURRENCY_SYMBOLID = 8, TIMEZONEID = 9, DISPLAYID_LIMIT = 10;
/**
* 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 locales list of locales in priority order, eg {"be", "fr"} for Breton first, then French if that fails.
* @return this, for chaining
*/
public GlobalizationPreferences setULocales(List locales) {
this.locales = new ArrayList(locales);
explicitLocales = true;
if (!explicitTerritory) guessTerritory();
if (!explicitCurrency) guessCurrency();
if (!explicitTimezone) guessTimeZone();
if (!explicitCalendar) guessCalendar();
return this;
}
/**
* @return a copy of the language/locale priority list.
*/
public List getULocales() {
return new ArrayList(locales); // clone for safety
}
/**
* Convenience function for getting the locales in priority order
* @return first item.
*/
public ULocale getULocale(int i) {
return (ULocale)locales.get(i);
}
/**
* Convenience routine for setting the language/locale priority list from an array.
* @see #setULocales(List locales)
* @param uLocales list of locales in an array
* @return this, for chaining
*/
public GlobalizationPreferences setULocales(ULocale[] uLocales) {
return setULocales(Arrays.asList(uLocales));
}
/**
* Convenience routine for setting the language/locale priority list from a single locale/language.
* @see #setULocales(List locales)
* @param uLocale single locale
* @return this, for chaining
*/
public GlobalizationPreferences setULocales(ULocale uLocale) {
return setULocales(new ULocale[]{uLocale});
}
//#ifndef FOUNDATION
/**
* Convenience routine for setting the locale priority list from an Accept-Language string.
* @see #setULocales(List locales)
* @param acceptLanguageString Accept-Language list, as defined by Section 14.4 of the RFC 2616 (HTTP 1.1)
* @return this, for chaining
*/
public GlobalizationPreferences setULocales(String acceptLanguageString) {
/*
Accept-Language = "Accept-Language" ":" 1#( language-range [ ";" "q" "=" qvalue ] )
x matches x-...
*/
// reorders in quality order
// don't care that it is not very efficient right now
Matcher acceptMatcher = Pattern.compile("\\s*([-_a-zA-Z]+)(;q=([.0-9]+))?\\s*").matcher("");
Map reorder = new TreeMap();
String[] pieces = acceptLanguageString.split(",");
for (int i = 0; i < pieces.length; ++i) {
Double qValue = new Double(1);
try {
if (!acceptMatcher.reset(pieces[i]).matches()) {
throw new IllegalArgumentException();
}
String qValueString = acceptMatcher.group(3);
if (qValueString != null) qValue = new Double(Double.parseDouble(qValueString));
} catch (Exception e) {
throw new IllegalArgumentException("element '" + pieces[i] + "' is not of the form '<locale>{;q=<number>}");
}
List items = (List)reorder.get(qValue);
if (items == null) reorder.put(qValue, items = new LinkedList());
items.add(0, acceptMatcher.group(1)); // reverse order, will reverse again
}
// now read out in reverse order
List result = new ArrayList();
for (Iterator it = reorder.keySet().iterator(); it.hasNext();) {
Object key = it.next();
List items = (List)reorder.get(key);
for (Iterator it2 = items.iterator(); it2.hasNext();) {
result.add(0, new ULocale((String)it2.next()));
}
}
return setULocales(result);
}
//#endif
/**
* 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
*/
public GlobalizationPreferences setTerritory(String territory) {
this.territory = territory;
explicitTerritory = true;
if (!explicitCurrency) guessCurrency();
if (!explicitTimezone) guessTimeZone();
if (!explicitCalendar) guessCalendar();
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.
*/
public String getTerritory() {
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
*/
public GlobalizationPreferences setCurrency(Currency currency) {
this.currency = currency;
explicitCurrency = true;
return this;
}
/**
* Get a copy of the currency computed according to the settings.
* @return currency code, explicit or implicit.
*/
public Currency getCurrency() {
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
*/
public GlobalizationPreferences setCalendar(Calendar calendar) {
this.calendar = calendar;
explicitCalendar = true;
return this;
}
/**
* Get a copy of the calendar according to the settings.
* @return currency code, explicit or implicit.
*/
public Calendar getCalendar() {
return (Calendar) calendar.clone(); // clone for safety
}
/**
* Sets the timezone ID. If this has not been set, uses default for territory.
* @param timezone a valid TZID (see UTS#35).
* @return the object, for chaining.
*/
public GlobalizationPreferences setTimezone(TimeZone timezone) {
this.timezone = timezone;
explicitTimezone = true;
return this;
}
/**
* Get the timezone. It was either explicitly set, or is heuristically computed from other settings.
* @return timezone, either implicitly or explicitly set
*/
public TimeZone getTimezone() {
return (TimeZone) timezone.clone(); // clone for safety
}
/**
* Set the date locale.
* @param dateLocale If not null, overrides the locale priority list for all the date formats.
* @return the object, for chaining
*/
public GlobalizationPreferences setDateLocale(ULocale dateLocale) {
this.dateLocale = dateLocale;
return this;
}
/**
* Gets the date locale, to be used in computing date formats. Overrides the general locale setting.
* @return date locale. Null if none was set explicitly.
*/
public ULocale getDateLocale() {
return dateLocale;
}
/**
* Set the number locale.
* @param numberLocale If not null, overrides the locale priority list for all the date formats.
* @return the object, for chaining
*/
public GlobalizationPreferences setNumberLocale(ULocale numberLocale) {
this.numberLocale = numberLocale;
return this;
}
/**
* Get the current number locale setting used for getNumberFormat.
* @return number locale. Null if none was set explicitly.
*/
public ULocale getNumberLocale() {
return numberLocale;
}
/**
* 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: LANGUAGE, etc.
* @return the display name
*/
public String getDisplayName(String id, int type) {
String result = id;
for (Iterator it = locales.iterator(); it.hasNext();) {
ULocale locale = (ULocale) it.next();
switch (type) {
case LOCALEID:
result = ULocale.getDisplayName(id, locale);
break;
case LANGUAGEID:
result = ULocale.getDisplayLanguage(id, locale);
break;
case SCRIPTID:
result = ULocale.getDisplayScript("und-" + id, locale);
break;
case TERRITORYID:
result = ULocale.getDisplayCountry("und-" + id, locale);
break;
case VARIANTID:
// TODO fix variant parsing
result = ULocale.getDisplayVariant("und-QQ-" + id, locale);
break;
case KEYWORDID:
result = ULocale.getDisplayKeyword(id, locale);
break;
case KEYWORD_VALUEID:
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 CURRENCY_SYMBOLID:
case CURRENCYID:
Currency temp = new Currency(id);
result =temp.getName(locale, type==CURRENCYID ? Currency.LONG_NAME : Currency.SYMBOL_NAME, new boolean[1]);
// 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 TIMEZONEID:
SimpleDateFormat dtf = new SimpleDateFormat("vvvv",locale);
dtf.setTimeZone(TimeZone.getTimeZone(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
// note, compiling with FOUNDATION omits this check for now
//#ifndef FOUNDATION
if (badTimezone.reset(result).matches()) continue;
//#endif
break;
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
if (!id.equals(result)) return result;
// TODO need better way of seeing if we fell back to root!!
// This will not work at all for lots of stuff
}
return result;
}
//#ifndef FOUNDATION
// TODO remove need for this
private static final Matcher badTimezone = Pattern.compile("[A-Z]{2}|.*\\s\\([A-Z]{2}\\)").matcher("");
//#endif
/**
* Set an explicit date format. Overrides both the date locale, and the locale priority list
* for a particular combination of dateStyle and timeStyle. NONE should be used if for the style,
* where only the date or time format individually is being set.
* @param dateStyle
* @param timeStyle
* @param format
* @return this, for chaining
*/
public GlobalizationPreferences setDateFormat(int dateStyle, int timeStyle, DateFormat format) {
if (dateFormats == null) dateFormats = new DateFormat[NONE+1][NONE+1];
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, if there is a non-null date locale, that is used.
* Otherwise, the language priority list is used. NONE should be used for the style,
* where only the date or time format individually is being gotten.
* @param dateStyle
* @param timeStyle
* @return a DateFormat, according to the above description
*/
public DateFormat getDateFormat(int dateStyle, int timeStyle) {
try {
DateFormat result = null;
if (dateFormats != null) result = dateFormats[dateStyle][timeStyle];
if (result != null) {
result = (DateFormat) result.clone(); // clone for safety
result.setCalendar(calendar);
} else {
// In the case of date formats, we don't have to look at more than one
// locale. May be different for other cases
ULocale currentLocale = dateLocale != null ? dateLocale : (ULocale)locales.get(0);
// TODO Make this one function.
if (timeStyle == NONE) {
result = DateFormat.getDateInstance(calendar, dateStyle, currentLocale);
} else if (dateStyle == NONE) {
result = DateFormat.getTimeInstance(calendar, timeStyle, currentLocale);
} else {
result = DateFormat.getDateTimeInstance(calendar, dateStyle, timeStyle, currentLocale);
}
}
return result;
} catch (RuntimeException e) {
IllegalArgumentException ex = new IllegalArgumentException("Cannot create DateFormat");
//#ifndef FOUNDATION
ex.initCause(e);
//#endif
throw ex;
}
}
/**
* 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, if there is a non-null date locale, that is used.
* Otherwise, the language priority list is used. NONE should be used for the style,
* where only the date or time format individually is being gotten.
*/
public NumberFormat getNumberFormat(int style) {
try {
NumberFormat result = null;
if (numberFormats != null) result = numberFormats[style];
if (result != null) {
result = (NumberFormat) result.clone(); // clone for safety
if (style == CURRENCY) {
result.setCurrency(currency);
}
return result;
}
// In the case of date formats, we don't have to look at more than one
// locale. May be different for other cases
ULocale currentLocale = numberLocale != null ? numberLocale : (ULocale)locales.get(0);
switch (style) {
case NUMBER: return NumberFormat.getInstance(currentLocale);
case SCIENTIFIC: return NumberFormat.getScientificInstance(currentLocale);
case INTEGER: return NumberFormat.getIntegerInstance(currentLocale);
case PERCENT: return NumberFormat.getPercentInstance(currentLocale);
case CURRENCY: result = NumberFormat.getCurrencyInstance(currentLocale);
result.setCurrency(currency);
return result;
}
} catch (RuntimeException e) {}
throw new IllegalArgumentException(); // fix later
}
/**
* Sets a number format explicitly. Overrides the number locale and the general locale settings.
*/
public GlobalizationPreferences setNumberFormat(int style, DateFormat format) {
if (numberFormats == null) numberFormats = new NumberFormat[NUMBER_LIMIT];
numberFormats[style] = (NumberFormat) format.clone(); // for safety
return this;
}
/**
* Restore the object to the initial state.
* @return the object, for chaining
*/
public GlobalizationPreferences clear() {
explicitLocales = explicitTerritory = explicitCurrency = explicitTimezone = explicitCalendar = false;
locales.add(ULocale.getDefault());
if (!explicitTerritory) guessTerritory();
if (!explicitCurrency) guessCurrency();
if (!explicitTimezone) guessTimeZone();
if (!explicitCalendar) guessCalendar();
return this;
}
// protected helper functions
protected void guessTerritory() {
// pass through locales to see if there is a territory.
for (Iterator it = locales.iterator(); it.hasNext();) {
ULocale locale = (ULocale)it.next();
String temp = locale.getCountry();
if (temp.length() != 0) {
territory = temp;
return;
}
}
// 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 = (ULocale)locales.iterator().next();
String language = firstLocale.getLanguage();
String script = firstLocale.getScript();
territory = null;
if (script.length() != 0) {
territory = (String) language_territory_hack_map.get(language + "_" + script);
}
if (territory == null) territory = (String) language_territory_hack_map.get(language);
if (territory == null) territory = "US"; // need *some* default
}
protected void guessCurrency() {
currency = Currency.getInstance(new ULocale("und-" + territory));
}
protected void 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 = (String) territory_tzid_hack_map.get(territory);
if (timezoneString == null) {
String[] attempt = ZoneMeta.getAvailableIDs(territory);
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];
}
}
timezone = TimeZone.getTimeZone(timezoneString);
}
protected void guessCalendar() {
// TODO add better API
calendar = Calendar.getInstance(new ULocale("und-" + territory));
}
// PRIVATES
private ArrayList locales = new ArrayList();
private String territory;
private Currency currency;
private TimeZone timezone;
private Calendar calendar;
private boolean explicitLocales;
private boolean explicitTerritory;
private boolean explicitCurrency;
private boolean explicitTimezone;
private boolean explicitCalendar;
private ULocale dateLocale;
private DateFormat[][] dateFormats;
private ULocale numberLocale;
private NumberFormat[] numberFormats;
{
clear();
}
//
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]);
}
}
}