Skip to content

Commit

Permalink
Merge pull request #191 from S4more/implement-emergency-shortnumber-m…
Browse files Browse the repository at this point in the history
…etadata

IsEmergency functions for short numbers
  • Loading branch information
rowanseymour authored Jan 18, 2025
2 parents a1a3a19 + 5f7382b commit a8597be
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 7 deletions.
6 changes: 3 additions & 3 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,8 @@ type PhoneNumberMetadataE struct {
}

// <!ELEMENT territory (references?, availableFormats?, generalDesc, noInternationalDialling?,
// fixedLine?, mobile?, pager?, tollFree?, premiumRate?,
// sharedCost?, personalNumber?, voip?, uan?, voicemail?)>
//fixedLine?, mobile?, pager?, tollFree?, premiumRate?,
//sharedCost?, personalNumber?, voip?, uan?, voicemail?)>
type TerritoryE struct {
// <!ATTLIST territory id CDATA #REQUIRED>
ID string `xml:"id,attr"`
Expand Down Expand Up @@ -670,7 +670,7 @@ type TerritoryE struct {
ShortCode *PhoneNumberDescE `xml:"shortCode"`

// <!ELEMENT uan (nationalNumberPattern, possibleLengths, exampleNumber)>
Emergency *PhoneNumberDescE `xml:"Emergency"`
Emergency *PhoneNumberDescE `xml:"emergency"`

// <!ELEMENT voicemail (nationalNumberPattern, possibleLengths, exampleNumber)>
CarrierSpecific *PhoneNumberDescE `xml:"carrierSpecific"`
Expand Down
2 changes: 1 addition & 1 deletion gen/metadata_bin.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gen/prefix_to_carriers_bin.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gen/prefix_to_timezone_bin.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gen/shortnumber_metadata_bin.go

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions shortnumber_info.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package phonenumbers

import (
"golang.org/x/exp/slices"

"github.com/nyaruka/phonenumbers/gen"
"google.golang.org/protobuf/proto"
)
Expand Down Expand Up @@ -183,3 +185,57 @@ func matchesPossibleNumberAndNationalNumber(number string, numberDesc *PhoneNumb
}
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)
}
138 changes: 138 additions & 0 deletions shortnumber_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)

////////// Copied from java-libphonenumber
Expand Down Expand Up @@ -72,3 +73,140 @@ func TestIsValidShortNumber(t *testing.T) {
}
assert.False(t, IsValidShortNumberForRegion(invalidNumber, "FR"))
}

func TestConnectsToEmergencyNumber_US(t *testing.T) {
assert.True(t, ConnectsToEmergencyNumber("911", "US"))
assert.True(t, ConnectsToEmergencyNumber("112", "US"))
assert.False(t, ConnectsToEmergencyNumber("999", "US"))
}

func TestConnectsToEmergencyLongNumber_US(t *testing.T) {
assert.True(t, ConnectsToEmergencyNumber("9116666666", "US"))
assert.True(t, ConnectsToEmergencyNumber("1126666666", "US"))
assert.False(t, ConnectsToEmergencyNumber("9996666666", "US"))
}

func TestConnectsToEmergencyNumberWithFormatting_US(t *testing.T) {

assert.True(t, ConnectsToEmergencyNumber("9-1-1", "US"))
assert.True(t, ConnectsToEmergencyNumber("1-1-2", "US"))
assert.False(t, ConnectsToEmergencyNumber("9-9-9", "US"))
}

func TestConnectsToEmergencyNumber_BR(t *testing.T) {
assert.True(t, ConnectsToEmergencyNumber("190", "BR"))
assert.True(t, ConnectsToEmergencyNumber("911", "BR"))
assert.False(t, ConnectsToEmergencyNumber("999", "BR"))
}

func TestConnectsToEmergencyNumberLongNumber_BR(t *testing.T) {
assert.False(t, ConnectsToEmergencyNumber("9111", "BR"))
assert.False(t, ConnectsToEmergencyNumber("1900", "BR"))
assert.False(t, ConnectsToEmergencyNumber("9996", "BR"))
}

func TestConnectsToEmergencyNumber_CL(t *testing.T) {
assert.True(t, ConnectsToEmergencyNumber("131", "CL"))
assert.True(t, ConnectsToEmergencyNumber("133", "CL"))
}

func TestConnectsToEmergencyNumberLongNumber_CL(t *testing.T) {
assert.False(t, ConnectsToEmergencyNumber("1313", "CL"))
assert.False(t, ConnectsToEmergencyNumber("1330", "CL"))
}

func TestConnectsToEmergencyNumber_AO(t *testing.T) {
assert.False(t, ConnectsToEmergencyNumber("911", "AO"))
assert.False(t, ConnectsToEmergencyNumber("222123456", "AO"))
assert.False(t, ConnectsToEmergencyNumber("923123456", "AO"))
}

func TestConnectsToEmergencyNumber_ZW(t *testing.T) {
assert.False(t, ConnectsToEmergencyNumber("911", "ZW"))
assert.False(t, ConnectsToEmergencyNumber("01312345", "ZW"))
assert.False(t, ConnectsToEmergencyNumber("0711234567", "ZW"))
}

func TestIsEmergencyNumber_US(t *testing.T) {
assert.True(t, IsEmergencyNumber("911", "US"))
assert.True(t, IsEmergencyNumber("112", "US"))
assert.False(t, IsEmergencyNumber("999", "US"))
}

func TestIsEmergencyNumberLongNumber_US(t *testing.T) {
assert.False(t, IsEmergencyNumber("9116666666", "US"))
assert.False(t, IsEmergencyNumber("1126666666", "US"))
assert.False(t, IsEmergencyNumber("9996666666", "US"))
}

func TestIsEmergencyNumberWithFormatting_US(t *testing.T) {
assert.True(t, IsEmergencyNumber("9-1-1", "US"))
assert.True(t, IsEmergencyNumber("*911", "US"))
assert.True(t, IsEmergencyNumber("1-1-2", "US"))
assert.True(t, IsEmergencyNumber("*112", "US"))
assert.False(t, IsEmergencyNumber("9-9-9", "US"))
assert.False(t, IsEmergencyNumber("*999", "US"))
}

func TestIsEmergencyNumberWithPlusSign_US(t *testing.T) {
assert.False(t, IsEmergencyNumber("+911", "US"))
assert.False(t, IsEmergencyNumber("\uFF0B911", "US"))
assert.False(t, IsEmergencyNumber(" +911", "US"))
assert.False(t, IsEmergencyNumber("+112", "US"))
assert.False(t, IsEmergencyNumber("+999", "US"))
}

func TestIsEmergencyNumber_BR(t *testing.T) {
assert.True(t, IsEmergencyNumber("911", "BR"))
assert.True(t, IsEmergencyNumber("190", "BR"))
assert.False(t, IsEmergencyNumber("999", "BR"))
}

func TestIsEmergencyNumberLongNumber_BR(t *testing.T) {
assert.False(t, IsEmergencyNumber("9111", "BR"))
assert.False(t, IsEmergencyNumber("1900", "BR"))
assert.False(t, IsEmergencyNumber("9996", "BR"))
}

func TestIsEmergencyNumber_AO(t *testing.T) {
assert.False(t, IsEmergencyNumber("911", "AO"))
assert.False(t, IsEmergencyNumber("222123456", "AO"))
assert.False(t, IsEmergencyNumber("923123456", "AO"))
}

func TestIsEmergencyNumber_ZW(t *testing.T) {
assert.False(t, IsEmergencyNumber("911", "ZW"))
assert.False(t, IsEmergencyNumber("01312345", "ZW"))
assert.False(t, IsEmergencyNumber("0711234567", "ZW"))
}

func TestEmergencyNumberForSharedCountryCallingCode(t *testing.T) {
assert.True(t, IsEmergencyNumber("112", "AU"))
assert.True(t, IsValidShortNumberForRegion(parse(t, "112", "AU"), "AU"))
assert.True(t, IsEmergencyNumber("112", "CX"))
assert.True(t, IsValidShortNumberForRegion(parse(t, "112", "CX"), "CX"))
sharedEmergencyNumber := &PhoneNumber{
CountryCode: proto.Int32(61),
NationalNumber: proto.Uint64(112),
}
assert.True(t, IsValidShortNumber(sharedEmergencyNumber))
}

func TestOverlappingNANPANumber(t *testing.T) {
assert.True(t, IsEmergencyNumber("211", "BB"))
assert.False(t, IsEmergencyNumber("211", "US"))
assert.False(t, IsEmergencyNumber("211", "CA"))
}

func TestCountryCallingCodeIsNotIgnored(t *testing.T) {
assert.False(t, IsPossibleShortNumberForRegion(parse(t, "+4640404", "SE"), "US"))
assert.False(t, IsValidShortNumberForRegion(parse(t, "+4640404", "SE"), "US"))
}

func parse(t *testing.T, number string, regionCode string) *PhoneNumber {
phoneNumber, err := Parse(number, regionCode)
if err != nil {
t.Fatalf("Test input data should always parse correctly: %s (%s)", number, regionCode)
}
return phoneNumber
}

0 comments on commit a8597be

Please sign in to comment.