Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IsEmergency functions for short numbers #191

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading