Skip to content

Commit

Permalink
Port initial short number support
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff Lai committed Jun 7, 2022
1 parent 985507a commit aeb9e22
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 3 deletions.
3 changes: 1 addition & 2 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,12 @@ func ip(value int32) *int32 {
return &value
}

func BuildPhoneMetadataCollection(inputXML []byte, liteBuild bool, specialBuild bool) (*PhoneMetadataCollection, error) {
func BuildPhoneMetadataCollection(inputXML []byte, liteBuild bool, specialBuild bool, isShortNumberMetadata bool) (*PhoneMetadataCollection, error) {
metadata := &PhoneNumberMetadataE{}
err := xml.Unmarshal(inputXML, metadata)
if err != nil {
panic(fmt.Sprintf("Error unmarshalling XML: %s", err))
}
isShortNumberMetadata := false
isAlternateFormatsMetadata := false
return buildPhoneMetadataFromElement(metadata, liteBuild, specialBuild, isShortNumberMetadata, isAlternateFormatsMetadata)
}
Expand Down
27 changes: 26 additions & 1 deletion cmd/buildmetadata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const (
metadataURL = "https://raw.githubusercontent.com/googlei18n/libphonenumber/master/resources/PhoneNumberMetadata.xml"
metadataPath = "metadata_bin.go"

shortNumberMetadataURL = "https://raw.githubusercontent.com/googlei18n/libphonenumber/master/resources/ShortNumberMetadata.xml"
shortNumberMetadataPath = "shortnumber_metadata_bin.go"

tzURL = "https://raw.githubusercontent.com/googlei18n/libphonenumber/master/resources/timezones/map_data.txt"
tzPath = "prefix_to_timezone_bin.go"
tzVar = "timezoneMapData"
Expand Down Expand Up @@ -248,7 +251,7 @@ func buildMetadata() *phonenumbers.PhoneMetadataCollection {
body := fetchURL(metadataURL)

log.Println("Building new metadata collection")
collection, err := phonenumbers.BuildPhoneMetadataCollection(body, false, false)
collection, err := phonenumbers.BuildPhoneMetadataCollection(body, false, false, false)
if err != nil {
log.Fatalf("Error converting XML: %s", err)
}
Expand All @@ -264,6 +267,27 @@ func buildMetadata() *phonenumbers.PhoneMetadataCollection {
return collection
}

func buildShortNumberMetadata() *phonenumbers.PhoneMetadataCollection {
log.Println("Fetching ShortNumberMetadata.xml from Github")
body := fetchURL(shortNumberMetadataURL)

log.Println("Building new short number metadata collection")
collection, err := phonenumbers.BuildPhoneMetadataCollection(body, false, false, true)
if err != nil {
log.Fatalf("Error converting XML: %s", err)
}

// write it out as a protobuf
data, err := proto.Marshal(collection)
if err != nil {
log.Fatalf("Error marshalling metadata: %v", err)
}

log.Println("Writing new metadata_bin.go")
writeFile(shortNumberMetadataPath, generateBinFile("shortNumberMetadataData", data))
return collection
}

// generates the file contents for a data file
func generateBinFile(variableName string, data []byte) []byte {
var compressed bytes.Buffer
Expand Down Expand Up @@ -455,6 +479,7 @@ func readMappingsForDir(dir string) map[int]string {

func main() {
metadata := buildMetadata()
buildShortNumberMetadata()
buildRegions(metadata)
buildTimezones()
buildPrefixData(&carrier)
Expand Down
19 changes: 19 additions & 0 deletions matcher.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package phonenumbers

import (
"regexp"
"strconv"
"strings"
"unicode"
Expand Down Expand Up @@ -208,3 +209,21 @@ func AllNumberGroupsAreExactlyPresent(
strings.HasSuffix(candidateGroups[candidateNumberGroupIndex],
formattedNumberGroups[0]))
}

// Returns whether the given national number (a string containing only decimal digits) matches
// the national number pattern defined in the given PhoneNumberDesc message.
func MatchNationalNumber(number string, numberDesc PhoneNumberDesc, allowPrefixMatch bool) bool {
nationalNumberPattern := numberDesc.GetNationalNumberPattern()
// We don't want to consider it a prefix match when matching non-empty input against an empty pattern.
if len(nationalNumberPattern) == 0 {
return false
}
patP := `^(?:` + nationalNumberPattern + `)$` // Strictly match
regex := regexFor(patP)
return match(number, regex, allowPrefixMatch)
}

// TODO: review this matches java version?
func match(number string, pattern *regexp.Regexp, allowPrefixMatch bool) bool {
return pattern.MatchString(number)
}
184 changes: 184 additions & 0 deletions shortnumber_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package phonenumbers

import (
proto "github.com/golang/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(shortNumberMetadataData)
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)
}
74 changes: 74 additions & 0 deletions shortnumber_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package phonenumbers

import (
"testing"

"github.com/stretchr/testify/assert"
)

////////// Copied from java-libphonenumber
/**
* Unit tests for ShortNumberInfo.java
*/

func TestIsPossibleShortNumber(t *testing.T) {
countryCode := int32(33)
nationalNumber := uint64(123456)
possibleNumber := &PhoneNumber{
CountryCode: &countryCode,
NationalNumber: &nationalNumber,
}
assert.True(t, IsPossibleShortNumber(*possibleNumber))

possibleNumber, err := Parse("123456", "FR")
if err != nil {
t.Errorf("Error parsing number: %s: %s", "123456", err)
}
assert.True(t, IsPossibleShortNumberForRegion(*possibleNumber, "FR"))

nationalNumber = 9
impossibleNumber := &PhoneNumber{
CountryCode: &countryCode,
NationalNumber: &nationalNumber,
}
assert.False(t, IsPossibleShortNumber(*impossibleNumber))

// Note that GB and GG share the country calling code 44, and that this number is possible but
// not valid.
countryCode = 44
nationalNumber = 11001
possibleNumber = &PhoneNumber{
CountryCode: &countryCode,
NationalNumber: &nationalNumber,
}
assert.True(t, IsPossibleShortNumber(*possibleNumber))
}

func TestIsValidShortNumber(t *testing.T) {
countryCode := int32(33)
nationalNumber := uint64(1010)
validNumber := &PhoneNumber{
CountryCode: &countryCode,
NationalNumber: &nationalNumber,
}
assert.True(t, IsValidShortNumber(*validNumber))

validNumber, err := Parse("1010", "FR")
if err != nil {
t.Errorf("Error parsing number: %s: %s", "1010", err)
}
assert.True(t, IsValidShortNumberForRegion(*validNumber, "FR"))

nationalNumber = uint64(123456)
invalidNumber := &PhoneNumber{
CountryCode: &countryCode,
NationalNumber: &nationalNumber,
}
assert.False(t, IsValidShortNumber(*invalidNumber))

invalidNumber, err = Parse("123456", "FR")
if err != nil {
t.Errorf("Error parsing number: %s: %s", "1010", err)
}
assert.False(t, IsValidShortNumberForRegion(*invalidNumber, "FR"))
}
Loading

0 comments on commit aeb9e22

Please sign in to comment.