| @ -0,0 +1,194 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers; | |||
| import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType; | |||
| import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; | |||
| import com.google.i18n.phonenumbers.prefixmapper.PrefixTimeZonesMap; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.io.ObjectInputStream; | |||
| import java.util.ArrayList; | |||
| import java.util.Collections; | |||
| import java.util.List; | |||
| import java.util.logging.Level; | |||
| import java.util.logging.Logger; | |||
| /** | |||
| * An offline mapper from phone numbers to time zones. | |||
| * | |||
| * @author Walter Erquinigo | |||
| */ | |||
| public class PhoneNumberToTimeZonesMapper { | |||
| private static PhoneNumberToTimeZonesMapper instance = null; | |||
| private static final String MAPPING_DATA_DIRECTORY = | |||
| "/com/google/i18n/phonenumbers/timezones/data/"; | |||
| private static final String MAPPING_DATA_FILE_NAME = "map_data"; | |||
| // This is defined by ICU as the unknown time zone. | |||
| private static final String UNKNOWN_TIMEZONE = "Etc/Unknown"; | |||
| // A list with the ICU unknown time zone as single element. | |||
| // @VisibleForTesting | |||
| static final List<String> UNKNOWN_TIME_ZONE_LIST = new ArrayList<String>(1); | |||
| static { | |||
| UNKNOWN_TIME_ZONE_LIST.add(UNKNOWN_TIMEZONE); | |||
| } | |||
| private static final Logger LOGGER = | |||
| Logger.getLogger(PhoneNumberToTimeZonesMapper.class.getName()); | |||
| private PrefixTimeZonesMap prefixTimeZonesMap = null; | |||
| // @VisibleForTesting | |||
| PhoneNumberToTimeZonesMapper(String prefixTimeZonesMapDataDirectory) { | |||
| this.prefixTimeZonesMap = loadPrefixTimeZonesMapFromFile( | |||
| prefixTimeZonesMapDataDirectory + MAPPING_DATA_FILE_NAME); | |||
| } | |||
| private PhoneNumberToTimeZonesMapper(PrefixTimeZonesMap prefixTimeZonesMap) { | |||
| this.prefixTimeZonesMap = prefixTimeZonesMap; | |||
| } | |||
| private static PrefixTimeZonesMap loadPrefixTimeZonesMapFromFile(String path) { | |||
| InputStream source = PhoneNumberToTimeZonesMapper.class.getResourceAsStream(path); | |||
| ObjectInputStream in = null; | |||
| PrefixTimeZonesMap map = new PrefixTimeZonesMap(); | |||
| try { | |||
| in = new ObjectInputStream(source); | |||
| map.readExternal(in); | |||
| } catch (IOException e) { | |||
| LOGGER.log(Level.WARNING, e.toString()); | |||
| } finally { | |||
| close(in); | |||
| } | |||
| return map; | |||
| } | |||
| private static void close(InputStream in) { | |||
| if (in != null) { | |||
| try { | |||
| in.close(); | |||
| } catch (IOException e) { | |||
| LOGGER.log(Level.WARNING, e.toString()); | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Helper class used for lazy instantiation of a PhoneNumberToTimeZonesMapper. This also loads the | |||
| * map data in a thread-safe way. | |||
| */ | |||
| private static class LazyHolder { | |||
| private static final PhoneNumberToTimeZonesMapper INSTANCE; | |||
| static { | |||
| PrefixTimeZonesMap map = | |||
| loadPrefixTimeZonesMapFromFile(MAPPING_DATA_DIRECTORY + MAPPING_DATA_FILE_NAME); | |||
| INSTANCE = new PhoneNumberToTimeZonesMapper(map); | |||
| } | |||
| } | |||
| /** | |||
| * Gets a {@link PhoneNumberToTimeZonesMapper} instance. | |||
| * | |||
| * <p> The {@link PhoneNumberToTimeZonesMapper} is implemented as a singleton. Therefore, calling | |||
| * this method multiple times will only result in one instance being created. | |||
| * | |||
| * @return a {@link PhoneNumberToTimeZonesMapper} instance | |||
| */ | |||
| public static synchronized PhoneNumberToTimeZonesMapper getInstance() { | |||
| return LazyHolder.INSTANCE; | |||
| } | |||
| /** | |||
| * Returns a list of time zones to which a phone number belongs. | |||
| * | |||
| * <p>This method assumes the validity of the number passed in has already been checked, and that | |||
| * the number is geo-localizable. We consider fixed-line and mobile numbers possible candidates | |||
| * for geo-localization. | |||
| * | |||
| * @param number a valid phone number for which we want to get the time zones to which it belongs | |||
| * @return a list of the corresponding time zones or a single element list with the default | |||
| * unknown time zone if no other time zone was found or if the number was invalid | |||
| */ | |||
| public List<String> getTimeZonesForGeographicalNumber(PhoneNumber number) { | |||
| return getTimeZonesForGeocodableNumber(number); | |||
| } | |||
| /** | |||
| * As per {@link #getTimeZonesForGeographicalNumber(PhoneNumber)} but explicitly checks | |||
| * the validity of the number passed in. | |||
| * | |||
| * @param number the phone number for which we want to get the time zones to which it belongs | |||
| * @return a list of the corresponding time zones or a single element list with the default | |||
| * unknown time zone if no other time zone was found or if the number was invalid | |||
| */ | |||
| public List<String> getTimeZonesForNumber(PhoneNumber number) { | |||
| PhoneNumberType numberType = PhoneNumberUtil.getInstance().getNumberType(number); | |||
| if (numberType == PhoneNumberType.UNKNOWN) { | |||
| return UNKNOWN_TIME_ZONE_LIST; | |||
| } else if (!canBeGeocoded(numberType)) { | |||
| return getCountryLevelTimeZonesforNumber(number); | |||
| } | |||
| return getTimeZonesForGeographicalNumber(number); | |||
| } | |||
| /** | |||
| * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a | |||
| * stricter check, as it determines if a number has a geographical association. Also, if new | |||
| * phone number types were added, we should check if this other method should be updated too. | |||
| * TODO: Remove duplication by completing the logic in the method in PhoneNumberUtil. | |||
| * For more information, see the comments in that method. | |||
| */ | |||
| private boolean canBeGeocoded(PhoneNumberType numberType) { | |||
| return (numberType == PhoneNumberType.FIXED_LINE || | |||
| numberType == PhoneNumberType.MOBILE || | |||
| numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE); | |||
| } | |||
| /** | |||
| * Returns a String with the ICU unknown time zone. | |||
| */ | |||
| public static String getUnknownTimeZone() { | |||
| return UNKNOWN_TIMEZONE; | |||
| } | |||
| /** | |||
| * Returns a list of time zones to which a geocodable phone number belongs. | |||
| * | |||
| * @param number the phone number for which we want to get the time zones to which it belongs | |||
| * @return the list of corresponding time zones or a single element list with the default | |||
| * unknown time zone if no other time zone was found or if the number was invalid | |||
| */ | |||
| private List<String> getTimeZonesForGeocodableNumber(PhoneNumber number) { | |||
| List<String> timezones = prefixTimeZonesMap.lookupTimeZonesForNumber(number); | |||
| return Collections.unmodifiableList(timezones.isEmpty() ? UNKNOWN_TIME_ZONE_LIST | |||
| : timezones); | |||
| } | |||
| /** | |||
| * Returns the list of time zones corresponding to the country calling code of {@code number}. | |||
| * | |||
| * @param number the phone number to look up | |||
| * @return the list of corresponding time zones or a single element list with the default | |||
| * unknown time zone if no other time zone was found | |||
| */ | |||
| private List<String> getCountryLevelTimeZonesforNumber(PhoneNumber number) { | |||
| List<String> timezones = prefixTimeZonesMap.lookupCountryLevelTimeZonesForNumber(number); | |||
| return Collections.unmodifiableList(timezones.isEmpty() ? UNKNOWN_TIME_ZONE_LIST | |||
| : timezones); | |||
| } | |||
| } | |||
| @ -0,0 +1,136 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers; | |||
| import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; | |||
| import junit.framework.TestCase; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| /** | |||
| * Unit tests for PhoneNumberToTimeZonesMapper.java | |||
| * | |||
| * @author Walter Erquinigo | |||
| */ | |||
| public class PhoneNumberToTimeZonesMapperTest extends TestCase { | |||
| private final PhoneNumberToTimeZonesMapper prefixTimeZonesMapper = | |||
| new PhoneNumberToTimeZonesMapper(TEST_MAPPING_DATA_DIRECTORY); | |||
| private static final String TEST_MAPPING_DATA_DIRECTORY = | |||
| "/com/google/i18n/phonenumbers/timezones/testing_data/"; | |||
| // Set up some test numbers to re-use. | |||
| private static final PhoneNumber AU_NUMBER = | |||
| new PhoneNumber().setCountryCode(61).setNationalNumber(236618300L); | |||
| private static final PhoneNumber CA_NUMBER = | |||
| new PhoneNumber().setCountryCode(1).setNationalNumber(6048406565L); | |||
| private static final PhoneNumber KO_NUMBER = | |||
| new PhoneNumber().setCountryCode(82).setNationalNumber(22123456L); | |||
| private static final PhoneNumber KO_INVALID_NUMBER = | |||
| new PhoneNumber().setCountryCode(82).setNationalNumber(1234L); | |||
| private static final PhoneNumber US_NUMBER1 = | |||
| new PhoneNumber().setCountryCode(1).setNationalNumber(6509600000L); | |||
| private static final PhoneNumber US_NUMBER2 = | |||
| new PhoneNumber().setCountryCode(1).setNationalNumber(2128120000L); | |||
| private static final PhoneNumber US_NUMBER3 = | |||
| new PhoneNumber().setCountryCode(1).setNationalNumber(6174240000L); | |||
| private static final PhoneNumber US_INVALID_NUMBER = | |||
| new PhoneNumber().setCountryCode(1).setNationalNumber(123456789L); | |||
| private static final PhoneNumber NUMBER_WITH_INVALID_COUNTRY_CODE = | |||
| new PhoneNumber().setCountryCode(999).setNationalNumber(2423651234L); | |||
| private static final PhoneNumber INTERNATIONAL_TOLL_FREE = | |||
| new PhoneNumber().setCountryCode(800).setNationalNumber(12345678L); | |||
| // NANPA time zones. | |||
| private static final String CHICAGO_TZ = "America/Chicago"; | |||
| private static final String LOS_ANGELES_TZ = "America/Los_Angeles"; | |||
| private static final String NEW_YORK_TZ = "America/New_York"; | |||
| private static final String WINNIPEG_TZ = "America/Winnipeg"; | |||
| // Non NANPA time zones. | |||
| private static final String SEOUL_TZ = "Asia/Seoul"; | |||
| private static final String SYDNEY_TZ = "Australia/Sydney"; | |||
| static List<String> buildListOfTimeZones(String ... timezones) { | |||
| ArrayList<String> timezonesList = new ArrayList<String>(timezones.length); | |||
| for (String timezone : timezones) { | |||
| timezonesList.add(timezone); | |||
| } | |||
| return timezonesList; | |||
| } | |||
| private static List<String> getNanpaTimeZonesList() { | |||
| return buildListOfTimeZones(NEW_YORK_TZ, CHICAGO_TZ, WINNIPEG_TZ, LOS_ANGELES_TZ); | |||
| } | |||
| public void testGetTimeZonesForNumber() { | |||
| // Test with invalid numbers even when their country code prefixes exist in the mapper. | |||
| assertEquals(PhoneNumberToTimeZonesMapper.UNKNOWN_TIME_ZONE_LIST, | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(US_INVALID_NUMBER)); | |||
| assertEquals(PhoneNumberToTimeZonesMapper.UNKNOWN_TIME_ZONE_LIST, | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(KO_INVALID_NUMBER)); | |||
| // Test with valid prefixes. | |||
| assertEquals(buildListOfTimeZones(SYDNEY_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(AU_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(SEOUL_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(KO_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(WINNIPEG_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(CA_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(LOS_ANGELES_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(US_NUMBER1)); | |||
| assertEquals(buildListOfTimeZones(NEW_YORK_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(US_NUMBER2)); | |||
| // Test with an invalid country code. | |||
| assertEquals(PhoneNumberToTimeZonesMapper.UNKNOWN_TIME_ZONE_LIST, | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(NUMBER_WITH_INVALID_COUNTRY_CODE)); | |||
| // Test with a non geographical phone number. | |||
| assertEquals(PhoneNumberToTimeZonesMapper.UNKNOWN_TIME_ZONE_LIST, | |||
| prefixTimeZonesMapper.getTimeZonesForNumber(INTERNATIONAL_TOLL_FREE)); | |||
| } | |||
| public void testGetTimeZonesForValidNumber() { | |||
| // Test with invalid numbers even when their country code prefixes exist in the mapper. | |||
| assertEquals(getNanpaTimeZonesList(), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(US_INVALID_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(SEOUL_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(KO_INVALID_NUMBER)); | |||
| // Test with valid prefixes. | |||
| assertEquals(buildListOfTimeZones(SYDNEY_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(AU_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(SEOUL_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(KO_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(WINNIPEG_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(CA_NUMBER)); | |||
| assertEquals(buildListOfTimeZones(LOS_ANGELES_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(US_NUMBER1)); | |||
| assertEquals(buildListOfTimeZones(NEW_YORK_TZ), | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber(US_NUMBER2)); | |||
| // Test with an invalid country code. | |||
| assertEquals(PhoneNumberToTimeZonesMapper.UNKNOWN_TIME_ZONE_LIST, | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber( | |||
| NUMBER_WITH_INVALID_COUNTRY_CODE)); | |||
| // Test with a non geographical phone number. | |||
| assertEquals(PhoneNumberToTimeZonesMapper.UNKNOWN_TIME_ZONE_LIST, | |||
| prefixTimeZonesMapper.getTimeZonesForGeographicalNumber( | |||
| INTERNATIONAL_TOLL_FREE)); | |||
| } | |||
| public void testGetTimeZonesForValidNumberSearchingAtCountryCodeLevel() { | |||
| // Test that the country level time zones are returned when the number passed in is valid but | |||
| // not covered by any non-country level prefixes in the mapper. | |||
| assertEquals(prefixTimeZonesMapper.getTimeZonesForNumber(US_NUMBER3), | |||
| getNanpaTimeZonesList()); | |||
| } | |||
| } | |||
| @ -0,0 +1,128 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers.prefixmapper; | |||
| import com.google.i18n.phonenumbers.PhoneNumberUtil; | |||
| import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; | |||
| import java.io.Externalizable; | |||
| import java.io.IOException; | |||
| import java.io.ObjectInput; | |||
| import java.io.ObjectOutput; | |||
| import java.util.LinkedList; | |||
| import java.util.List; | |||
| import java.util.SortedMap; | |||
| import java.util.StringTokenizer; | |||
| /** | |||
| * A utility that maps phone number prefixes to a list of strings describing the time zones to | |||
| * which each prefix belongs. | |||
| * | |||
| * @author Walter Erquinigo | |||
| */ | |||
| public class PrefixTimeZonesMap implements Externalizable { | |||
| private final PhonePrefixMap phonePrefixMap = new PhonePrefixMap(); | |||
| private static final String RAW_STRING_TIMEZONES_SEPARATOR = "&"; | |||
| /** | |||
| * Creates a {@link PrefixTimeZoneMap} initialized with {@code sortedPrefixTimeZoneMap}. Note | |||
| * that the underlying implementation of this method is expensive thus should not be called by | |||
| * time-critical applications. | |||
| * | |||
| * @param sortedPrefixTimeZoneMap a map from phone number prefixes to their corresponding time | |||
| * zones, sorted in ascending order of the phone number prefixes as integers. | |||
| */ | |||
| public void readPrefixTimeZonesMap(SortedMap<Integer, String> sortedPrefixTimeZoneMap) { | |||
| phonePrefixMap.readPhonePrefixMap(sortedPrefixTimeZoneMap); | |||
| } | |||
| /** | |||
| * Supports Java Serialization. | |||
| */ | |||
| public void writeExternal(ObjectOutput objectOutput) throws IOException { | |||
| phonePrefixMap.writeExternal(objectOutput); | |||
| } | |||
| public void readExternal(ObjectInput objectInput) throws IOException { | |||
| phonePrefixMap.readExternal(objectInput); | |||
| } | |||
| /** | |||
| * Returns the list of time zones {@code key} corresponds to. | |||
| * | |||
| * <p>{@code key} could be the calling country code and the full significant number of a | |||
| * certain number, or it could be just a phone-number prefix. | |||
| * For example, the full number 16502530000 (from the phone-number +1 650 253 0000) is a valid | |||
| * input. Also, any of its prefixes, such as 16502, is also valid. | |||
| * | |||
| * @param key the key to look up | |||
| * @return the list of corresponding time zones | |||
| */ | |||
| private List<String> lookupTimeZonesForNumber(long key) { | |||
| // Lookup in the map data. The returned String may consist of several time zones, so it must be | |||
| // split. | |||
| String timezonesString = phonePrefixMap.lookup(key); | |||
| if (timezonesString == null) { | |||
| return new LinkedList<String>(); | |||
| } | |||
| return tokenizeRawOutputString(timezonesString); | |||
| } | |||
| /** | |||
| * As per {@link #lookupTimeZonesForNumber(long)}, but receives the number as a PhoneNumber | |||
| * instead of a long. | |||
| * | |||
| * @param number the phone number to look up | |||
| * @return the list of corresponding time zones | |||
| */ | |||
| public List<String> lookupTimeZonesForNumber(PhoneNumber number) { | |||
| long phonePrefix = Long.parseLong(number.getCountryCode() + | |||
| PhoneNumberUtil.getInstance().getNationalSignificantNumber(number)); | |||
| return lookupTimeZonesForNumber(phonePrefix); | |||
| } | |||
| /** | |||
| * Returns the list of time zones {@code number}'s calling country code corresponds to. | |||
| * | |||
| * @param number the phone number to look up | |||
| * @return the list of corresponding time zones | |||
| */ | |||
| public List<String> lookupCountryLevelTimeZonesForNumber(PhoneNumber number) { | |||
| return lookupTimeZonesForNumber(number.getCountryCode()); | |||
| } | |||
| /** | |||
| * Split {@code timezonesString} into all the time zones that are part of it. | |||
| */ | |||
| private List<String> tokenizeRawOutputString(String timezonesString) { | |||
| StringTokenizer tokenizer = new StringTokenizer(timezonesString, | |||
| RAW_STRING_TIMEZONES_SEPARATOR); | |||
| LinkedList<String> timezonesList = new LinkedList<String>(); | |||
| while (tokenizer.hasMoreTokens()) { | |||
| timezonesList.add(tokenizer.nextToken()); | |||
| } | |||
| return timezonesList; | |||
| } | |||
| /** | |||
| * Dumps the mappings contained in the phone prefix map. | |||
| */ | |||
| @Override | |||
| public String toString() { | |||
| return phonePrefixMap.toString(); | |||
| } | |||
| } | |||
| @ -0,0 +1,178 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers.prefixmapper; | |||
| import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; | |||
| import junit.framework.TestCase; | |||
| import java.io.ByteArrayInputStream; | |||
| import java.io.ByteArrayOutputStream; | |||
| import java.io.IOException; | |||
| import java.io.ObjectInputStream; | |||
| import java.io.ObjectOutputStream; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import java.util.SortedMap; | |||
| import java.util.TreeMap; | |||
| /** | |||
| * Unittests for PrefixTimeZonesMap.java | |||
| * | |||
| * @author Walter Erquinigo | |||
| */ | |||
| public class PrefixTimeZonesMapTest extends TestCase { | |||
| private final PrefixTimeZonesMap prefixTimeZonesMapForUS = new PrefixTimeZonesMap(); | |||
| private final PrefixTimeZonesMap prefixTimeZonesMapForRU = new PrefixTimeZonesMap(); | |||
| // US time zones | |||
| private static final String CHICAGO_TZ = "America/Chicago"; | |||
| private static final String DENVER_TZ = "America/Denver"; | |||
| private static final String LOS_ANGELES_TZ = "America/Los_Angeles"; | |||
| private static final String NEW_YORK_TZ = "America/New_York"; | |||
| // Russian time zones | |||
| private static final String IRKUTSK_TZ = "Asia/Irkutsk"; | |||
| private static final String MOSCOW_TZ = "Europe/Moscow"; | |||
| private static final String VLADIVOSTOK_TZ = "Asia/Vladivostok"; | |||
| private static final String YEKATERINBURG_TZ = "Asia/Yekaterinburg"; | |||
| public PrefixTimeZonesMapTest() { | |||
| SortedMap<Integer, String> sortedMapForUS = new TreeMap<Integer, String>(); | |||
| sortedMapForUS.put(1, NEW_YORK_TZ + "&" + CHICAGO_TZ + "&" + LOS_ANGELES_TZ + "&" + DENVER_TZ); | |||
| sortedMapForUS.put(1201, NEW_YORK_TZ); | |||
| sortedMapForUS.put(1205, CHICAGO_TZ); | |||
| sortedMapForUS.put(1208292, LOS_ANGELES_TZ); | |||
| sortedMapForUS.put(1208234, DENVER_TZ); | |||
| sortedMapForUS.put(1541367, LOS_ANGELES_TZ); | |||
| sortedMapForUS.put(1423843, NEW_YORK_TZ); | |||
| sortedMapForUS.put(1402721, CHICAGO_TZ); | |||
| sortedMapForUS.put(1208888, DENVER_TZ); | |||
| prefixTimeZonesMapForUS.readPrefixTimeZonesMap(sortedMapForUS); | |||
| SortedMap<Integer, String> sortedMapForRU = new TreeMap<Integer, String>(); | |||
| sortedMapForRU.put(7421, VLADIVOSTOK_TZ); | |||
| sortedMapForRU.put(7879, MOSCOW_TZ); | |||
| sortedMapForRU.put(7342, YEKATERINBURG_TZ); | |||
| sortedMapForRU.put(7395, IRKUTSK_TZ); | |||
| prefixTimeZonesMapForRU.readPrefixTimeZonesMap(sortedMapForRU); | |||
| } | |||
| static List<String> buildListOfTimeZones(String ... timezones) { | |||
| ArrayList<String> timezonesList = new ArrayList<String>(timezones.length); | |||
| for (String timezone : timezones) { | |||
| timezonesList.add(timezone); | |||
| } | |||
| return timezonesList; | |||
| } | |||
| private static SortedMap<Integer, String> createMapCandidate() { | |||
| SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>(); | |||
| sortedMap.put(1212, NEW_YORK_TZ); | |||
| sortedMap.put(1213, NEW_YORK_TZ); | |||
| sortedMap.put(1214, NEW_YORK_TZ); | |||
| sortedMap.put(1480, CHICAGO_TZ); | |||
| return sortedMap; | |||
| } | |||
| public void testLookupTimeZonesForNumberCountryLevel_US() { | |||
| PhoneNumber number = new PhoneNumber(); | |||
| number.setCountryCode(1).setNationalNumber(1000000000L); | |||
| assertEquals(buildListOfTimeZones(NEW_YORK_TZ, CHICAGO_TZ, LOS_ANGELES_TZ, DENVER_TZ), | |||
| prefixTimeZonesMapForUS.lookupTimeZonesForNumber(number)); | |||
| } | |||
| public void testLookupTimeZonesForNumber_ValidNumber_Chicago() { | |||
| PhoneNumber number = new PhoneNumber(); | |||
| number.setCountryCode(1).setNationalNumber(2051235458L); | |||
| assertEquals(buildListOfTimeZones(CHICAGO_TZ), | |||
| prefixTimeZonesMapForUS.lookupTimeZonesForNumber(number)); | |||
| } | |||
| public void testLookupTimeZonesForNumber_LA() { | |||
| PhoneNumber number = new PhoneNumber(); | |||
| number.setCountryCode(1).setNationalNumber(2082924565L); | |||
| assertEquals(buildListOfTimeZones(LOS_ANGELES_TZ), | |||
| prefixTimeZonesMapForUS.lookupTimeZonesForNumber(number)); | |||
| } | |||
| public void testLookupTimeZonesForNumber_NY() { | |||
| PhoneNumber number = new PhoneNumber(); | |||
| number.setCountryCode(1).setNationalNumber(2016641234L); | |||
| assertEquals(buildListOfTimeZones(NEW_YORK_TZ), | |||
| prefixTimeZonesMapForUS.lookupTimeZonesForNumber(number)); | |||
| } | |||
| public void testLookupTimeZonesForNumber_CH() { | |||
| PhoneNumber number = new PhoneNumber(); | |||
| number.setCountryCode(41).setNationalNumber(446681300L); | |||
| assertEquals(buildListOfTimeZones(), | |||
| prefixTimeZonesMapForUS.lookupTimeZonesForNumber(number)); | |||
| } | |||
| public void testLookupTimeZonesForNumber_RU() { | |||
| PhoneNumber number = new PhoneNumber(); | |||
| number.setCountryCode(7).setNationalNumber(87945154L); | |||
| assertEquals(buildListOfTimeZones(MOSCOW_TZ), | |||
| prefixTimeZonesMapForRU.lookupTimeZonesForNumber(number)); | |||
| number.setNationalNumber(421548578L); | |||
| assertEquals(buildListOfTimeZones(VLADIVOSTOK_TZ), | |||
| prefixTimeZonesMapForRU.lookupTimeZonesForNumber(number)); | |||
| number.setNationalNumber(342457897L); | |||
| assertEquals(buildListOfTimeZones(YEKATERINBURG_TZ), | |||
| prefixTimeZonesMapForRU.lookupTimeZonesForNumber(number)); | |||
| // A mobile number | |||
| number.setNationalNumber(9342457897L); | |||
| assertEquals(buildListOfTimeZones(), | |||
| prefixTimeZonesMapForRU.lookupTimeZonesForNumber(number)); | |||
| // An invalid number (too short) | |||
| number.setNationalNumber(3951L); | |||
| assertEquals(buildListOfTimeZones(IRKUTSK_TZ), | |||
| prefixTimeZonesMapForRU.lookupTimeZonesForNumber(number)); | |||
| } | |||
| /** | |||
| * Creates a new PrefixTimeZonesMap serializing the provided map to a stream and then reading | |||
| * this stream. The resulting PrefixTimeZonesMap is expected to be strictly equal to the provided | |||
| * one from which it was generated. | |||
| */ | |||
| private static PrefixTimeZonesMap createNewPrefixTimeZonesMap( | |||
| PrefixTimeZonesMap prefixTimeZonesMap) throws IOException { | |||
| ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |||
| ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); | |||
| prefixTimeZonesMap.writeExternal(objectOutputStream); | |||
| objectOutputStream.flush(); | |||
| PrefixTimeZonesMap newPrefixTimeZonesMap = new PrefixTimeZonesMap(); | |||
| newPrefixTimeZonesMap.readExternal( | |||
| new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()))); | |||
| return newPrefixTimeZonesMap; | |||
| } | |||
| public void testReadWriteExternal() throws IOException { | |||
| PrefixTimeZonesMap localPrefixTimeZonesMap = new PrefixTimeZonesMap(); | |||
| localPrefixTimeZonesMap.readPrefixTimeZonesMap(createMapCandidate()); | |||
| PrefixTimeZonesMap newPrefixTimeZonesMap = createNewPrefixTimeZonesMap(localPrefixTimeZonesMap); | |||
| assertEquals(localPrefixTimeZonesMap.toString(), newPrefixTimeZonesMap.toString()); | |||
| } | |||
| } | |||
| @ -0,0 +1,24 @@ | |||
| # Copyright (C) 2012 The Libphonenumber Authors | |||
| # 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. | |||
| 1|America/New_York&America/Chicago&America/Winnipeg&America/Los_Angeles | |||
| 1201|America/New_York | |||
| 1212812|America/New_York | |||
| 1234|America/New_York | |||
| 1604|America/Winnipeg | |||
| 1617423|America/Chicago | |||
| 1650960|America/Los_Angeles | |||
| 1989|Ameriac/Los_Angeles | |||
| 612|Australia/Sydney | |||
| 82|Asia/Seoul | |||
| @ -0,0 +1,143 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers.buildtools; | |||
| import com.google.i18n.phonenumbers.prefixmapper.PrefixTimeZonesMap; | |||
| import java.io.BufferedInputStream; | |||
| import java.io.BufferedReader; | |||
| import java.io.File; | |||
| import java.io.FileInputStream; | |||
| import java.io.FileOutputStream; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.io.InputStreamReader; | |||
| import java.io.ObjectOutputStream; | |||
| import java.io.OutputStream; | |||
| import java.nio.charset.Charset; | |||
| import java.util.SortedMap; | |||
| import java.util.TreeMap; | |||
| import java.util.logging.Level; | |||
| import java.util.logging.Logger; | |||
| /** | |||
| * A utility that generates the binary serialization of the prefix/time zones mappings from | |||
| * a human-readable text file. | |||
| * | |||
| * @author Walter Erquinigo | |||
| */ | |||
| public class GenerateTimeZonesMapData { | |||
| private final File inputTextFile; | |||
| private static final String MAPPING_DATA_FILE_NAME = "map_data"; | |||
| // The IO Handler used to output the generated binary file. | |||
| private final AbstractPhonePrefixDataIOHandler ioHandler; | |||
| private static final Logger LOGGER = Logger.getLogger(GenerateTimeZonesMapData.class.getName()); | |||
| public GenerateTimeZonesMapData(File inputTextFile, AbstractPhonePrefixDataIOHandler ioHandler) | |||
| throws IOException { | |||
| this.inputTextFile = inputTextFile; | |||
| if (!inputTextFile.isFile()) { | |||
| throw new IOException("The provided input text file does not exist."); | |||
| } | |||
| this.ioHandler = ioHandler; | |||
| } | |||
| /** | |||
| * Reads phone prefix data from the provided input stream and returns a SortedMap with the | |||
| * prefix to time zones mappings. | |||
| */ | |||
| // @VisibleForTesting | |||
| static SortedMap<Integer, String> parseTextFile(InputStream input) | |||
| throws IOException, RuntimeException { | |||
| final SortedMap<Integer, String> timeZoneMap = new TreeMap<Integer, String>(); | |||
| BufferedReader bufferedReader = | |||
| new BufferedReader(new InputStreamReader( | |||
| new BufferedInputStream(input), Charset.forName("UTF-8"))); | |||
| int lineNumber = 1; | |||
| for (String line; (line = bufferedReader.readLine()) != null; lineNumber++) { | |||
| line = line.trim(); | |||
| if (line.length() == 0 || line.startsWith("#")) { | |||
| continue; | |||
| } | |||
| int indexOfPipe = line.indexOf('|'); | |||
| if (indexOfPipe == -1) { | |||
| throw new RuntimeException(String.format("line %d: malformatted data, expected '|'", | |||
| lineNumber)); | |||
| } | |||
| Integer prefix = Integer.parseInt(line.substring(0, indexOfPipe)); | |||
| String timezones = line.substring(indexOfPipe + 1); | |||
| if (timezones.isEmpty()) { | |||
| throw new RuntimeException(String.format("line %d: missing time zones", lineNumber)); | |||
| } | |||
| if (timeZoneMap.put(prefix, timezones) != null) { | |||
| throw new RuntimeException(String.format("duplicated prefix %d", prefix)); | |||
| } | |||
| } | |||
| return timeZoneMap; | |||
| } | |||
| /** | |||
| * Writes the provided phone prefix/time zones map to the provided output stream. | |||
| * | |||
| * @throws IOException | |||
| */ | |||
| // @VisibleForTesting | |||
| static void writeToBinaryFile(SortedMap<Integer, String> sortedMap, OutputStream output) | |||
| throws IOException { | |||
| // Build the corresponding PrefixTimeZonesMap and serialize it to the binary format. | |||
| PrefixTimeZonesMap prefixTimeZonesMap = new PrefixTimeZonesMap(); | |||
| prefixTimeZonesMap.readPrefixTimeZonesMap(sortedMap); | |||
| ObjectOutputStream objectOutputStream = new ObjectOutputStream(output); | |||
| prefixTimeZonesMap.writeExternal(objectOutputStream); | |||
| objectOutputStream.flush(); | |||
| } | |||
| /** | |||
| * Runs the prefix to time zones map data generator. | |||
| * | |||
| * @throws IOException | |||
| */ | |||
| public void run() throws IOException { | |||
| FileInputStream fileInputStream = null; | |||
| FileOutputStream fileOutputStream = null; | |||
| try { | |||
| fileInputStream = new FileInputStream(inputTextFile); | |||
| SortedMap<Integer, String> mappings = parseTextFile(fileInputStream); | |||
| File outputBinaryFile = ioHandler.createFile(MAPPING_DATA_FILE_NAME); | |||
| try { | |||
| fileOutputStream = new FileOutputStream(outputBinaryFile); | |||
| writeToBinaryFile(mappings, fileOutputStream); | |||
| ioHandler.addFileToOutput(outputBinaryFile); | |||
| } finally { | |||
| ioHandler.closeFile(fileOutputStream); | |||
| } | |||
| } catch (RuntimeException e) { | |||
| LOGGER.log(Level.SEVERE, | |||
| "Error processing file " + inputTextFile.getAbsolutePath()); | |||
| throw e; | |||
| } catch (IOException e) { | |||
| LOGGER.log(Level.SEVERE, e.getMessage()); | |||
| } finally { | |||
| ioHandler.closeFile(fileInputStream); | |||
| ioHandler.closeFile(fileOutputStream); | |||
| ioHandler.close(); | |||
| } | |||
| LOGGER.log(Level.INFO, "Time zone data successfully generated."); | |||
| } | |||
| } | |||
| @ -0,0 +1,60 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers.buildtools; | |||
| import com.google.i18n.phonenumbers.Command; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| import java.util.logging.Level; | |||
| import java.util.logging.Logger; | |||
| /** | |||
| * Entry point class used to invoke the generation of the binary time zone data files. | |||
| * | |||
| * @author Walter Erquinigo | |||
| * @author Philippe Liard | |||
| */ | |||
| public class GenerateTimeZonesMapDataEntryPoint extends Command { | |||
| private static final Logger LOGGER = Logger.getLogger(GenerateTimeZonesMapData.class.getName()); | |||
| @Override | |||
| public String getCommandName() { | |||
| return "GenerateTimeZonesMapData"; | |||
| } | |||
| @Override | |||
| public boolean start() { | |||
| String[] args = getArgs(); | |||
| if (args.length != 3) { | |||
| LOGGER.log(Level.SEVERE, | |||
| "usage: GenerateTimeZonesMapData /path/to/input/text_file " + | |||
| "/path/to/output/directory"); | |||
| return false; | |||
| } | |||
| try { | |||
| GenerateTimeZonesMapData generateTimeZonesMapData = new GenerateTimeZonesMapData( | |||
| new File(args[1]), new PhonePrefixDataIOHandler(new File(args[2]))); | |||
| generateTimeZonesMapData.run(); | |||
| } catch (IOException e) { | |||
| LOGGER.log(Level.SEVERE, e.getMessage()); | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| @ -0,0 +1,136 @@ | |||
| /* | |||
| * Copyright (C) 2012 The Libphonenumber Authors | |||
| * | |||
| * 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.google.i18n.phonenumbers.buildtools; | |||
| import com.google.i18n.phonenumbers.prefixmapper.PrefixTimeZonesMap; | |||
| import junit.framework.TestCase; | |||
| import java.io.ByteArrayInputStream; | |||
| import java.io.ByteArrayOutputStream; | |||
| import java.io.IOException; | |||
| import java.io.ObjectInputStream; | |||
| import java.util.Map; | |||
| import java.util.SortedMap; | |||
| /** | |||
| * Unittests for GenerateTimeZonesMapData.java | |||
| * | |||
| * @author Walter Erquinigo | |||
| */ | |||
| public class GenerateTimeZonesMapDataTest extends TestCase { | |||
| private static final String BRUSSELS_TZ = "Europe/Brussels"; | |||
| private static final String PARIS_TZ = "Europe/Paris"; | |||
| private static final String PARIS_BRUSSELS_LINES = | |||
| "322|" + BRUSSELS_TZ + "\n331|" + PARIS_TZ + "\n"; | |||
| private static SortedMap<Integer, String> parseTextFileHelper(String input) throws IOException { | |||
| return GenerateTimeZonesMapData.parseTextFile(new ByteArrayInputStream(input.getBytes())); | |||
| } | |||
| public void testParseTextFile() throws IOException { | |||
| Map<Integer, String> result = parseTextFileHelper(PARIS_BRUSSELS_LINES); | |||
| assertEquals(2, result.size()); | |||
| assertEquals(PARIS_TZ, result.get(331)); | |||
| assertEquals(BRUSSELS_TZ, result.get(322)); | |||
| } | |||
| public void testParseTextFileIgnoresComments() throws IOException { | |||
| Map<Integer, String> result = parseTextFileHelper("# Hello\n" + PARIS_BRUSSELS_LINES); | |||
| assertEquals(2, result.size()); | |||
| assertEquals(PARIS_TZ, result.get(331)); | |||
| assertEquals(BRUSSELS_TZ, result.get(322)); | |||
| } | |||
| public void testParseTextFileIgnoresBlankLines() throws IOException { | |||
| Map<Integer, String> result = parseTextFileHelper("\n" + PARIS_BRUSSELS_LINES); | |||
| assertEquals(2, result.size()); | |||
| assertEquals(PARIS_TZ, result.get(331)); | |||
| assertEquals(BRUSSELS_TZ, result.get(322)); | |||
| } | |||
| public void testParseTextFileIgnoresTrailingWhitespaces() throws IOException { | |||
| Map<Integer, String> result = parseTextFileHelper( | |||
| "331|" + PARIS_TZ + "\n322|" + BRUSSELS_TZ + " \n"); | |||
| assertEquals(2, result.size()); | |||
| assertEquals(PARIS_TZ, result.get(331)); | |||
| assertEquals(BRUSSELS_TZ, result.get(322)); | |||
| } | |||
| public void testParseTextFileThrowsExceptionWithMalformattedData() throws IOException { | |||
| try { | |||
| parseTextFileHelper("331"); | |||
| fail(); | |||
| } catch (RuntimeException e) { | |||
| // Expected. | |||
| } | |||
| } | |||
| public void testParseTextFileThrowsExceptionWithMissingTimeZone() throws IOException { | |||
| try { | |||
| parseTextFileHelper("331|"); | |||
| fail(); | |||
| } catch (RuntimeException e) { | |||
| // Expected. | |||
| } | |||
| } | |||
| // Returns a String representing the input after serialization and deserialization by | |||
| // PrefixTimeZonesMap. | |||
| private static String convertDataHelper(String input) throws IOException { | |||
| ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(input.getBytes()); | |||
| ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |||
| SortedMap<Integer, String> prefixTimeZonesMapping = parseTextFileHelper(input); | |||
| GenerateTimeZonesMapData.writeToBinaryFile(prefixTimeZonesMapping, byteArrayOutputStream); | |||
| // The byte array output stream now contains the corresponding serialized prefix to time zones | |||
| // map. Try to deserialize it and compare it with the initial input. | |||
| PrefixTimeZonesMap prefixTimeZonesMap = new PrefixTimeZonesMap(); | |||
| prefixTimeZonesMap.readExternal( | |||
| new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()))); | |||
| return prefixTimeZonesMap.toString(); | |||
| } | |||
| public void testConvertData() throws IOException { | |||
| String input = PARIS_BRUSSELS_LINES; | |||
| String dataAfterDeserialization = convertDataHelper(input); | |||
| assertEquals(input, dataAfterDeserialization); | |||
| } | |||
| public void testConvertThrowsExceptionWithMissingTimeZone() throws IOException { | |||
| String input = PARIS_BRUSSELS_LINES + "3341|\n"; | |||
| try { | |||
| String dataAfterDeserialization = convertDataHelper(input); | |||
| } catch (RuntimeException e) { | |||
| // Expected. | |||
| } | |||
| } | |||
| public void testConvertDataThrowsExceptionWithDuplicatedPrefixes() throws IOException { | |||
| String input = "331|" + PARIS_TZ + "\n331|" + BRUSSELS_TZ + "\n"; | |||
| try { | |||
| convertDataHelper(input); | |||
| fail(); | |||
| } catch (RuntimeException e) { | |||
| // Expected. | |||
| } | |||
| } | |||
| } | |||