| /* |
| * ********************************************************************* |
| * Copyright (c) 2002-2004, International Business Machines Corporation and others. All Rights Reserved. |
| * ********************************************************************* |
| * Author: Mark Davis |
| * ********************************************************************* |
| */ |
| |
| package org.unicode.cldr.util; |
| |
| import com.ibm.icu.text.DateFormat; |
| import com.ibm.icu.text.MessageFormat; |
| import com.ibm.icu.text.SimpleDateFormat; |
| import com.ibm.icu.text.UFormat; |
| import com.ibm.icu.util.BasicTimeZone; |
| import com.ibm.icu.util.Calendar; |
| import com.ibm.icu.util.CurrencyAmount; |
| import com.ibm.icu.util.TimeZone; |
| import com.ibm.icu.util.TimeZoneTransition; |
| import java.text.FieldPosition; |
| import java.text.ParsePosition; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.regex.Matcher; |
| import org.unicode.cldr.tool.LikelySubtags; |
| import org.unicode.cldr.util.CLDRFile.DraftStatus; |
| import org.unicode.cldr.util.SupplementalDataInfo.MetaZoneRange; |
| |
| /** |
| * TimezoneFormatter. Class that uses CLDR data directly to parse / format timezone names according |
| * to the specification in TR#35. Note: there are some areas where the spec needs fixing. |
| * |
| * @author davis |
| */ |
| public class TimezoneFormatter extends UFormat { |
| |
| /** */ |
| private static final long serialVersionUID = -506645087792499122L; |
| |
| private static final long TIME = new Date().getTime(); |
| public static boolean SHOW_DRAFT = false; |
| |
| public enum Location { |
| GMT, |
| LOCATION, |
| NON_LOCATION; |
| |
| @Override |
| public String toString() { |
| return this == GMT ? "gmt" : this == LOCATION ? "location" : "non-location"; |
| } |
| } |
| |
| public enum Type { |
| GENERIC, |
| SPECIFIC; |
| |
| public String toString(boolean daylight) { |
| return this == GENERIC ? "generic" : daylight ? "daylight" : "standard"; |
| } |
| |
| @Override |
| public String toString() { |
| return name().toLowerCase(Locale.ENGLISH); |
| } |
| } |
| |
| public enum Length { |
| SHORT, |
| LONG, |
| OTHER; |
| |
| @Override |
| public String toString() { |
| return this == SHORT ? "short" : this == LONG ? "long" : "other"; |
| } |
| } |
| |
| public enum Format { |
| VVVV(Type.GENERIC, Location.LOCATION, Length.OTHER), |
| vvvv(Type.GENERIC, Location.NON_LOCATION, Length.LONG), |
| v(Type.GENERIC, Location.NON_LOCATION, Length.SHORT), |
| zzzz(Type.SPECIFIC, Location.NON_LOCATION, Length.LONG), |
| z(Type.SPECIFIC, Location.NON_LOCATION, Length.SHORT), |
| ZZZZ(Type.GENERIC, Location.GMT, Length.LONG), |
| Z(Type.GENERIC, Location.GMT, Length.SHORT), |
| ZZZZZ(Type.GENERIC, Location.GMT, Length.OTHER); |
| final Type type; |
| final Location location; |
| final Length length; |
| |
| private Format(Type type, Location location, Length length) { |
| this.type = type; |
| this.location = location; |
| this.length = length; |
| } |
| } |
| |
| // /** |
| // * Type parameter for formatting |
| // */ |
| // public static final int GMT = 0, GENERIC = 1, STANDARD = 2, DAYLIGHT = 3, TYPE_LIMIT = 4; |
| // |
| // /** |
| // * Arrays of names, for testing. Should be const, but we can't do that in Java |
| // */ |
| // public static final List LENGTH = Arrays.asList(new String[] {"short", "long"}); |
| // public static final List TYPE = Arrays.asList(new String[] {"gmt", "generic", "standard", |
| // "daylight"}); |
| |
| // static fields built from Timezone Database for formatting and parsing |
| |
| // private static final Map zone_countries = StandardCodes.make().getZoneToCounty(); |
| // private static final Map countries_zoneSet = StandardCodes.make().getCountryToZoneSet(); |
| // private static final Map old_new = StandardCodes.make().getZoneLinkold_new(); |
| |
| private static SupplementalDataInfo sdi = SupplementalDataInfo.getInstance(); |
| |
| // instance fields built from CLDR data for formatting and parsing |
| |
| private transient SimpleDateFormat hourFormatPlus = new SimpleDateFormat(); |
| private transient SimpleDateFormat hourFormatMinus = new SimpleDateFormat(); |
| private transient MessageFormat gmtFormat, |
| regionFormat, |
| regionFormatStandard, |
| regionFormatDaylight, |
| fallbackFormat; |
| // private transient String abbreviationFallback, preferenceOrdering; |
| private transient Set<String> singleCountriesSet; |
| |
| // private for computation |
| private transient Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); |
| private transient SimpleDateFormat rfc822Plus = new SimpleDateFormat("+HHmm"); |
| private transient SimpleDateFormat rfc822Minus = new SimpleDateFormat("-HHmm"); |
| |
| { |
| TimeZone gmt = TimeZone.getTimeZone("GMT"); |
| rfc822Plus.setTimeZone(gmt); |
| rfc822Minus.setTimeZone(gmt); |
| } |
| |
| // input parameters |
| private CLDRFile desiredLocaleFile; |
| private String inputLocaleID; |
| private boolean skipDraft; |
| |
| public TimezoneFormatter(Factory cldrFactory, String localeID, boolean includeDraft) { |
| this(cldrFactory.make(localeID, true, includeDraft)); |
| } |
| |
| public TimezoneFormatter(Factory cldrFactory, String localeID, DraftStatus minimalDraftStatus) { |
| this(cldrFactory.make(localeID, true, minimalDraftStatus)); |
| } |
| |
| /** |
| * Create from a cldrFactory and a locale id. |
| * |
| * @see CLDRFile |
| */ |
| public TimezoneFormatter(CLDRFile resolvedLocaleFile) { |
| desiredLocaleFile = resolvedLocaleFile; |
| inputLocaleID = desiredLocaleFile.getLocaleID(); |
| String hourFormatString = getStringValue("//ldml/dates/timeZoneNames/hourFormat"); |
| String[] hourFormatStrings = CldrUtility.splitArray(hourFormatString, ';'); |
| ICUServiceBuilder icuServiceBuilder = |
| new ICUServiceBuilder().setCldrFile(desiredLocaleFile); |
| hourFormatPlus = icuServiceBuilder.getDateFormat("gregorian", 0, 1); |
| hourFormatPlus.applyPattern(hourFormatStrings[0]); |
| hourFormatMinus = icuServiceBuilder.getDateFormat("gregorian", 0, 1); |
| hourFormatMinus.applyPattern(hourFormatStrings[1]); |
| gmtFormat = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/gmtFormat")); |
| regionFormat = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/regionFormat")); |
| regionFormatStandard = |
| new MessageFormat( |
| getStringValue( |
| "//ldml/dates/timeZoneNames/regionFormat[@type=\"standard\"]")); |
| regionFormatDaylight = |
| new MessageFormat( |
| getStringValue( |
| "//ldml/dates/timeZoneNames/regionFormat[@type=\"daylight\"]")); |
| fallbackFormat = |
| new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/fallbackFormat")); |
| checkForDraft("//ldml/dates/timeZoneNames/singleCountries"); |
| // default value if not in root. Only needed for CLDR 1.3 |
| String singleCountriesList = |
| "Africa/Bamako America/Godthab America/Santiago America/Guayaquil" |
| + " Asia/Shanghai Asia/Tashkent Asia/Kuala_Lumpur Europe/Madrid Europe/Lisbon" |
| + " Europe/London Pacific/Auckland Pacific/Tahiti"; |
| String temp = desiredLocaleFile.getFullXPath("//ldml/dates/timeZoneNames/singleCountries"); |
| if (temp != null) { |
| XPathParts xpp = XPathParts.getFrozenInstance(temp); |
| temp = xpp.findAttributeValue("singleCountries", "list"); |
| if (temp != null) { |
| singleCountriesList = temp; |
| } |
| } |
| singleCountriesSet = new TreeSet<>(CldrUtility.splitList(singleCountriesList, ' ')); |
| } |
| |
| /** */ |
| private String getStringValue(String cleanPath) { |
| checkForDraft(cleanPath); |
| return desiredLocaleFile.getWinningValue(cleanPath); |
| } |
| |
| private String getName(int territory_name, String country, boolean skipDraft2) { |
| checkForDraft(CLDRFile.getKey(territory_name, country)); |
| return desiredLocaleFile.getName(territory_name, country); |
| } |
| |
| private void checkForDraft(String cleanPath) { |
| String xpath = desiredLocaleFile.getFullXPath(cleanPath); |
| |
| if (SHOW_DRAFT && xpath != null && xpath.indexOf("[@draft=\"true\"]") >= 0) { |
| System.out.println("Draft in " + inputLocaleID + ":\t" + cleanPath); |
| } |
| } |
| |
| /** Formatting based on pattern and date. */ |
| public String getFormattedZone(String zoneid, String pattern, long date) { |
| Format format = Format.valueOf(pattern); |
| return getFormattedZone(zoneid, format.location, format.type, format.length, date); |
| } |
| |
| /** Formatting based on broken out features and date. */ |
| public String getFormattedZone( |
| String inputZoneid, Location location, Type type, Length length, long date) { |
| String zoneid = TimeZone.getCanonicalID(inputZoneid); |
| BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(zoneid); |
| int gmtOffset1 = timeZone.getOffset(date); |
| MetaZoneRange metaZoneRange = sdi.getMetaZoneRange(zoneid, date); |
| String metazone = metaZoneRange == null ? "?" : metaZoneRange.metazone; |
| boolean noTimezoneChangeWithin184Days = noTimezoneChangeWithin184Days(timeZone, date); |
| boolean daylight = gmtOffset1 != timeZone.getRawOffset(); |
| return getFormattedZone( |
| inputZoneid, |
| location, |
| type, |
| length, |
| daylight, |
| gmtOffset1, |
| metazone, |
| noTimezoneChangeWithin184Days); |
| } |
| |
| /** |
| * Low-level routine for formatting based on zone, broken-out features, plus special settings |
| * (which are usually computed from the date, but are here for specific access.) |
| * |
| * @param inputZoneid |
| * @param location |
| * @param type |
| * @param length |
| * @param daylight |
| * @param gmtOffset1 |
| * @param metazone |
| * @param noTimezoneChangeWithin184Days |
| * @return |
| */ |
| public String getFormattedZone( |
| String inputZoneid, |
| Location location, |
| Type type, |
| Length length, |
| boolean daylight, |
| int gmtOffset1, |
| String metazone, |
| boolean noTimezoneChangeWithin184Days) { |
| String formatted = |
| getFormattedZoneInternal( |
| inputZoneid, |
| location, |
| type, |
| length, |
| daylight, |
| gmtOffset1, |
| metazone, |
| noTimezoneChangeWithin184Days); |
| if (formatted != null) { |
| return formatted; |
| } |
| if (type == Type.GENERIC && location == Location.NON_LOCATION) { |
| formatted = |
| getFormattedZone( |
| inputZoneid, |
| Location.LOCATION, |
| type, |
| length, |
| daylight, |
| gmtOffset1, |
| metazone, |
| noTimezoneChangeWithin184Days); |
| if (formatted != null) { |
| return formatted; |
| } |
| } |
| return getFormattedZone( |
| inputZoneid, |
| Location.GMT, |
| null, |
| Length.LONG, |
| daylight, |
| gmtOffset1, |
| metazone, |
| noTimezoneChangeWithin184Days); |
| } |
| |
| private String getFormattedZoneInternal( |
| String inputZoneid, |
| Location location, |
| Type type, |
| Length length, |
| boolean daylight, |
| int gmtOffset1, |
| String metazone, |
| boolean noTimezoneChangeWithin184Days) { |
| |
| String result; |
| // 1. Canonicalize the Olson ID according to the table in supplemental data. |
| // Use that canonical ID in each of the following steps. |
| // * America/Atka => America/Adak |
| // * Australia/ACT => Australia/Sydney |
| |
| String zoneid = TimeZone.getCanonicalID(inputZoneid); |
| // BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(zoneid); |
| // if (zoneid == null) zoneid = inputZoneid; |
| |
| switch (location) { |
| default: |
| throw new IllegalArgumentException("Bad enum value for location: " + location); |
| |
| case GMT: |
| // 2. For RFC 822 GMT format ("Z") return the results according to the RFC. |
| // America/Los_Angeles → "-0800" |
| // Note: The digits in this case are always from the western digits, 0..9. |
| if (length == Length.SHORT) { |
| return gmtOffset1 < 0 |
| ? rfc822Minus.format(new Date(-gmtOffset1)) |
| : rfc822Plus.format(new Date(gmtOffset1)); |
| } |
| |
| // 3. For the localized GMT format, use the gmtFormat (such as "GMT{0}" or "HMG{0}") |
| // with the hourFormat |
| // (such as "+HH:mm;-HH:mm" or "+HH.mm;-HH.mm"). |
| // America/Los_Angeles → "GMT-08:00" // standard time |
| // America/Los_Angeles → "HMG-07:00" // daylight time |
| // Etc/GMT+3 → "GMT-03.00" // note that TZ tzids have inverse polarity! |
| // Note: The digits should be whatever are appropriate for the locale used to format |
| // the time zone, not |
| // necessarily from the western digits, 0..9. For example, they might be from ०..९. |
| |
| DateFormat format = gmtOffset1 < 0 ? hourFormatMinus : hourFormatPlus; |
| calendar.setTimeInMillis(Math.abs(gmtOffset1)); |
| result = format.format(calendar); |
| return gmtFormat.format(new Object[] {result}); |
| // 4. For ISO 8601 time zone format ("ZZZZZ") return the results according to the |
| // ISO 8601. |
| // America/Los_Angeles → "-08:00" |
| // Etc/GMT → Z // special case of UTC |
| // Note: The digits in this case are always from the western digits, 0..9. |
| |
| // TODO |
| case NON_LOCATION: |
| // 5. For the non-location formats (generic or specific), |
| // 5.1 if there is an explicit translation for the TZID in timeZoneNames according |
| // to type (generic, |
| // standard, or daylight) in the resolved locale, return it. |
| // America/Los_Angeles → "Heure du Pacifique (ÉUA)" // generic |
| // America/Los_Angeles → 太平洋標準時 // standard |
| // America/Los_Angeles → Yhdysvaltain Tyynenmeren kesäaika // daylight |
| // Europe/Dublin → Am Samhraidh na hÉireann // daylight |
| // Note: This translation may not at all be literal: it would be what is most |
| // recognizable for people using |
| // the target language. |
| |
| String formatValue = getLocalizedExplicitTzid(zoneid, type, length, daylight); |
| if (formatValue != null) { |
| return formatValue; |
| } |
| |
| // 5.2 Otherwise, if there is a metazone standard format, |
| // and the offset and daylight offset do not change within 184 day +/- interval |
| // around the exact formatted time, use the metazone standard format ("Mountain |
| // Standard Time" for Phoenix). |
| // (184 is the smallest number that is at least 6 months AND the smallest number |
| // that is more than 1/2 year |
| // (Gregorian)). |
| if (metazone == null) { |
| metazone = sdi.getMetaZoneRange(zoneid, TIME).metazone; |
| } |
| String metaZoneName = getLocalizedMetazone(metazone, type, length, daylight); |
| if (metaZoneName == null && noTimezoneChangeWithin184Days) { |
| metaZoneName = getLocalizedMetazone(metazone, Type.SPECIFIC, length, false); |
| } |
| |
| // 5.3 Otherwise, if there is a metazone generic format, then do the following: |
| // *** CHANGE to |
| // 5.2 Get the appropriate metazone format (generic, standard, daylight). |
| // if there is none, (do old 5.2). |
| // if there is either one, then do the following |
| |
| if (metaZoneName != null) { |
| |
| // 5.3.1 Compare offset at the requested time with the preferred zone for the |
| // current locale; if same, |
| // we use the metazone generic format. |
| // "Pacific Time" for Vancouver if the locale is en-CA, or for Los Angeles if |
| // locale is en-US. Note that |
| // the fallback is the golden zone. |
| // The metazone data actually supplies the preferred zone for a country. |
| String localeId = desiredLocaleFile.getLocaleID(); |
| LanguageTagParser languageTagParser = new LanguageTagParser(); |
| String defaultRegion = languageTagParser.set(localeId).getRegion(); |
| // If the locale does not have a country the likelySubtags supplemental data is |
| // used to get the most |
| // likely country. |
| if (defaultRegion.isEmpty()) { |
| String localeMax = LikelySubtags.maximize(localeId, sdi.getLikelySubtags()); |
| defaultRegion = languageTagParser.set(localeMax).getRegion(); |
| if (defaultRegion.isEmpty()) { |
| return "001"; // CLARIFY |
| } |
| } |
| Map<String, String> regionToZone = |
| sdi.getMetazoneToRegionToZone().get(metazone); |
| String preferredLocalesZone = regionToZone.get(defaultRegion); |
| if (preferredLocalesZone == null) { |
| preferredLocalesZone = regionToZone.get("001"); |
| } |
| // TimeZone preferredTimeZone = TimeZone.getTimeZone(preferredZone); |
| // CLARIFY: do we mean that the offset is the same at the current time, or that |
| // the zone is the same??? |
| // the following code does the latter. |
| if (zoneid.equals(preferredLocalesZone)) { |
| return metaZoneName; |
| } |
| |
| // 5.3.2 If the zone is the preferred zone for its country but not for the |
| // country of the locale, use |
| // the metazone generic format + (country) |
| // [Generic partial location] "Pacific Time (Canada)" for the zone Vancouver in |
| // the locale en_MX. |
| |
| String zoneIdsCountry = TimeZone.getRegion(zoneid); |
| String preferredZonesCountrysZone = regionToZone.get(zoneIdsCountry); |
| if (preferredZonesCountrysZone == null) { |
| preferredZonesCountrysZone = regionToZone.get("001"); |
| } |
| if (zoneid.equals(preferredZonesCountrysZone)) { |
| String countryName = getLocalizedCountryName(zoneIdsCountry); |
| return fallbackFormat.format( |
| new Object[] { |
| countryName, metaZoneName |
| }); // UGLY, should be able to |
| // just list |
| } |
| |
| // If all else fails, use metazone generic format + (city). |
| // [Generic partial location]: "Mountain Time (Phoenix)", "Pacific Time |
| // (Whitehorse)" |
| String cityName = getLocalizedExemplarCity(zoneid); |
| return fallbackFormat.format(new Object[] {cityName, metaZoneName}); |
| } |
| // |
| // Otherwise, fall back. |
| // Note: In composing the metazone + city or country: use the fallbackFormat |
| // |
| // {1} will be the metazone |
| // {0} will be a qualifier (city or country) |
| // Example: Pacific Time (Phoenix) |
| |
| if (length == Length.LONG) { |
| return getRegionFallback( |
| zoneid, |
| type == Type.GENERIC || noTimezoneChangeWithin184Days |
| ? regionFormat |
| : daylight ? regionFormatDaylight : regionFormatStandard); |
| } |
| return null; |
| |
| case LOCATION: |
| |
| // 6.1 For the generic location format: |
| return getRegionFallback(zoneid, regionFormat); |
| |
| // FIX examples |
| // Otherwise, get both the exemplar city and country name. Format them with the |
| // fallbackRegionFormat (for |
| // example, "{1} Time ({0})". For example: |
| // America/Buenos_Aires → "Argentina Time (Buenos Aires)" |
| // // if the fallbackRegionFormat is "{1} Time ({0})". |
| // America/Buenos_Aires → "Аргентина (Буэнос-Айрес)" |
| // // if both are translated, and the fallbackRegionFormat is "{1} ({0})". |
| // America/Buenos_Aires → "AR (Буэнос-Айрес)" |
| // // if Argentina is not translated. |
| // America/Buenos_Aires → "Аргентина (Buenos Aires)" |
| // // if Buenos Aires is not translated. |
| // America/Buenos_Aires → "AR (Buenos Aires)" |
| // // if both are not translated. |
| // Note: As with the regionFormat, exceptional cases need to be explicitly |
| // translated. |
| } |
| } |
| |
| private String getRegionFallback(String zoneid, MessageFormat regionFallbackFormat) { |
| // Use as the country name, the explicitly localized country if available, otherwise the raw |
| // country code. |
| // If the localized exemplar city is not available, use as the exemplar city the last field |
| // of the raw TZID, |
| // stripping off the prefix and turning _ into space. |
| // CU → "CU" // no localized country name for Cuba |
| |
| // CLARIFY that above applies to 5.3.2 also! |
| |
| // America/Los_Angeles → "Los Angeles" // no localized exemplar city |
| // From <timezoneData> get the country code for the zone, and determine whether there is |
| // only one timezone |
| // in the country. |
| // If there is only one timezone or the zone id is in the singleCountries list, |
| // format the country name with the regionFormat (for example, "{0} Time"), and return it. |
| // Europe/Rome → IT → Italy Time // for English |
| // Africa/Monrovia → LR → "Hora de Liberja" |
| // America/Havana → CU → "Hora de CU" // if CU is not localized |
| // Note: If a language does require grammatical changes when composing strings, then it |
| // should either use a |
| // neutral format such as what is in root, or put all exceptional cases in explicitly |
| // translated strings. |
| // |
| |
| // Note: <timezoneData> may not have data for new TZIDs. |
| // |
| // If the country for the zone cannot be resolved, format the exemplar city |
| // (it is unlikely that the localized exemplar city is available in this case, |
| // so the exemplar city might be composed by the last field of the raw TZID as described |
| // above) |
| // with the regionFormat (for example, "{0} Time"), and return it. |
| // ***FIX by changing to: if the country can't be resolved, or the zonesInRegion are not |
| // unique |
| |
| String zoneIdsCountry = TimeZone.getRegion(zoneid); |
| if (zoneIdsCountry != null) { |
| String[] zonesInRegion = TimeZone.getAvailableIDs(zoneIdsCountry); |
| if (zonesInRegion != null && zonesInRegion.length == 1 |
| || singleCountriesSet.contains(zoneid)) { |
| String countryName = getLocalizedCountryName(zoneIdsCountry); |
| return regionFallbackFormat.format(new Object[] {countryName}); |
| } |
| } |
| String cityName = getLocalizedExemplarCity(zoneid); |
| return regionFallbackFormat.format(new Object[] {cityName}); |
| } |
| |
| public boolean noTimezoneChangeWithin184Days(BasicTimeZone timeZone, long date) { |
| // TODO Fix this to look at the real times |
| TimeZoneTransition startTransition = timeZone.getPreviousTransition(date, true); |
| if (startTransition == null) { |
| // System.out.println("No transition for " + timeZone.getID() + " on " + new |
| // Date(date)); |
| return true; |
| } |
| if (!atLeast184Days(startTransition.getTime(), date)) { |
| return false; |
| } else { |
| TimeZoneTransition nextTransition = timeZone.getNextTransition(date, false); |
| if (nextTransition != null && !atLeast184Days(date, nextTransition.getTime())) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean atLeast184Days(long start, long end) { |
| long transitionDays = (end - start) / (24 * 60 * 60 * 1000); |
| return transitionDays >= 184; |
| } |
| |
| private String getLocalizedExplicitTzid( |
| String zoneid, Type type, Length length, boolean daylight) { |
| String formatValue = |
| desiredLocaleFile.getWinningValue( |
| "//ldml/dates/timeZoneNames/zone[@type=\"" |
| + zoneid |
| + "\"]/" |
| + length.toString() |
| + "/" |
| + type.toString(daylight)); |
| return formatValue; |
| } |
| |
| public String getLocalizedMetazone( |
| String metazone, Type type, Length length, boolean daylight) { |
| if (metazone == null) { |
| return null; |
| } |
| String name = |
| desiredLocaleFile.getWinningValue( |
| "//ldml/dates/timeZoneNames/metazone[@type=\"" |
| + metazone |
| + "\"]/" |
| + length.toString() |
| + "/" |
| + type.toString(daylight)); |
| return name; |
| } |
| |
| private String getLocalizedCountryName(String zoneIdsCountry) { |
| String countryName = desiredLocaleFile.getName(CLDRFile.TERRITORY_NAME, zoneIdsCountry); |
| if (countryName == null) { |
| countryName = zoneIdsCountry; |
| } |
| return countryName; |
| } |
| |
| public String getLocalizedExemplarCity(String timezoneString) { |
| String exemplarCity = |
| desiredLocaleFile.getWinningValue( |
| "//ldml/dates/timeZoneNames/zone[@type=\"" |
| + timezoneString |
| + "\"]/exemplarCity"); |
| if (exemplarCity == null) { |
| exemplarCity = |
| timezoneString.substring(timezoneString.lastIndexOf('/') + 1).replace('_', ' '); |
| } |
| return exemplarCity; |
| } |
| |
| /** Used for computation in parsing */ |
| private static final int WALL_LIMIT = 2, STANDARD_LIMIT = 4; |
| |
| private static final String[] zoneTypes = { |
| "\"]/long/generic", |
| "\"]/short/generic", |
| "\"]/long/standard", |
| "\"]/short/standard", |
| "\"]/long/daylight", |
| "\"]/short/daylight" |
| }; |
| |
| private transient Matcher m = PatternCache.get("([-+])([0-9][0-9])([0-9][0-9])").matcher(""); |
| |
| private transient boolean parseInfoBuilt; |
| private final transient Map<String, String> localizedCountry_countryCode = new HashMap<>(); |
| private final transient Map<String, String> exemplar_zone = new HashMap<>(); |
| private final transient Map<Object, Object> localizedExplicit_zone = new HashMap<>(); |
| private final transient Map<String, String> country_zone = new HashMap<>(); |
| |
| /** |
| * Returns zoneid. In case of an offset, returns "Etc/GMT+/-HH" or "Etc/GMT+/-HHmm". Remember |
| * that Olson IDs have reversed signs! |
| */ |
| public String parse(String inputText, ParsePosition parsePosition) { |
| long[] offsetMillisOutput = new long[1]; |
| String result = parse(inputText, parsePosition, offsetMillisOutput); |
| if (result == null || result.length() != 0) return result; |
| long offsetMillis = offsetMillisOutput[0]; |
| String sign = "Etc/GMT-"; |
| if (offsetMillis < 0) { |
| offsetMillis = -offsetMillis; |
| sign = "Etc/GMT+"; |
| } |
| long minutes = (offsetMillis + 30 * 1000) / (60 * 1000); |
| long hours = minutes / 60; |
| minutes = minutes % 60; |
| result = sign + String.valueOf(hours); |
| if (minutes != 0) result += ":" + String.valueOf(100 + minutes).substring(1, 3); |
| return result; |
| } |
| |
| /** |
| * Returns zoneid, or if a gmt offset, returns "" and a millis value in offsetMillis[0]. If we |
| * can't parse, return null |
| */ |
| public String parse(String inputText, ParsePosition parsePosition, long[] offsetMillis) { |
| // if we haven't parsed before, build parsing info |
| if (!parseInfoBuilt) buildParsingInfo(); |
| int startOffset = parsePosition.getIndex(); |
| // there are the following possible formats |
| |
| // Explicit strings |
| // If the result is a Long it is millis, otherwise it is the zoneID |
| Object result = localizedExplicit_zone.get(inputText); |
| if (result != null) { |
| if (result instanceof String) return (String) result; |
| offsetMillis[0] = ((Long) result).longValue(); |
| return ""; |
| } |
| |
| // RFC 822 |
| if (m.reset(inputText).matches()) { |
| int hours = Integer.parseInt(m.group(2)); |
| int minutes = Integer.parseInt(m.group(3)); |
| int millis = hours * 60 * 60 * 1000 + minutes * 60 * 1000; |
| if (m.group(1).equals("-")) millis = -millis; // check sign! |
| offsetMillis[0] = millis; |
| return ""; |
| } |
| |
| // GMT-style (also fallback for daylight/standard) |
| |
| Object[] results = gmtFormat.parse(inputText, parsePosition); |
| if (results != null) { |
| if (results.length == 0) { |
| // for debugging |
| results = gmtFormat.parse(inputText, parsePosition); |
| } |
| String hours = (String) results[0]; |
| parsePosition.setIndex(0); |
| Date date = hourFormatPlus.parse(hours, parsePosition); |
| if (date != null) { |
| offsetMillis[0] = date.getTime(); |
| return ""; |
| } |
| parsePosition.setIndex(0); |
| date = hourFormatMinus.parse(hours, parsePosition); // negative format |
| if (date != null) { |
| offsetMillis[0] = -date.getTime(); |
| return ""; |
| } |
| } |
| |
| // Generic fallback, example: city or city (country) |
| |
| // first remove the region format if possible |
| |
| parsePosition.setIndex(startOffset); |
| Object[] x = regionFormat.parse(inputText, parsePosition); |
| if (x != null) { |
| inputText = (String) x[0]; |
| } |
| |
| String city = null, country = null; |
| parsePosition.setIndex(startOffset); |
| x = fallbackFormat.parse(inputText, parsePosition); |
| if (x != null) { |
| city = (String) x[0]; |
| country = (String) x[1]; |
| // at this point, we don't really need the country, so ignore it |
| // the city could be the last field of a zone, or could be an exemplar city |
| // we have built the map so that both work |
| return exemplar_zone.get(city); |
| } |
| |
| // see if the string is a localized country |
| String countryCode = localizedCountry_countryCode.get(inputText); |
| if (countryCode == null) countryCode = country; // if not, try raw code |
| return country_zone.get(countryCode); |
| } |
| |
| /** Internal method. Builds parsing tables. */ |
| private void buildParsingInfo() { |
| // TODO Auto-generated method stub |
| |
| // Exemplar cities (plus constructed ones) |
| // and add all the last fields. |
| |
| // // do old ones first, we don't care if they are overriden |
| // for (Iterator it = old_new.keySet().iterator(); it.hasNext();) { |
| // String zoneid = (String) it.next(); |
| // exemplar_zone.put(getFallbackName(zoneid), zoneid); |
| // } |
| |
| // then canonical ones |
| for (String zoneid : TimeZone.getAvailableIDs()) { |
| exemplar_zone.put(getFallbackName(zoneid), zoneid); |
| } |
| |
| // now add exemplar cities, AND pick up explicit strings, AND localized countries |
| String prefix = "//ldml/dates/timeZoneNames/zone[@type=\""; |
| String countryPrefix = "//ldml/localeDisplayNames/territories/territory[@type=\""; |
| Map<String, Comparable> localizedNonWall = new HashMap<>(); |
| Set<String> skipDuplicates = new HashSet<>(); |
| for (Iterator<String> it = desiredLocaleFile.iterator(); it.hasNext(); ) { |
| String path = it.next(); |
| // dumb, simple implementation |
| if (path.startsWith(prefix)) { |
| String zoneId = matchesPart(path, prefix, "\"]/exemplarCity"); |
| if (zoneId != null) { |
| String name = desiredLocaleFile.getWinningValue(path); |
| if (name != null) exemplar_zone.put(name, zoneId); |
| } |
| for (int i = 0; i < zoneTypes.length; ++i) { |
| zoneId = matchesPart(path, prefix, zoneTypes[i]); |
| if (zoneId != null) { |
| String name = desiredLocaleFile.getWinningValue(path); |
| if (name == null) continue; |
| if (i < WALL_LIMIT) { // wall time |
| localizedExplicit_zone.put(name, zoneId); |
| } else { |
| // TODO: if a daylight or standard string is ambiguous, return GMT!! |
| Object dup = localizedNonWall.get(name); |
| if (dup != null) { |
| skipDuplicates.add(name); |
| // TODO: use Etc/GMT... localizedNonWall.remove(name); |
| TimeZone tz = TimeZone.getTimeZone(zoneId); |
| int offset = tz.getRawOffset(); |
| if (i >= STANDARD_LIMIT) { |
| offset += tz.getDSTSavings(); |
| } |
| localizedNonWall.put(name, (long) offset); |
| } else { |
| localizedNonWall.put(name, zoneId); |
| } |
| } |
| } |
| } |
| } else { |
| // now do localizedCountry_countryCode |
| String countryCode = matchesPart(path, countryPrefix, "\"]"); |
| if (countryCode != null) { |
| String name = desiredLocaleFile.getStringValue(path); |
| if (name != null) localizedCountry_countryCode.put(name, countryCode); |
| } |
| } |
| } |
| // add to main set |
| for (Iterator<String> it = localizedNonWall.keySet().iterator(); it.hasNext(); ) { |
| String key = it.next(); |
| Object value = localizedNonWall.get(key); |
| localizedExplicit_zone.put(key, value); |
| } |
| // now build country_zone. Could check each time for the singleCountries list, but this is |
| // simpler |
| for (String key : StandardCodes.make().getGoodAvailableCodes("territory")) { |
| String[] tzids = TimeZone.getAvailableIDs(key); |
| if (tzids == null || tzids.length == 0) continue; |
| // only use if there is a single element OR there is a singleCountrySet element |
| if (tzids.length == 1) { |
| country_zone.put(key, tzids[0]); |
| } else { |
| Set<String> set = new LinkedHashSet<>(Arrays.asList(tzids)); // make modifyable |
| set.retainAll(singleCountriesSet); |
| if (set.size() == 1) { |
| country_zone.put(key, set.iterator().next()); |
| } |
| } |
| } |
| parseInfoBuilt = true; |
| } |
| |
| /** Internal method for simple building tables */ |
| private String matchesPart(String input, String prefix, String suffix) { |
| if (!input.startsWith(prefix)) return null; |
| if (!input.endsWith(suffix)) return null; |
| return input.substring(prefix.length(), input.length() - suffix.length()); |
| } |
| |
| /** Returns the name for a timezone id that will be returned as a fallback. */ |
| public static String getFallbackName(String zoneid) { |
| String result; |
| int pos = zoneid.lastIndexOf('/'); |
| result = pos < 0 ? zoneid : zoneid.substring(pos + 1); |
| result = result.replace('_', ' '); |
| return result; |
| } |
| |
| /** Getter */ |
| public boolean isSkipDraft() { |
| return skipDraft; |
| } |
| |
| /** Setter */ |
| public TimezoneFormatter setSkipDraft(boolean skipDraft) { |
| this.skipDraft = skipDraft; |
| return this; |
| } |
| |
| @Override |
| public Object parseObject(String source, ParsePosition pos) { |
| TimeZone foo; |
| CurrencyAmount fii; |
| com.ibm.icu.text.UnicodeSet fuu; |
| return null; |
| } |
| |
| @Override |
| public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { |
| // TODO Auto-generated method stub |
| return null; |
| } |
| |
| // The following are just for compatibility, until some fixes are made. |
| |
| public static final List<String> LENGTH = |
| Arrays.asList(Length.SHORT.toString(), Length.LONG.toString()); |
| public static final int LENGTH_LIMIT = LENGTH.size(); |
| public static final int TYPE_LIMIT = Type.values().length; |
| |
| public String getFormattedZone( |
| String zoneId, String pattern, boolean daylight, int offset, boolean b) { |
| Format format = Format.valueOf(pattern); |
| return getFormattedZone( |
| zoneId, format.location, format.type, format.length, daylight, offset, null, false); |
| } |
| |
| public String getFormattedZone(String zoneId, int length, int type, int offset, boolean b) { |
| return getFormattedZone( |
| zoneId, |
| Location.LOCATION, |
| Type.values()[type], |
| Length.values()[length], |
| false, |
| offset, |
| null, |
| true); |
| } |
| |
| public String getFormattedZone(String zoneId, String pattern, long time, boolean b) { |
| return getFormattedZone(zoneId, pattern, time); |
| } |
| |
| // end compat |
| |
| } |