package org.unicode.cldr.util; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.regex.Matcher; import org.unicode.cldr.draft.FileUtilities; import org.unicode.cldr.tool.Option; import org.unicode.cldr.tool.Option.Options; import org.unicode.cldr.tool.TablePrinter; import org.unicode.cldr.util.SupplementalDataInfo.DateRange; import org.unicode.cldr.util.SupplementalDataInfo.MetaZoneRange; import org.unicode.cldr.util.TimezoneFormatter.Format; import com.ibm.icu.impl.Row.R5; import com.ibm.icu.text.MessageFormat; import com.ibm.icu.text.SimpleDateFormat; import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.ULocale; public class VerifyZones { private static final CLDRConfig CLDR_CONFIG = CLDRConfig.getInstance(); private static final String DIR = CLDRPaths.CHART_DIRECTORY + "verify/zones/"; private static final boolean DEBUG = false; final static Options myOptions = new Options(); enum MyOptions { organization(".*", "CLDR", "organization"), filter(".*", ".*", "locale filter (regex)"), timezoneFilter(".*", null, "timezone filter (regex)"),; // boilerplate final Option option; MyOptions(String argumentPattern, String defaultArgument, String helpText) { option = myOptions.add(this, argumentPattern, defaultArgument, helpText); } } static long date = new Date(new Date().getYear(), 0, 15, 0, 0, 0).getTime(); static long date6 = date + 182L * 24 * 60 * 60 * 1000; static class MetazoneRow extends R5 { public MetazoneRow(Integer order, Integer rawOffset, String container, int orderInMetazone, String metazone, String zone) { super(((long) order << 32) + rawOffset, container, metazone, orderInMetazone, zone); } public String getContainer() { return get1(); } public String getMetazone() { return get2(); } public String getZone() { return get4(); } } public static class ZoneFormats { private String gmtFormat; private String hourFormat; private String[] hourFormatPlusMinus; private ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder(); private CLDRFile cldrFile; public enum Length { LONG, SHORT; public String toString() { return name().toLowerCase(Locale.ENGLISH); } } public enum Type { generic, standard, daylight, genericOrStandard } public ZoneFormats set(CLDRFile cldrFile) { this.cldrFile = cldrFile; gmtFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/gmtFormat"); hourFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/hourFormat"); hourFormatPlusMinus = hourFormat.split(";"); icuServiceBuilder.setCldrFile(cldrFile); return this; } public String formatGMT(TimeZone currentZone) { int tzOffset = currentZone.getRawOffset(); SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian", hourFormatPlusMinus[tzOffset >= 0 ? 0 : 1]); String hoursMinutes = dateFormat.format(tzOffset >= 0 ? tzOffset : -tzOffset); return MessageFormat.format(gmtFormat, hoursMinutes); } public String getExemplarCity(String timezoneString) { String exemplarCity = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/zone[@type=\"" + timezoneString + "\"]/exemplarCity"); if (exemplarCity == null) { exemplarCity = timezoneString.substring(timezoneString.lastIndexOf('/') + 1).replace('_', ' '); } return exemplarCity; } public String getMetazoneName(String metazone, Length length, Type typeIn) { Type type = typeIn == Type.genericOrStandard ? Type.generic : typeIn; String name = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/metazone[@type=\"" + metazone + "\"]/" + length + "/" + type); return name != null ? name : typeIn != Type.genericOrStandard ? "n/a" : getMetazoneName(metazone, length, Type.standard); } } private final static SupplementalDataInfo sdi = SupplementalDataInfo.getInstance(); private final static Map> metazoneToRegionToZone = sdi.getMetazoneToRegionToZone(); private final static Set rows = new TreeSet(); private final static Set goldenZones = new HashSet(); private final static Map countryToOrder = new HashMap(); private final static List FORMAT_LIST = Arrays.asList(Format.VVVV, Format.vvvv, Format.v, Format.zzzz, Format.z, Format.zzzz, Format.z); static { // find out which canonical zones are not in a metazone Map nameToCountry = new TreeMap(); String[] zones = TimeZone.getAvailableIDs(); Set zoneSet = new LinkedHashSet(); Set noncanonical = new LinkedHashSet(); for (String zone : zones) { String countryCode = TimeZone.getRegion(zone); String englishTerritory = ULocale.getDisplayCountry("und-" + countryCode, ULocale.ENGLISH); nameToCountry.put(englishTerritory, countryCode); String canon = TimeZone.getCanonicalID(zone); if (canon.equals(zone)) { zoneSet.add(canon); } else { noncanonical.add(zone); } } // get mapping of country names to ints int i = 0; for (Entry entry : nameToCountry.entrySet()) { countryToOrder.put(entry.getValue(), i++); } //System.out.println("Canonical zones:\t" + zoneSet.size() + "\t" + zoneSet); //System.out.println("Non-canonical zones:\t" + noncanonical.size() + "\t" + noncanonical); Set metazones = sdi.getAllMetazones(); if (DEBUG && !metazones.equals(metazoneToRegionToZone.keySet())) { System.out.println("Mismatch between metazones"); showVennSets(metazones, metazoneToRegionToZone.keySet()); } Set zonesInMetazones = new LinkedHashSet(); for (String metazone : metazones) { //String container = PathHeader.getMetazonePageTerritory(metazone); Map regionToZone = metazoneToRegionToZone.get(metazone); String zone = regionToZone.get("001"); goldenZones.add(zone); zonesInMetazones.add(zone); // TimeZone currentZone = TimeZone.getTimeZone(tz_string); // int order = Containment.getOrder(container); // int offsetOrder = currentZone.getRawOffset(); // MetazoneRow row = new MetazoneRow(order, offsetOrder, container, 0, metazone, tz_string); // rows.add(row); addRow(metazone, zone, 0); } //System.out.println("Zones. A = canonical zones, B = zones in metazonesToRegionToZone"); //showVennSets(zoneSet, zonesInMetazones); vennSets(zoneSet, zonesInMetazones); Set found = new LinkedHashSet(); for (String zone : zoneSet) { Set metaZoneRanges = sdi.getMetaZoneRanges(zone); if (metaZoneRanges == null) { continue; } for (MetaZoneRange metaZoneRange : metaZoneRanges) { if (metaZoneRange.dateRange.getTo() == DateRange.END_OF_TIME) { found.add(zone); addRow(metaZoneRange.metazone, zone, 1); break; } } } // zoneSet.removeAll(found); // for (String zone : zoneSet) { // found.add(zone); // // TimeZone currentZone = TimeZone.getTimeZone(tz_string); // // int offsetOrder = currentZone.getRawOffset(); // // MetazoneRow row = new MetazoneRow(Integer.MAX_VALUE, offsetOrder, "001", 1, "None", tz_string); // // rows.add(row); // addRow("None", zone, 1); // } if (DEBUG) System.out.println("\nSorted"); for (MetazoneRow row : rows) { if (row.getMetazone().equals("Europe_Central")) { if (DEBUG) System.out.println(row); } } } private static void addRow(String metaZone, String tz_string, int orderInMetazone) { TimeZone currentZone = TimeZone.getTimeZone(tz_string); String container = PathHeader.getMetazonePageTerritory(metaZone); if (container == null) { return; // skip } int order = Containment.getOrder(container); int offsetOrder = currentZone.getRawOffset(); orderInMetazone = (orderInMetazone << 16) | (hasDaylight(currentZone) ? 0 : 1) | countryToOrder.get(TimeZone.getRegion(tz_string)); MetazoneRow row = new MetazoneRow(order, offsetOrder, container, orderInMetazone, metaZone, tz_string); if (metaZone.equals("Europe_Central")) { if (DEBUG) System.out.println(row); } rows.add(row); } private static void showVennSets(Set zoneSet, Set zonesInMetazones) { Set common = new LinkedHashSet(); Set firstMinusSecond = new LinkedHashSet(); Set secondMinusFirst = new LinkedHashSet(); vennSets(zoneSet, zonesInMetazones, common, firstMinusSecond, secondMinusFirst); if (!common.isEmpty()) System.out.println("A & B:\t" + common.size() + "\t" + common); if (!firstMinusSecond.isEmpty()) System.out.println("A - B:\t" + firstMinusSecond.size() + "\t" + firstMinusSecond); if (!secondMinusFirst.isEmpty()) System.out.println("B - A:\t" + secondMinusFirst.size() + "\t" + secondMinusFirst); } private static void vennSets(Set first, Set second, Set common, Set firstMinusSecond, Set secondMinusFirst) { common.clear(); common.addAll(first); common.retainAll(second); firstMinusSecond.clear(); firstMinusSecond.addAll(first); firstMinusSecond.removeAll(common); secondMinusFirst.clear(); secondMinusFirst.addAll(second); secondMinusFirst.removeAll(common); } @SuppressWarnings("unused") private static void vennSets(Set first, Set second, Set common) { common.clear(); common.addAll(first); common.retainAll(second); first.removeAll(common); second.removeAll(common); } private static void vennSets(Set first, Set second) { first.removeAll(second); second.removeAll(first); } /** * Produce a set of static tables from the vxml data. Only a stopgap until the above is integrated into ST. * * @param args * @throws IOException */ public static void main(String[] args) throws IOException { myOptions.parse(MyOptions.organization, args, true); String organization = MyOptions.organization.option.getValue(); String filter = MyOptions.filter.option.getValue(); String timezoneFilterString = MyOptions.timezoneFilter.option.getValue(); Matcher timezoneFilter = timezoneFilterString == null ? null : PatternCache.get(timezoneFilterString) .matcher(""); Factory factory2 = Factory.make(CLDRPaths.MAIN_DIRECTORY, filter); CLDRFile englishCldrFile = factory2.make("en", true); DateTimeFormats.writeCss(DIR); final CLDRFile english = CLDR_CONFIG.getEnglish(); Map indexMap = new TreeMap<>(CLDR_CONFIG.getCollator()); for (String localeID : factory2.getAvailableLanguages()) { Level level = StandardCodes.make().getLocaleCoverageLevel(organization, localeID); if (Level.MODERN.compareTo(level) > 0) { continue; } CLDRFile cldrFile = factory2.make(localeID, true); PrintWriter out = FileUtilities.openUTF8Writer(DIR, localeID + ".html"); String title = "Verify Time Zones: " + englishCldrFile.getName(localeID); out.println("\n" + "\n" + "" + title + "\n" + "\n" + "

" + title + "

\n" + "

Index

\n"); showZones(timezoneFilter, englishCldrFile, cldrFile, out); out.println(""); out.close(); indexMap.put(english.getName(localeID), localeID + ".html"); } try (PrintWriter index = DateTimeFormats.openIndex(DIR, "Time Zones")) { DateTimeFormats.writeIndexMap(indexMap, index); } // Look at DateTimeFormats.java if (true) return; // Set defaultContentLocales = sdi.getDefaultContentLocales(); // NumberFormat enf = NumberFormat.getIntegerInstance(ULocale.ENGLISH); // enf.setGroupingUsed(false); // Set debugCreationErrors = new LinkedHashSet(); // Set errors = new LinkedHashSet(); // // for (String locale : factory2.getAvailableLanguages()) { // if (defaultContentLocales.contains(locale)) { // continue; // } // Level level = StandardCodes.make().getLocaleCoverageLevel(organization, locale); // if (Level.MODERN.compareTo(level) > 0) { // continue; // } // // // one path for group-3, one for group-4 // int factor = USES_GROUPS_OF_4.contains(locale) ? 10000 : 1000; // // ULocale locale2 = new ULocale(locale); // NumberFormat nf = NumberFormat.getIntegerInstance(locale2); // nf.setMaximumFractionDigits(0); // CLDRFile cldrFile = factory2.make(locale, true, DraftStatus.contributed); // PluralInfo pluralInfo = sdi.getPlurals(locale); // Set samples = new TreeSet(); // for (Entry> entry : pluralInfo.getCountToExamplesMap().entrySet()) { // samples.add(entry.getValue().get(0)); // } // String[] debugOriginals = null; // CompactDecimalFormat cdf = BuildIcuCompactDecimalFormat.build(cldrFile, debugCreationErrors, debugOriginals, // Style.SHORT, locale2); // captureErrors(debugCreationErrors, errors, locale, "short"); // CompactDecimalFormat cdfs = BuildIcuCompactDecimalFormat.build(cldrFile, debugCreationErrors, debugOriginals, // Style.LONG, locale2); // captureErrors(debugCreationErrors, errors, locale, "long"); // // Set samples2 = new TreeSet(); // for (int i = 10; i < factor; i *= 10) { // for (Double sample : samples) { // samples2.add(sample*i); // } // } // samples.addAll(samples2); // samples.add(1.5d); // System.out.println("———\t" + englishCldrFile.getName(locale) + "\t———"); // // String column12 = (locale + "\t" + englishCldrFile.getName(locale)); // System.out.print(column12 + // "\tNumeric\tCompact-Short\tCompact-Long\tFixed Numeric\tFixed Compact-Short\tFixed Compact-Long\n"); // // try { // // we print the __ so that it can be imported into a spreadsheet without problems. // for (long i = factor; i <= 100000000000000L; i *= factor) { // for (Double sample : samples) { // double source = i * sample; // if (false && source == 22000000 && locale.equals("cs")) { // System.out.println("**"); // } // System.out.print(locale + "\t__" + enf.format(source)); // System.out.print("\t__" + nf.format(source)); // String formatted = cdf.format(source); // System.out.print("\t__" + formatted); // formatted = cdfs.format(source); // System.out.println("\t__" + formatted); // } // System.out.println(); // } // } catch (Exception e) { // e.printStackTrace(); // } // } // for (String s : errors) { // System.out.println(s); // } } public static void showZones(Matcher timezoneFilter, CLDRFile englishCldrFile, CLDRFile nativeCdrFile, Appendable out) throws IOException { TablePrinter tablePrinter = new TablePrinter() // .setCaption("Timezone Formats") .setTableAttributes("class='dtf-table'") .addColumn("Metazone").setHeaderCell(true).setSpanRows(true) .setHeaderAttributes("class='dtf-th'").setCellAttributes("class='dtf-s'") .addColumn("Region: TZID").setHeaderCell(true).setSpanRows(true) .setHeaderAttributes("class='dtf-th'").setCellAttributes("class='dtf-s'") //.setCellPattern(CldrUtility.getDoubleLinkMsg()) // HACK because anchors don't work any more // .addColumn("Region: City").setHeaderCell(true).setSpanRows(true) // .addColumn("Region/City").setSpanRows(true) ; // .addColumn("Code", "class='source'", CldrUtility.getDoubleLinkMsg(), "class='source'", true).setSpanRows(true) boolean daylight = false; for (Format s : FORMAT_LIST) { tablePrinter.addColumn(s.toString() + "
" + s.type.toString(daylight) + "
" + s.location + "
" + s.length).setSpanRows(true).setHeaderAttributes("class='dtf-th'") .setCellAttributes("class='dtf-s'"); if (s == Format.z) { daylight = true; // reset for final 2 items } } tablePrinter.addColumn("View").setHeaderCell(true).setHeaderAttributes("class='dtf-th'").setCellAttributes("class='dtf-s'"); ZoneFormats englishZoneFormats = new ZoneFormats().set(englishCldrFile); addZones(englishZoneFormats, nativeCdrFile, timezoneFilter, tablePrinter); out.append(tablePrinter.toString() + "\n"); } private static void addZones(ZoneFormats englishZoneFormats, CLDRFile cldrFile, Matcher timezoneFilter, TablePrinter output) throws IOException { CLDRFile englishCldrFile = englishZoneFormats.cldrFile; //ZoneFormats nativeZoneFormats = new ZoneFormats().set(cldrFile); TimezoneFormatter tzformatter = new TimezoneFormatter(cldrFile); for (MetazoneRow row : rows) { String grouping = row.getContainer(); String metazone = row.getMetazone(); String tzid = row.getZone(); TimeZone currentZone = TimeZone.getTimeZone(tzid); TimeZone tz = currentZone; String englishGrouping = englishCldrFile.getName(CLDRFile.TERRITORY_NAME, grouping); String metazoneInfo = englishGrouping + "
" + englishZoneFormats.formatGMT(currentZone) + "
" + "MZ: " + metazone; boolean isGolden = goldenZones.contains(tzid); String countryCode2 = TimeZone.getRegion(tzid); if (countryCode2.equals("001")) { continue; } String englishTerritory = englishCldrFile.getName(CLDRFile.TERRITORY_NAME, countryCode2); output.addRow() .addCell(metazoneInfo) .addCell(englishTerritory + ": " + tzid.replace("/", "/\u200B")); long date2 = getStandardDate(tz); for (Format pattern : FORMAT_LIST) { String formattedZone = tzformatter.getFormattedZone(tzid, pattern.toString(), date2); if (isGolden) { formattedZone = "" + formattedZone + ""; } output.addCell(formattedZone); if (pattern == Format.z) { if (!hasDaylight(tz)) { output.addCell("n/a"); output.addCell("n/a"); break; } date2 = date2 == date ? date6 : date; // reverse for final 2 items } } String view = PathHeader.getLinkedView(surveyUrl, cldrFile, METAZONE_PREFIX + metazone + METAZONE_SUFFIX); if (view == null) { view = PathHeader.getLinkedView(surveyUrl, cldrFile, METAZONE_PREFIX + metazone + METAZONE_SUFFIX2); } output.addCell(view == null ? "" : view); output.finishRow(); } } private static String surveyUrl = CLDR_CONFIG.getProperty("CLDR_SURVEY_URL", "http://st.unicode.org/cldr-apps/survey"); static private String METAZONE_PREFIX = "//ldml/dates/timeZoneNames/metazone[@type=\""; static private String METAZONE_SUFFIX = "\"]/long/generic"; static private String METAZONE_SUFFIX2 = "\"]/long/standard"; private static boolean hasDaylight(TimeZone tz) { int dateOffset = tz.getOffset(date); return dateOffset != tz.getRawOffset() || dateOffset != tz.getOffset(date6); } private static long getStandardDate(TimeZone tz) { return tz.getOffset(date) == tz.getRawOffset() ? date : date6; } private static long getDaylightDate(TimeZone tz) { return tz.getOffset(date) == tz.getRawOffset() ? date6 : date; } private static void captureErrors(Set debugCreationErrors, Set errors, String locale, String length) { if (debugCreationErrors.size() != 0) { for (String s : debugCreationErrors) { errors.add(locale + "\t" + length + "\t" + s); } debugCreationErrors.clear(); } } }