-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6caa19e
commit e8695b3
Showing
5 changed files
with
118 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,66 @@ | ||
package urns | ||
|
||
import ( | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/nyaruka/gocommon/i18n" | ||
"github.com/nyaruka/phonenumbers" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// FromLocalPhone returns a validated tel URN | ||
func FromLocalPhone(number string, country string) (URN, error) { | ||
path, err := ParsePhone(number, country) | ||
var nonTelCharsRegex = regexp.MustCompile(`[^0-9A-Za-z]`) | ||
|
||
// ParsePhone returns a validated phone URN. If it can parse a possible number then that is used.. otherwise any value | ||
// that validates as a phone URN is used. | ||
func ParsePhone(raw string, country i18n.Country) (URN, error) { | ||
// strip all non-tel characters.. only preserving an optional leading + | ||
raw = strings.TrimSpace(raw) | ||
hasPlus := strings.HasPrefix(raw, "+") | ||
raw = nonTelCharsRegex.ReplaceAllString(raw, "") | ||
if hasPlus { | ||
raw = "+" + raw | ||
} | ||
|
||
number, err := parsePhoneOrShortcode(raw, country) | ||
if err != nil { | ||
return NilURN, err | ||
if err == phonenumbers.ErrInvalidCountryCode { | ||
return NilURN, errors.New("invalid country code") | ||
} | ||
|
||
return NewFromParts(Phone, raw, "", "") | ||
} | ||
|
||
return NewURNFromParts(Phone, path, "", "") | ||
return NewFromParts(Phone, number, "", "") | ||
} | ||
|
||
// ToLocalPhone converts a phone URN to a local number in the given country | ||
func ToLocalPhone(u URN, country string) string { | ||
_, path, _, _ := u.ToParts() | ||
// tries to extract a valid phone number or shortcode from the given string | ||
func parsePhoneOrShortcode(raw string, country i18n.Country) (string, error) { | ||
parsed, err := phonenumbers.Parse(raw, string(country)) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
parsed, err := phonenumbers.Parse(path, country) | ||
if err == nil { | ||
return strconv.FormatUint(parsed.GetNationalNumber(), 10) | ||
if phonenumbers.IsPossibleNumberWithReason(parsed) == phonenumbers.IS_POSSIBLE { | ||
return phonenumbers.Format(parsed, phonenumbers.E164), nil | ||
} | ||
return path | ||
} | ||
|
||
// ParsePhone tries to parse the given string as a phone number and if successful returns it as E164 | ||
func ParsePhone(s, country string) (string, error) { | ||
parsed, err := phonenumbers.Parse(s, country) | ||
if err != nil { | ||
return "", errors.Wrap(err, "unable to parse number") | ||
if phonenumbers.IsPossibleShortNumberForRegion(parsed, string(country)) { | ||
return phonenumbers.Format(parsed, phonenumbers.NATIONAL), nil | ||
} | ||
|
||
if phonenumbers.IsPossibleNumberWithReason(parsed) != phonenumbers.IS_POSSIBLE { | ||
// if it's not a possible number, try adding a + and parsing again | ||
if !strings.HasPrefix(s, "+") { | ||
return ParsePhone("+"+s, country) | ||
} | ||
return "", errors.New("unable to parse phone number or shortcode") | ||
} | ||
|
||
return "", errors.New("not a possible number") | ||
// ToLocalPhone converts a phone URN to a local phone number.. without any leading zeros | ||
func ToLocalPhone(u URN, country i18n.Country) string { | ||
_, path, _, _ := u.ToParts() | ||
|
||
parsed, err := phonenumbers.Parse(path, string(country)) | ||
if err != nil { | ||
return path | ||
} | ||
|
||
return phonenumbers.Format(parsed, phonenumbers.E164), nil | ||
return strconv.FormatUint(parsed.GetNationalNumber(), 10) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,90 @@ | ||
package urns_test | ||
package urns | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/nyaruka/gocommon/urns" | ||
"github.com/nyaruka/gocommon/i18n" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestFromLocalPhone(t *testing.T) { | ||
func TestParsePhone(t *testing.T) { | ||
testCases := []struct { | ||
number string | ||
country string | ||
expected urns.URN | ||
hasError bool | ||
input string | ||
country i18n.Country | ||
expected URN | ||
}{ | ||
{"tel:0788383383", "RW", "tel:+250788383383", false}, | ||
{"tel: +250788383383 ", "KE", "tel:+250788383383", false}, // already has country code | ||
{"tel:(917)992-5253", "US", "tel:+19179925253", false}, | ||
{"tel:800-CABBAGE", "US", "tel:+18002222243", false}, | ||
{"tel:+62877747666", "ID", "tel:+62877747666", false}, | ||
{"tel:0877747666", "ID", "tel:+62877747666", false}, | ||
{"tel:07531669965", "GB", "tel:+447531669965", false}, | ||
{"tel:263780821000", "ZW", "tel:+263780821000", false}, | ||
{" 0788383383 ", "RW", "tel:+250788383383"}, | ||
{"+250788383383 ", "RW", "tel:+250788383383"}, // already has country code and leading + | ||
{"250788383383 ", "RW", "tel:+250788383383"}, // already has country code and no leading + | ||
{"+250788383383 ", "KE", "tel:+250788383383"}, // already has a different country code | ||
{"(917)992-5253", "US", "tel:+19179925253"}, | ||
{"800-CABBAGE", "US", "tel:+18002222243"}, | ||
{"+62877747666", "ID", "tel:+62877747666"}, | ||
{"0877747666", "ID", "tel:+62877747666"}, | ||
{"07531669965", "GB", "tel:+447531669965"}, | ||
{"263780821000", "ZW", "tel:+263780821000"}, | ||
|
||
{"1", "RW", "tel:1"}, | ||
{"123456", "RW", "tel:123456"}, | ||
{"mtn", "RW", "tel:mtn"}, | ||
{"!mtn!", "RW", "tel:mtn"}, // non tel chars stripped | ||
|
||
{"0788383383", "ZZ", urns.NilURN, true}, // invalid country code | ||
{"1", "RW", urns.NilURN, true}, | ||
{"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "RW", urns.NilURN, true}, | ||
{"0788383383", "ZZ", NilURN}, // invalid country code | ||
{"1234567890123456789012345678901234567890123456789012345678901234567890123456789", "RW", NilURN}, // too long | ||
} | ||
|
||
for i, tc := range testCases { | ||
urn, err := urns.FromLocalPhone(tc.number, tc.country) | ||
urn, err := ParsePhone(tc.input, tc.country) | ||
|
||
if tc.hasError { | ||
assert.Error(t, err, "%d: expected error for %s, %s", i, tc.number, tc.country) | ||
if tc.expected == NilURN { | ||
assert.Error(t, err, "%d: expected error for %s, %s", i, tc.input, tc.country) | ||
} else { | ||
assert.NoError(t, err, "%d: unexpected error for %s, %s", i, tc.number, tc.country) | ||
assert.Equal(t, tc.expected, urn, "%d: created URN mismatch for %s, %s", i, tc.number, tc.country) | ||
assert.NoError(t, err, "%d: unexpected error for %s, %s", i, tc.input, tc.country) | ||
assert.Equal(t, tc.expected, urn, "%d: created URN mismatch for %s, %s", i, tc.input, tc.country) | ||
} | ||
} | ||
} | ||
|
||
func TestParsePhone(t *testing.T) { | ||
func TestParsePhoneOrShortcode(t *testing.T) { | ||
tcs := []struct { | ||
input string | ||
country string | ||
parsed string | ||
input string | ||
country i18n.Country | ||
expected string | ||
}{ | ||
{"+250788123123", "", "+250788123123"}, // international number fine without country | ||
{"+250 788 123-123", "", "+250788123123"}, // fine if not E164 formatted | ||
{"0788123123", "RW", "+250788123123"}, | ||
{"206 555 1212", "US", "+12065551212"}, | ||
{"12065551212", "US", "+12065551212"}, // country code but no + | ||
{"5912705", "US", ""}, // is only possible as a local number so ignored | ||
{"10000", "US", ""}, | ||
{"+250 788 123-123", "", "+250788123123"}, // still fine if not E164 formatted | ||
|
||
{"0788123123", "RW", "+250788123123"}, // country code added | ||
{" (206)555-1212 ", "US", "+12065551212"}, // punctiation removed | ||
{"800-CABBAGE", "US", "+18002222243"}, // letters converted to numbers | ||
{"12065551212", "US", "+12065551212"}, // country code but no + | ||
{"10000", "US", "10000"}, // valid short code for US | ||
|
||
{"5912705", "US", ""}, // is only possible as a local number so ignored | ||
} | ||
|
||
for _, tc := range tcs { | ||
if tc.parsed != "" { | ||
parsed, err := urns.ParsePhone(tc.input, tc.country) | ||
parsed, err := parsePhoneOrShortcode(tc.input, tc.country) | ||
|
||
if tc.expected != "" { | ||
assert.NoError(t, err, "unexpected error for '%s'", tc.input) | ||
assert.Equal(t, parsed, tc.parsed, "result mismatch for '%s'", tc.input) | ||
assert.Equal(t, tc.expected, parsed, "result mismatch for '%s'", tc.input) | ||
} else { | ||
_, err := urns.ParsePhone(tc.input, tc.country) | ||
assert.Error(t, err, "expected error for '%s'", tc.input) | ||
} | ||
} | ||
} | ||
func TestToLocalPhone(t *testing.T) { | ||
tcs := []struct { | ||
urn URN | ||
country i18n.Country | ||
expected string | ||
}{ | ||
{"tel:+250788123123", "", "788123123"}, | ||
{"tel:123123", "", "123123"}, | ||
} | ||
|
||
for _, tc := range tcs { | ||
assert.Equal(t, tc.expected, ToLocalPhone(tc.urn, tc.country), "local mismatch for '%s'", tc.urn) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters