| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.libcore.timezone.tzlookup; |
| |
| import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile; |
| import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneTree; |
| import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneUsage; |
| import com.android.libcore.timezone.util.Errors; |
| import com.android.libcore.timezone.util.Errors.HaltExecutionException; |
| import com.ibm.icu.util.BasicTimeZone; |
| import com.ibm.icu.util.Calendar; |
| import com.ibm.icu.util.GregorianCalendar; |
| import com.ibm.icu.util.TimeZone; |
| import com.ibm.icu.util.TimeZoneRule; |
| |
| import java.io.IOException; |
| import java.text.ParseException; |
| import java.time.Instant; |
| import java.time.temporal.ChronoUnit; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| import javax.xml.stream.XMLStreamException; |
| |
| /** |
| * Generates the tzlookup.xml file using the information from countryzones.txt and zones.tab. |
| * |
| * See {@link #main(String[])} for commandline information. |
| */ |
| public final class TzLookupGenerator { |
| |
| /** |
| * The start time (inclusive) for calculating country zone rules. 19700101 00:00:00 UTC. Chosen |
| * because this is the point in time for which the tzdb zone.tab data is supposed to be correct. |
| */ |
| public static final Instant ZONE_USAGE_CALCS_START = Instant.EPOCH; |
| |
| /** |
| * The end time (exclusive) for generating country zone usage. 20380119 03:14:07 UTC. Any times |
| * after this will be considered "infinity" for the "notAfter" value and not included. Chosen |
| * because this is a "nice round number" and has historical significance for people that deal |
| * with computer time. There is no particular reason to choose this over another time; any |
| * future time after the last time we expect the code to reasonably encounter will do. |
| */ |
| public static final Instant ZONE_USAGE_NOT_AFTER_CUT_OFF = |
| Instant.ofEpochSecond(Integer.MAX_VALUE); |
| |
| /** |
| * The end time (exclusive) for calculating country zone usage. The time zone periods are |
| * calculated to this point. The main requirement is that it's after |
| * {@link #ZONE_USAGE_NOT_AFTER_CUT_OFF} by an amount larger than the usual daylight savings |
| * period; here we use 2 years. |
| */ |
| public static final Instant ZONE_USAGE_CALCS_END = |
| ZONE_USAGE_NOT_AFTER_CUT_OFF.plus(2 * 365, ChronoUnit.DAYS); |
| |
| private final String countryZonesFile; |
| private final String zoneTabFile; |
| private final String backwardFile; |
| private final String outputFile; |
| |
| /** |
| * Executes the generator. |
| */ |
| public static void main(String[] args) throws Exception { |
| if (args.length != 4) { |
| System.err.println( |
| "usage: java com.android.libcore.timezone.tzlookup.TzLookupGenerator" |
| + " <input proto file> <zone.tab file> <backward file>" |
| + " <output xml file>"); |
| System.exit(0); |
| } |
| boolean success = new TzLookupGenerator(args[0], args[1], args[2], args[3]).execute(); |
| System.exit(success ? 0 : 1); |
| } |
| |
| TzLookupGenerator(String countryZonesFile, String zoneTabFile, String backwardFile, |
| String outputFile) { |
| this.countryZonesFile = countryZonesFile; |
| this.zoneTabFile = zoneTabFile; |
| this.backwardFile = backwardFile; |
| this.outputFile = outputFile; |
| } |
| |
| boolean execute() { |
| Errors errors = new Errors(); |
| try { |
| // Parse the countryzones input file. |
| CountryZonesFile.CountryZones countryZonesIn = |
| parseAndValidateCountryZones(countryZonesFile, errors); |
| |
| // Check the countryzones.txt rules version matches the version that ICU is using. |
| String icuTzDataVersion = TimeZone.getTZDataVersion(); |
| String inputIanaVersion = countryZonesIn.getIanaVersion(); |
| if (!icuTzDataVersion.equals(inputIanaVersion)) { |
| throw errors.addFatalAndHalt("Input data (countryzones.txt) is for " |
| + inputIanaVersion + " but the ICU you have is for " + icuTzDataVersion); |
| } |
| |
| |
| // Pull out information we want to validate against from zone.tab (which we have to |
| // assume matches the ICU version since it doesn't contain its own version info). |
| Map<String, List<String>> zoneTabMapping = parseZoneTabFile(zoneTabFile, errors); |
| |
| List<CountryZonesFile.Country> countriesIn = countryZonesIn.getCountriesList(); |
| List<String> countriesInIsos = CountryZonesFileSupport.extractIsoCodes(countriesIn); |
| |
| // Sanity check the countryzones file only contains lower-case country codes. The output |
| // file uses them and the on-device code assumes lower case. |
| if (!Utils.allLowerCaseAscii(countriesInIsos)) { |
| throw errors.addFatalAndHalt( |
| "Non-lowercase country ISO codes found in: " + countriesInIsos); |
| } |
| // Sanity check the countryzones file doesn't contain duplicate country entries. |
| if (!Utils.allUnique(countriesInIsos)) { |
| throw errors.addFatalAndHalt( |
| "Duplicate input country entries found: " + countriesInIsos); |
| } |
| |
| // Validate the country iso codes found in the countryzones.txt against those in |
| // zone.tab. zone.tab uses upper case, countryzones uses lower case. |
| List<String> upperCaseCountriesInIsos = Utils.toUpperCase(countriesInIsos); |
| Set<String> timezonesCountryIsos = new HashSet<>(upperCaseCountriesInIsos); |
| Set<String> zoneTabCountryIsos = zoneTabMapping.keySet(); |
| if (!zoneTabCountryIsos.equals(timezonesCountryIsos)) { |
| throw errors.addFatalAndHalt(zoneTabFile + " contains " |
| + Utils.subtract(zoneTabCountryIsos, timezonesCountryIsos) |
| + " not present in countryzones, " |
| + countryZonesFile + " contains " |
| + Utils.subtract(timezonesCountryIsos, zoneTabCountryIsos) |
| + " not present in zonetab."); |
| } |
| |
| // Obtain and validate a mapping from old IDs to new IDs. |
| Map<String, String> zoneIdLinks = parseAndValidateBackwardFile(backwardFile, errors); |
| errors.throwIfError("Errors accumulated"); |
| |
| TzLookupFile.TimeZones timeZonesOut = createOutputTimeZones( |
| inputIanaVersion, zoneTabMapping, countriesIn, zoneIdLinks, errors); |
| errors.throwIfError("Errors accumulated"); |
| |
| // Write the output structure if there wasn't an error. |
| logInfo("Writing " + outputFile); |
| try { |
| TzLookupFile.write(timeZonesOut, outputFile); |
| } catch (XMLStreamException e) { |
| throw errors.addFatalAndHalt("Unable to write output file", e); |
| } |
| return true; |
| } catch (HaltExecutionException | IOException e) { |
| logError("Stopping due to fatal condition", e); |
| return false; |
| } finally { |
| // Report all warnings / errors |
| if (!errors.isEmpty()) { |
| logInfo("Issues:\n" + errors.asString()); |
| } |
| } |
| } |
| |
| private Map<String, List<String>> parseZoneTabFile(String zoneTabFile, Errors errors) |
| throws HaltExecutionException { |
| errors.pushScope("Parsing " + zoneTabFile); |
| try { |
| ZoneTabFile zoneTabIn; |
| zoneTabIn = ZoneTabFile.parse(zoneTabFile); |
| return ZoneTabFile.createCountryToOlsonIdsMap(zoneTabIn); |
| } catch (ParseException | IOException e) { |
| throw errors.addFatalAndHalt("Unable to parse " + zoneTabFile, e); |
| } finally { |
| errors.popScope(); |
| } |
| } |
| |
| /** |
| * Load the backward file and return the links contained within. This is used as the source of |
| * equivalent time zone IDs. |
| */ |
| private static Map<String, String> parseAndValidateBackwardFile( |
| String backwardFile, Errors errors) { |
| errors.pushScope("Parsing " + backwardFile); |
| try { |
| BackwardFile backwardIn = BackwardFile.parse(backwardFile); |
| |
| // Validate the links. |
| Map<String, String> zoneIdLinks = backwardIn.getDirectLinks(); |
| zoneIdLinks.forEach( |
| (k, v) -> { |
| if (invalidTimeZoneId(k)) { |
| errors.addError("Bad 'from' link: " + k + "->" + v); |
| } |
| if (invalidTimeZoneId(v)) { |
| errors.addError("Bad 'to' link: " + k + "->" + v); |
| } |
| }); |
| return zoneIdLinks; |
| } catch (ParseException | IOException e) { |
| errors.addError("Unable to parse " + backwardFile, e); |
| return null; |
| } finally { |
| errors.popScope(); |
| } |
| } |
| |
| private static CountryZonesFile.CountryZones parseAndValidateCountryZones( |
| String countryZonesFile, Errors errors) throws HaltExecutionException { |
| errors.pushScope("Parsing " + countryZonesFile); |
| try { |
| CountryZonesFile.CountryZones countryZonesIn; |
| countryZonesIn = CountryZonesFileSupport.parseCountryZonesTextFile(countryZonesFile); |
| return countryZonesIn; |
| } catch (ParseException | IOException e) { |
| throw errors.addFatalAndHalt("Unable to parse " + countryZonesFile, e); |
| } finally { |
| errors.popScope(); |
| } |
| } |
| |
| private static TzLookupFile.TimeZones createOutputTimeZones(String inputIanaVersion, |
| Map<String, List<String>> zoneTabMapping, List<CountryZonesFile.Country> countriesIn, |
| Map<String, String> zoneIdLinks, Errors errors) |
| throws HaltExecutionException { |
| |
| // Start constructing the output structure. |
| TzLookupFile.TimeZones timeZonesOut = new TzLookupFile.TimeZones(inputIanaVersion); |
| TzLookupFile.CountryZones countryZonesOut = new TzLookupFile.CountryZones(); |
| timeZonesOut.setCountryZones(countryZonesOut); |
| |
| // The time use when sampling the offsets for a zone. |
| final long offsetSampleTimeMillis = getSampleOffsetTimeMillisForData(inputIanaVersion); |
| |
| // The start time to use when working out whether a zone has used UTC. |
| // We don't care about historical use of UTC (e.g. parts of Europe like France prior |
| // to WW2) so we start looking at the beginning of "this year". |
| long everUseUtcStartTimeMillis = getYearStartTimeMillisForData(inputIanaVersion); |
| |
| // Process each Country. |
| for (CountryZonesFile.Country countryIn : countriesIn) { |
| String isoCode = countryIn.getIsoCode(); |
| List<String> zoneTabCountryTimeZoneIds = zoneTabMapping.get(isoCode.toUpperCase()); |
| if (zoneTabCountryTimeZoneIds == null) { |
| errors.addError("Country=" + isoCode + " missing from zone.tab"); |
| // No point in continuing. |
| continue; |
| } |
| |
| TzLookupFile.Country countryOut = processCountry( |
| offsetSampleTimeMillis, everUseUtcStartTimeMillis, countryIn, |
| zoneTabCountryTimeZoneIds, zoneIdLinks, errors); |
| if (countryOut == null) { |
| // Continue processing countries if there are only errors. |
| continue; |
| } |
| countryZonesOut.addCountry(countryOut); |
| } |
| errors.throwIfError("One or more countries failed"); |
| return timeZonesOut; |
| } |
| |
| private static TzLookupFile.Country processCountry(long offsetSampleTimeMillis, |
| long everUseUtcStartTimeMillis, CountryZonesFile.Country countryIn, |
| List<String> zoneTabCountryTimeZoneIds, Map<String, String> zoneIdLinks, |
| Errors errors) { |
| String isoCode = countryIn.getIsoCode(); |
| errors.pushScope("country=" + isoCode); |
| try { |
| // Each Country must have >= 1 time zone. |
| List<CountryZonesFile.TimeZoneMapping> timeZonesIn = |
| countryIn.getTimeZoneMappingsList(); |
| if (timeZonesIn.isEmpty()) { |
| errors.addError("No time zones"); |
| // No point in continuing. |
| return null; |
| } |
| |
| List<String> countryTimeZoneIds = CountryZonesFileSupport.extractIds(timeZonesIn); |
| |
| // Look for duplicate time zone IDs. |
| if (!Utils.allUnique(countryTimeZoneIds)) { |
| errors.addError("country's zones=" + countryTimeZoneIds + " contains duplicates"); |
| // No point in continuing. |
| return null; |
| } |
| |
| // Each Country needs a default time zone ID (but we can guess in some cases). |
| String defaultTimeZoneId = determineCountryDefaultZoneId(countryIn, errors); |
| if (defaultTimeZoneId == null) { |
| // No point in continuing. |
| return null; |
| } |
| boolean defaultTimeZoneBoost = |
| determineCountryDefaultTimeZoneBoost(countryIn, errors); |
| |
| // Validate the default. |
| if (!countryTimeZoneIds.contains(defaultTimeZoneId)) { |
| errors.addError("defaultTimeZoneId=" + defaultTimeZoneId |
| + " is not one of the country's zones=" + countryTimeZoneIds); |
| // No point in continuing. |
| return null; |
| } |
| |
| // Validate the other zone IDs. |
| try { |
| errors.pushScope("validate country zone ids"); |
| for (String countryTimeZoneId : countryTimeZoneIds) { |
| if (invalidTimeZoneId(countryTimeZoneId)) { |
| errors.addError("countryTimeZoneId=" + countryTimeZoneId |
| + " is not a valid zone ID"); |
| } |
| } |
| if (errors.hasError()) { |
| // No point in continuing. |
| return null; |
| } |
| } finally { |
| errors.popScope(); |
| } |
| |
| // Work out the hint for whether the country uses a zero offset from UTC. |
| boolean everUsesUtc = anyZonesUseUtc(countryTimeZoneIds, everUseUtcStartTimeMillis); |
| |
| // Validate the country information against the equivalent information in zone.tab. |
| errors.pushScope("zone.tab comparison"); |
| try { |
| // Look for unexpected duplicate time zone IDs in zone.tab |
| if (!Utils.allUnique(zoneTabCountryTimeZoneIds)) { |
| errors.addError("Duplicate time zone IDs found:" + zoneTabCountryTimeZoneIds); |
| // No point in continuing. |
| return null; |
| } |
| |
| // Validate the IDs being used against the IANA data for the country. If it fails |
| // the countryzones.txt needs to be updated with new IDs (or an alias can be added |
| // if there's some reason to keep using the old ID). |
| validateCountryZonesTzIdsAgainstIana(isoCode, zoneTabCountryTimeZoneIds, |
| timeZonesIn, zoneIdLinks, errors); |
| if (errors.hasError()) { |
| // No point in continuing. |
| return null; |
| } |
| } finally { |
| errors.popScope(); |
| } |
| |
| // Calculate countryZoneUsage. |
| CountryZoneUsage countryZoneUsage = calculateCountryZoneUsage(countryIn, errors); |
| if (countryZoneUsage == null) { |
| // No point in continuing with this country. |
| return null; |
| } |
| |
| // Add the country to the output structure. |
| TzLookupFile.Country countryOut = new TzLookupFile.Country( |
| isoCode, defaultTimeZoneId, defaultTimeZoneBoost, everUsesUtc); |
| |
| // Process each input time zone. |
| for (CountryZonesFile.TimeZoneMapping timeZoneIn : timeZonesIn) { |
| errors.pushScope( |
| "id=" + timeZoneIn.getId() + ", offset=" + timeZoneIn.getUtcOffset() |
| + ", shownInPicker=" + timeZoneIn.getShownInPicker()); |
| try { |
| // Validate the offset information in countryIn. |
| validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn, errors); |
| |
| String timeZoneInId = timeZoneIn.getId(); |
| boolean shownInPicker = timeZoneIn.getShownInPicker(); |
| if (!countryZoneUsage.hasEntry(timeZoneInId)) { |
| // This implies a programming error. |
| errors.addError("No entry in CountryZoneUsage for " + timeZoneInId); |
| return null; |
| } |
| |
| // The notUsedAfterInstant can be null if the zone is used until at least |
| // ZONE_CALCS_END_INSTANT. That's what we want. |
| Instant notUsedAfterInstant = |
| countryZoneUsage.getNotUsedAfterInstant(timeZoneInId); |
| |
| // Add the id mapping and associated metadata. |
| TzLookupFile.TimeZoneMapping timeZoneIdOut = |
| new TzLookupFile.TimeZoneMapping( |
| timeZoneInId, shownInPicker, notUsedAfterInstant); |
| countryOut.addTimeZoneIdentifier(timeZoneIdOut); |
| } finally { |
| errors.popScope(); |
| } |
| } |
| return countryOut; |
| } finally{ |
| // End of country processing. |
| errors.popScope(); |
| } |
| } |
| |
| private static void validateCountryZonesTzIdsAgainstIana(String isoCode, |
| List<String> zoneTabCountryTimeZoneIds, |
| List<CountryZonesFile.TimeZoneMapping> timeZoneMappings, |
| Map<String, String> zoneIdLinks, Errors errors) { |
| |
| List<String> expectedIanaTimeZoneIds = new ArrayList<>(); |
| for (CountryZonesFile.TimeZoneMapping mapping : timeZoneMappings) { |
| String timeZoneId = mapping.getId(); |
| String expectedIanaTimeZoneId; |
| if (!mapping.hasAliasId()) { |
| expectedIanaTimeZoneId = timeZoneId; |
| } else { |
| String aliasTimeZoneId = mapping.getAliasId(); |
| |
| // Confirm the alias is valid. |
| if (!aliasTimeZoneId.equals(zoneIdLinks.get(timeZoneId))) { |
| errors.addError(timeZoneId + " does not link to " + aliasTimeZoneId); |
| return; |
| } |
| expectedIanaTimeZoneId = aliasTimeZoneId; |
| } |
| expectedIanaTimeZoneIds.add(expectedIanaTimeZoneId); |
| } |
| |
| if (!Utils.setEquals(zoneTabCountryTimeZoneIds, expectedIanaTimeZoneIds)) { |
| errors.addError("IANA lists " + isoCode |
| + " as having zones: " + zoneTabCountryTimeZoneIds |
| + ", but countryzones has " + expectedIanaTimeZoneIds); |
| } |
| } |
| |
| /** |
| * Determines the default zone ID for the country. |
| */ |
| private static String determineCountryDefaultZoneId( |
| CountryZonesFile.Country countryIn, Errors errors) { |
| List<CountryZonesFile.TimeZoneMapping> timeZonesIn = countryIn.getTimeZoneMappingsList(); |
| String defaultTimeZoneId; |
| if (countryIn.hasDefaultTimeZoneId()) { |
| defaultTimeZoneId = countryIn.getDefaultTimeZoneId(); |
| if (invalidTimeZoneId(defaultTimeZoneId)) { |
| errors.addError( |
| "Default time zone ID " + defaultTimeZoneId + " is not valid"); |
| // No point in continuing. |
| return null; |
| } |
| } else { |
| if (timeZonesIn.size() > 1) { |
| errors.addError( |
| "To pick a default time zone there must be a single offset group"); |
| // No point in continuing. |
| return null; |
| } |
| defaultTimeZoneId = timeZonesIn.get(0).getId(); |
| } |
| return defaultTimeZoneId; |
| } |
| |
| /** |
| * Determines the defaultTimeZoneBoost value for the country. |
| */ |
| private static boolean determineCountryDefaultTimeZoneBoost( |
| CountryZonesFile.Country countryIn, Errors errors) { |
| if (!countryIn.hasDefaultTimeZoneBoost()) { |
| return false; |
| } |
| |
| boolean defaultTimeZoneBoost = countryIn.getDefaultTimeZoneBoost(); |
| if (!countryIn.hasDefaultTimeZoneId() && defaultTimeZoneBoost) { |
| errors.addError( |
| "defaultTimeZoneBoost is specified but defaultTimeZoneId is not explicit"); |
| } |
| |
| return defaultTimeZoneBoost; |
| } |
| |
| /** |
| * Returns true if any of the zones use UTC after the time specified. |
| */ |
| private static boolean anyZonesUseUtc(List<String> timeZoneIds, long startTimeMillis) { |
| for (String timeZoneId : timeZoneIds) { |
| BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(timeZoneId); |
| TimeZoneRule[] rules = timeZone.getTimeZoneRules(startTimeMillis); |
| for (TimeZoneRule rule : rules) { |
| int utcOffset = rule.getRawOffset() + rule.getDSTSavings(); |
| if (utcOffset == 0) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a sample time related to the IANA version to enable any offset validation to be |
| * repeatable (rather than depending on the current time when the tool is run). |
| */ |
| private static long getSampleOffsetTimeMillisForData(String inputIanaVersion) { |
| // Uses <year>/07/02 12:00:00 UTC, where year is taken from the IANA version + 1. |
| // This is fairly arbitrary, but reflects the fact that we want a point in the future |
| // WRT to the data, and once a year has been picked then half-way through seems about right. |
| Calendar calendar = getYearStartForData(inputIanaVersion); |
| calendar.set(calendar.get(Calendar.YEAR) + 1, Calendar.JULY, 2, 12, 0, 0); |
| return calendar.getTimeInMillis(); |
| } |
| |
| /** |
| * Returns the 1st Jan 00:00:00 UTC time on the year the IANA version relates to. Therefore |
| * guaranteed to be before the data is ever used and can be treated as "the beginning of time" |
| * (assuming derived information won't be used for historical calculations). |
| */ |
| private static long getYearStartTimeMillisForData(String inputIanaVersion) { |
| return getYearStartForData(inputIanaVersion).getTimeInMillis(); |
| } |
| |
| private static Calendar getYearStartForData(String inputIanaVersion) { |
| String yearString = inputIanaVersion.substring(0, inputIanaVersion.length() - 1); |
| int year = Integer.parseInt(yearString); |
| Calendar calendar = new GregorianCalendar(TimeZone.GMT_ZONE); |
| calendar.clear(); |
| calendar.set(year, Calendar.JANUARY, 1, 0, 0, 0); |
| return calendar; |
| } |
| |
| private static boolean invalidTimeZoneId(String timeZoneId) { |
| TimeZone zone = TimeZone.getTimeZone(timeZoneId); |
| return !(zone instanceof BasicTimeZone) || zone.getID().equals(TimeZone.UNKNOWN_ZONE_ID); |
| } |
| |
| private static void validateNonDstOffset(long offsetSampleTimeMillis, |
| CountryZonesFile.Country country, CountryZonesFile.TimeZoneMapping timeZoneIn, |
| Errors errors) { |
| String utcOffsetString = timeZoneIn.getUtcOffset(); |
| long utcOffsetMillis; |
| try { |
| utcOffsetMillis = Utils.parseUtcOffsetToMillis(utcOffsetString); |
| } catch (ParseException e) { |
| errors.addError("Bad offset string: " + utcOffsetString); |
| return; |
| } |
| |
| final long minimumGranularity = TimeUnit.MINUTES.toMillis(15); |
| if (utcOffsetMillis % minimumGranularity != 0) { |
| errors.addWarning( |
| "Unexpected granularity: not a multiple of 15 minutes: " + utcOffsetString); |
| } |
| |
| String timeZoneIdIn = timeZoneIn.getId(); |
| if (invalidTimeZoneId(timeZoneIdIn)) { |
| errors.addError("Time zone ID=" + timeZoneIdIn + " is not valid"); |
| return; |
| } |
| |
| // Check the offset Android has matches what ICU thinks. |
| TimeZone timeZone = TimeZone.getTimeZone(timeZoneIdIn); |
| int[] offsets = new int[2]; |
| timeZone.getOffset(offsetSampleTimeMillis, false /* local */, offsets); |
| int actualOffsetMillis = offsets[0]; |
| if (actualOffsetMillis != utcOffsetMillis) { |
| errors.addError("Offset mismatch: You will want to confirm the ordering for " |
| + country.getIsoCode() + " still makes sense. Raw offset for " |
| + timeZoneIdIn + " is " + Utils.toUtcOffsetString(actualOffsetMillis) |
| + " and not " + Utils.toUtcOffsetString(utcOffsetMillis) |
| + " at " + Utils.formatUtc(offsetSampleTimeMillis)); |
| } |
| } |
| |
| private static CountryZoneUsage calculateCountryZoneUsage( |
| CountryZonesFile.Country countryIn, Errors errors) { |
| errors.pushScope("Building zone tree"); |
| try { |
| CountryZoneTree countryZoneTree = CountryZoneTree.create( |
| countryIn, ZONE_USAGE_CALCS_START, ZONE_USAGE_CALCS_END); |
| List<String> countryIssues = countryZoneTree.validateNoPriorityClashes(); |
| if (!countryIssues.isEmpty()) { |
| errors.addError("Issues validating country zone trees. Adjust priorities:"); |
| countryIssues.forEach(errors::addError); |
| return null; |
| } |
| return countryZoneTree.calculateCountryZoneUsage(ZONE_USAGE_NOT_AFTER_CUT_OFF); |
| } finally { |
| errors.popScope(); |
| } |
| } |
| |
| private static void logError(String msg) { |
| System.err.println("E: " + msg); |
| } |
| |
| private static void logError(String s, Throwable e) { |
| logError(s); |
| e.printStackTrace(System.err); |
| } |
| |
| private static void logInfo(String msg) { |
| System.err.println("I: " + msg); |
| } |
| } |