Skip to content

Commit a8597be

Browse files
authored
Merge pull request #191 from S4more/implement-emergency-shortnumber-metadata
IsEmergency functions for short numbers
2 parents a1a3a19 + 5f7382b commit a8597be

7 files changed

+201
-7
lines changed

builder.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,8 @@ type PhoneNumberMetadataE struct {
575575
}
576576

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

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

675675
// <!ELEMENT voicemail (nationalNumberPattern, possibleLengths, exampleNumber)>
676676
CarrierSpecific *PhoneNumberDescE `xml:"carrierSpecific"`

gen/metadata_bin.go

+1-1
Large diffs are not rendered by default.

gen/prefix_to_carriers_bin.go

+1-1
Large diffs are not rendered by default.

gen/prefix_to_timezone_bin.go

+1-1
Large diffs are not rendered by default.

gen/shortnumber_metadata_bin.go

+1-1
Large diffs are not rendered by default.

shortnumber_info.go

+56
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package phonenumbers
22

33
import (
4+
"golang.org/x/exp/slices"
5+
46
"github.com/nyaruka/phonenumbers/gen"
57
"google.golang.org/protobuf/proto"
68
)
@@ -183,3 +185,57 @@ func matchesPossibleNumberAndNationalNumber(number string, numberDesc *PhoneNumb
183185
}
184186
return MatchNationalNumber(number, *numberDesc, false)
185187
}
188+
189+
// In these countries, if extra digits are added to an emergency number, it no longer connects
190+
// to the emergency service.
191+
var REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = []string{"BR", "CL", "NI"}
192+
193+
func matchesEmergencyNumber(number string, regionCode string, allowPrefixMatch bool) bool {
194+
possibleNumber := extractPossibleNumber(number)
195+
// Returns false if the number starts with a plus sign. We don't believe dialing the country
196+
// code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
197+
// add additional logic here to handle it.
198+
if PLUS_CHARS_PATTERN.MatchString(possibleNumber) {
199+
return false
200+
}
201+
202+
phoneMetadata := getShortNumberMetadataForRegion(regionCode)
203+
if phoneMetadata == nil || phoneMetadata.GetEmergency() == nil {
204+
return false
205+
}
206+
207+
normalizedNumber := NormalizeDigitsOnly(possibleNumber)
208+
209+
allowPrefixMatchForRegion := allowPrefixMatch && !slices.Contains(REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT, regionCode)
210+
return MatchNationalNumber(normalizedNumber, *phoneMetadata.GetEmergency(), allowPrefixMatchForRegion)
211+
}
212+
213+
// Returns true if the given number exactly matches an emergency service number in the given
214+
// region.
215+
//
216+
// This method takes into account cases where the number might contain formatting, but doesn't
217+
// allow additional digits to be appended. Note that isEmergencyNumber(number, region)
218+
// implies connectsToEmergencyNumber(number, region).
219+
//
220+
// number: the phone number to test
221+
// regionCode: the region where the phone number is being dialed
222+
// return: whether the number exactly matches an emergency services number in the given region
223+
func IsEmergencyNumber(number string, regionCode string) bool {
224+
return matchesEmergencyNumber(number, regionCode, false)
225+
}
226+
227+
// Returns true if the given number, exactly as dialed, might be used to connect to an emergency
228+
// service in the given region.
229+
//
230+
// This method accepts a string, rather than a PhoneNumber, because it needs to distinguish
231+
// cases such as "+1 911" and "911", where the former may not connect to an emergency service in
232+
// all cases but the latter would. This method takes into account cases where the number might
233+
// contain formatting, or might have additional digits appended (when it is okay to do that in
234+
// the specified region).
235+
//
236+
// number: the phone number to test
237+
// regionCode: the region where the phone number is being dialed
238+
// return: whether the number might be used to connect to an emergency service in the given region
239+
func ConnectsToEmergencyNumber(number string, regionCode string) bool {
240+
return matchesEmergencyNumber(number, regionCode, true)
241+
}

shortnumber_info_test.go

+138
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/assert"
7+
"google.golang.org/protobuf/proto"
78
)
89

910
////////// Copied from java-libphonenumber
@@ -72,3 +73,140 @@ func TestIsValidShortNumber(t *testing.T) {
7273
}
7374
assert.False(t, IsValidShortNumberForRegion(invalidNumber, "FR"))
7475
}
76+
77+
func TestConnectsToEmergencyNumber_US(t *testing.T) {
78+
assert.True(t, ConnectsToEmergencyNumber("911", "US"))
79+
assert.True(t, ConnectsToEmergencyNumber("112", "US"))
80+
assert.False(t, ConnectsToEmergencyNumber("999", "US"))
81+
}
82+
83+
func TestConnectsToEmergencyLongNumber_US(t *testing.T) {
84+
assert.True(t, ConnectsToEmergencyNumber("9116666666", "US"))
85+
assert.True(t, ConnectsToEmergencyNumber("1126666666", "US"))
86+
assert.False(t, ConnectsToEmergencyNumber("9996666666", "US"))
87+
}
88+
89+
func TestConnectsToEmergencyNumberWithFormatting_US(t *testing.T) {
90+
91+
assert.True(t, ConnectsToEmergencyNumber("9-1-1", "US"))
92+
assert.True(t, ConnectsToEmergencyNumber("1-1-2", "US"))
93+
assert.False(t, ConnectsToEmergencyNumber("9-9-9", "US"))
94+
}
95+
96+
func TestConnectsToEmergencyNumber_BR(t *testing.T) {
97+
assert.True(t, ConnectsToEmergencyNumber("190", "BR"))
98+
assert.True(t, ConnectsToEmergencyNumber("911", "BR"))
99+
assert.False(t, ConnectsToEmergencyNumber("999", "BR"))
100+
}
101+
102+
func TestConnectsToEmergencyNumberLongNumber_BR(t *testing.T) {
103+
assert.False(t, ConnectsToEmergencyNumber("9111", "BR"))
104+
assert.False(t, ConnectsToEmergencyNumber("1900", "BR"))
105+
assert.False(t, ConnectsToEmergencyNumber("9996", "BR"))
106+
}
107+
108+
func TestConnectsToEmergencyNumber_CL(t *testing.T) {
109+
assert.True(t, ConnectsToEmergencyNumber("131", "CL"))
110+
assert.True(t, ConnectsToEmergencyNumber("133", "CL"))
111+
}
112+
113+
func TestConnectsToEmergencyNumberLongNumber_CL(t *testing.T) {
114+
assert.False(t, ConnectsToEmergencyNumber("1313", "CL"))
115+
assert.False(t, ConnectsToEmergencyNumber("1330", "CL"))
116+
}
117+
118+
func TestConnectsToEmergencyNumber_AO(t *testing.T) {
119+
assert.False(t, ConnectsToEmergencyNumber("911", "AO"))
120+
assert.False(t, ConnectsToEmergencyNumber("222123456", "AO"))
121+
assert.False(t, ConnectsToEmergencyNumber("923123456", "AO"))
122+
}
123+
124+
func TestConnectsToEmergencyNumber_ZW(t *testing.T) {
125+
assert.False(t, ConnectsToEmergencyNumber("911", "ZW"))
126+
assert.False(t, ConnectsToEmergencyNumber("01312345", "ZW"))
127+
assert.False(t, ConnectsToEmergencyNumber("0711234567", "ZW"))
128+
}
129+
130+
func TestIsEmergencyNumber_US(t *testing.T) {
131+
assert.True(t, IsEmergencyNumber("911", "US"))
132+
assert.True(t, IsEmergencyNumber("112", "US"))
133+
assert.False(t, IsEmergencyNumber("999", "US"))
134+
}
135+
136+
func TestIsEmergencyNumberLongNumber_US(t *testing.T) {
137+
assert.False(t, IsEmergencyNumber("9116666666", "US"))
138+
assert.False(t, IsEmergencyNumber("1126666666", "US"))
139+
assert.False(t, IsEmergencyNumber("9996666666", "US"))
140+
}
141+
142+
func TestIsEmergencyNumberWithFormatting_US(t *testing.T) {
143+
assert.True(t, IsEmergencyNumber("9-1-1", "US"))
144+
assert.True(t, IsEmergencyNumber("*911", "US"))
145+
assert.True(t, IsEmergencyNumber("1-1-2", "US"))
146+
assert.True(t, IsEmergencyNumber("*112", "US"))
147+
assert.False(t, IsEmergencyNumber("9-9-9", "US"))
148+
assert.False(t, IsEmergencyNumber("*999", "US"))
149+
}
150+
151+
func TestIsEmergencyNumberWithPlusSign_US(t *testing.T) {
152+
assert.False(t, IsEmergencyNumber("+911", "US"))
153+
assert.False(t, IsEmergencyNumber("\uFF0B911", "US"))
154+
assert.False(t, IsEmergencyNumber(" +911", "US"))
155+
assert.False(t, IsEmergencyNumber("+112", "US"))
156+
assert.False(t, IsEmergencyNumber("+999", "US"))
157+
}
158+
159+
func TestIsEmergencyNumber_BR(t *testing.T) {
160+
assert.True(t, IsEmergencyNumber("911", "BR"))
161+
assert.True(t, IsEmergencyNumber("190", "BR"))
162+
assert.False(t, IsEmergencyNumber("999", "BR"))
163+
}
164+
165+
func TestIsEmergencyNumberLongNumber_BR(t *testing.T) {
166+
assert.False(t, IsEmergencyNumber("9111", "BR"))
167+
assert.False(t, IsEmergencyNumber("1900", "BR"))
168+
assert.False(t, IsEmergencyNumber("9996", "BR"))
169+
}
170+
171+
func TestIsEmergencyNumber_AO(t *testing.T) {
172+
assert.False(t, IsEmergencyNumber("911", "AO"))
173+
assert.False(t, IsEmergencyNumber("222123456", "AO"))
174+
assert.False(t, IsEmergencyNumber("923123456", "AO"))
175+
}
176+
177+
func TestIsEmergencyNumber_ZW(t *testing.T) {
178+
assert.False(t, IsEmergencyNumber("911", "ZW"))
179+
assert.False(t, IsEmergencyNumber("01312345", "ZW"))
180+
assert.False(t, IsEmergencyNumber("0711234567", "ZW"))
181+
}
182+
183+
func TestEmergencyNumberForSharedCountryCallingCode(t *testing.T) {
184+
assert.True(t, IsEmergencyNumber("112", "AU"))
185+
assert.True(t, IsValidShortNumberForRegion(parse(t, "112", "AU"), "AU"))
186+
assert.True(t, IsEmergencyNumber("112", "CX"))
187+
assert.True(t, IsValidShortNumberForRegion(parse(t, "112", "CX"), "CX"))
188+
sharedEmergencyNumber := &PhoneNumber{
189+
CountryCode: proto.Int32(61),
190+
NationalNumber: proto.Uint64(112),
191+
}
192+
assert.True(t, IsValidShortNumber(sharedEmergencyNumber))
193+
}
194+
195+
func TestOverlappingNANPANumber(t *testing.T) {
196+
assert.True(t, IsEmergencyNumber("211", "BB"))
197+
assert.False(t, IsEmergencyNumber("211", "US"))
198+
assert.False(t, IsEmergencyNumber("211", "CA"))
199+
}
200+
201+
func TestCountryCallingCodeIsNotIgnored(t *testing.T) {
202+
assert.False(t, IsPossibleShortNumberForRegion(parse(t, "+4640404", "SE"), "US"))
203+
assert.False(t, IsValidShortNumberForRegion(parse(t, "+4640404", "SE"), "US"))
204+
}
205+
206+
func parse(t *testing.T, number string, regionCode string) *PhoneNumber {
207+
phoneNumber, err := Parse(number, regionCode)
208+
if err != nil {
209+
t.Fatalf("Test input data should always parse correctly: %s (%s)", number, regionCode)
210+
}
211+
return phoneNumber
212+
}

0 commit comments

Comments
 (0)