shortnumber_info.go (166 lines of code) (raw):
package phonenumbers
import (
"golang.org/x/exp/slices"
"github.com/nyaruka/phonenumbers/gen"
"google.golang.org/protobuf/proto"
)
var (
shortNumberRegionToMetadataMap = make(map[string]*PhoneMetadata)
)
func readFromShortNumberRegionToMetadataMap(key string) (*PhoneMetadata, bool) {
v, ok := shortNumberRegionToMetadataMap[key]
return v, ok
}
func writeToShortNumberRegionToMetadataMap(key string, val *PhoneMetadata) {
shortNumberRegionToMetadataMap[key] = val
}
func init() {
err := loadShortNumberMetadataFromFile()
if err != nil {
panic(err)
}
}
var (
currShortNumberMetadataColl *PhoneMetadataCollection
shortNumberReloadMetadata = true
)
func ShortNumberMetadataCollection() (*PhoneMetadataCollection, error) {
if !shortNumberReloadMetadata {
return currShortNumberMetadataColl, nil
}
rawBytes, err := decodeUnzipString(gen.ShortNumberData)
if err != nil {
return nil, err
}
metadataCollection := &PhoneMetadataCollection{}
err = proto.Unmarshal(rawBytes, metadataCollection)
shortNumberReloadMetadata = false
return metadataCollection, err
}
func loadShortNumberMetadataFromFile() error {
metadataCollection, err := ShortNumberMetadataCollection()
if err != nil {
return err
} else if currShortNumberMetadataColl == nil {
currShortNumberMetadataColl = metadataCollection
}
metadataList := metadataCollection.GetMetadata()
if len(metadataList) == 0 {
return ErrEmptyMetadata
}
for _, meta := range metadataList {
region := meta.GetId()
if region == "001" {
// it's a non geographical entity, unused
} else {
writeToShortNumberRegionToMetadataMap(region, meta)
}
}
return nil
}
// Check whether a short number is a possible number. If a country calling code is shared by
// multiple regions, this returns true if it's possible in any of them. This provides a more
// lenient check than #isValidShortNumber.
// See IsPossibleShortNumberForRegion(PhoneNumber, string) for details.
func IsPossibleShortNumber(number *PhoneNumber) bool {
regionsCodes := GetRegionCodesForCountryCode(int(number.GetCountryCode()))
shortNumberLength := len(GetNationalSignificantNumber(number))
for _, region := range regionsCodes {
phoneMetadata := getShortNumberMetadataForRegion(region)
if phoneMetadata == nil {
continue
}
if phoneMetadata.GeneralDesc.hasPossibleLength(int32(shortNumberLength)) {
return true
}
}
return false
}
// Check whether a short number is a possible number when dialed from the given region. This
// provides a more lenient check than IsValidShortNumberForRegion.
func IsPossibleShortNumberForRegion(number *PhoneNumber, regionDialingFrom string) bool {
if !regionDialingFromMatchesNumber(number, regionDialingFrom) {
return false
}
phoneMetadata := getShortNumberMetadataForRegion(regionDialingFrom)
if phoneMetadata == nil {
return false
}
numberLength := len(GetNationalSignificantNumber(number))
return phoneMetadata.GeneralDesc.hasPossibleLength(int32(numberLength))
}
// Tests whether a short number matches a valid pattern. If a country calling code is shared by
// multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify
// the number is actually in use, which is impossible to tell by just looking at the number
// itself. See IsValidShortNumberForRegion(PhoneNumber, String) for details.
func IsValidShortNumber(number *PhoneNumber) bool {
regionCodes := GetRegionCodesForCountryCode(int(number.GetCountryCode()))
regionCode := getRegionCodeForShortNumberFromRegionList(number, regionCodes)
if len(regionCodes) > 1 && regionCode != "" {
// If a matching region had been found for the phone number from among two or more regions,
// then we have already implicitly verified its validity for that region.
return true
}
return IsValidShortNumberForRegion(number, regionCode)
}
// Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify
// the number is actually in use, which is impossible to tell by just looking at the number itself.
func IsValidShortNumberForRegion(number *PhoneNumber, regionDialingFrom string) bool {
if !regionDialingFromMatchesNumber(number, regionDialingFrom) {
return false
}
phoneMetadata := getShortNumberMetadataForRegion(regionDialingFrom)
if phoneMetadata == nil {
return false
}
shortNumber := GetNationalSignificantNumber(number)
generalDesc := phoneMetadata.GeneralDesc
if !matchesPossibleNumberAndNationalNumber(shortNumber, generalDesc) {
return false
}
shortNumberDesc := phoneMetadata.GetShortCode()
return matchesPossibleNumberAndNationalNumber(shortNumber, shortNumberDesc)
}
func getShortNumberMetadataForRegion(regionCode string) *PhoneMetadata {
val, _ := readFromShortNumberRegionToMetadataMap(regionCode)
return val
}
func getRegionCodeForShortNumberFromRegionList(number *PhoneNumber, regionCodes []string) string {
if len(regionCodes) == 0 {
return ""
}
if len(regionCodes) == 1 {
return regionCodes[0]
}
nationalNumber := GetNationalSignificantNumber(number)
for _, regionCode := range regionCodes {
phoneMetadata := getShortNumberMetadataForRegion(regionCode)
if phoneMetadata != nil && matchesPossibleNumberAndNationalNumber(nationalNumber, phoneMetadata.GetShortCode()) {
// The number is valid for this region.
return regionCode
}
}
return ""
}
// Helper method to check that the country calling code of the number matches the region it's
// being dialed from.
func regionDialingFromMatchesNumber(number *PhoneNumber, regionDialingFrom string) bool {
regionCodes := GetRegionCodesForCountryCode(int(number.GetCountryCode()))
for _, region := range regionCodes {
if region == regionDialingFrom {
return true
}
}
return false
}
// TODO: Once we have benchmarked ShortNumberInfo, consider if it is worth keeping
// this performance optimization.
func matchesPossibleNumberAndNationalNumber(number string, numberDesc *PhoneNumberDesc) bool {
if numberDesc == nil {
return false
}
if len(numberDesc.PossibleLength) > 0 && !numberDesc.hasPossibleLength(int32(len(number))) {
return false
}
return MatchNationalNumber(number, *numberDesc, false)
}
// In these countries, if extra digits are added to an emergency number, it no longer connects
// to the emergency service.
var REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = []string{"BR", "CL", "NI"}
func matchesEmergencyNumber(number string, regionCode string, allowPrefixMatch bool) bool {
possibleNumber := extractPossibleNumber(number)
// Returns false if the number starts with a plus sign. We don't believe dialing the country
// code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
// add additional logic here to handle it.
if PLUS_CHARS_PATTERN.MatchString(possibleNumber) {
return false
}
phoneMetadata := getShortNumberMetadataForRegion(regionCode)
if phoneMetadata == nil || phoneMetadata.GetEmergency() == nil {
return false
}
normalizedNumber := NormalizeDigitsOnly(possibleNumber)
allowPrefixMatchForRegion := allowPrefixMatch && !slices.Contains(REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT, regionCode)
return MatchNationalNumber(normalizedNumber, *phoneMetadata.GetEmergency(), allowPrefixMatchForRegion)
}
// Returns true if the given number exactly matches an emergency service number in the given
// region.
//
// This method takes into account cases where the number might contain formatting, but doesn't
// allow additional digits to be appended. Note that isEmergencyNumber(number, region)
// implies connectsToEmergencyNumber(number, region).
//
// number: the phone number to test
// regionCode: the region where the phone number is being dialed
// return: whether the number exactly matches an emergency services number in the given region
func IsEmergencyNumber(number string, regionCode string) bool {
return matchesEmergencyNumber(number, regionCode, false)
}
// Returns true if the given number, exactly as dialed, might be used to connect to an emergency
// service in the given region.
//
// This method accepts a string, rather than a PhoneNumber, because it needs to distinguish
// cases such as "+1 911" and "911", where the former may not connect to an emergency service in
// all cases but the latter would. This method takes into account cases where the number might
// contain formatting, or might have additional digits appended (when it is okay to do that in
// the specified region).
//
// number: the phone number to test
// regionCode: the region where the phone number is being dialed
// return: whether the number might be used to connect to an emergency service in the given region
func ConnectsToEmergencyNumber(number string, regionCode string) bool {
return matchesEmergencyNumber(number, regionCode, true)
}