-
Notifications
You must be signed in to change notification settings - Fork 10
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
Showing
4 changed files
with
365 additions
and
0 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 |
---|---|---|
@@ -0,0 +1,218 @@ | ||
package util | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"regexp" | ||
"slices" | ||
"strconv" | ||
"strings" | ||
|
||
phoneiso3166 "github.com/onlinecity/go-phone-iso3166" | ||
pn "github.com/ttacon/libphonenumber" | ||
) | ||
|
||
const ( | ||
MinPhoneSymbolCount = 5 | ||
CountryPhoneCodeDE = 49 | ||
CountryPhoneCodeAG = 54 | ||
CountryPhoneCodeMX = 52 | ||
CountryPhoneCodeUS = "1443" | ||
CountryPhoneCodePS = 970 | ||
CountryPhoneCodeUZ = 998 | ||
PalestineRegion = "PS" | ||
BangladeshRegion = "BD" | ||
) | ||
|
||
var ( | ||
ErrPhoneTooShort = errors.New("phone is too short - must be at least 5 symbols") | ||
ErrCannotDetermineCountry = errors.New("cannot determine phone country code") | ||
ErrCannotParsePhone = errors.New("cannot parse phone number") | ||
|
||
TrimmedPhoneRegexp = regexp.MustCompile(`\D+`) | ||
UndefinedUSCodes = []string{"1445", "1945", "1840", "1448", "1279", "1839"} | ||
) | ||
|
||
// FormatNumberForWA forms in the format according to the rules https://faq.whatsapp.com/1294841057948784 | ||
func FormatNumberForWA(number string) (string, error) { | ||
parsedPhone, err := ParsePhone(number) | ||
|
||
if err != nil { | ||
return "", err | ||
} | ||
|
||
var formattedPhoneNumber string | ||
switch parsedPhone.GetCountryCode() { | ||
case CountryPhoneCodeAG: | ||
formattedPhoneNumber = Add9AGIFNeed(parsedPhone) | ||
default: | ||
formattedPhoneNumber = pn.Format(parsedPhone, pn.E164) | ||
} | ||
|
||
return formattedPhoneNumber, nil | ||
} | ||
|
||
// ParsePhone this function parses the number as a string | ||
// For Mexican numbers `1` is always added to the national number because it is always removed during parsing. | ||
// Attention when formatted in libphonenumber.INTERNATIONAL 1 will not be after the country code, even though | ||
// it is in the national number. | ||
// But for Argentine numbers there is no automatic addition 9 to the country code. | ||
func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { | ||
trimmedPhone := TrimmedPhoneRegexp.ReplaceAllString(phoneNumber, "") | ||
if len(trimmedPhone) < MinPhoneSymbolCount { | ||
return nil, ErrPhoneTooShort | ||
} | ||
|
||
countryCode := getCountryCode(trimmedPhone) | ||
if countryCode == "" { | ||
return nil, ErrCannotDetermineCountry | ||
} | ||
|
||
parsedPhone, err := pn.Parse(trimmedPhone, countryCode) | ||
if err != nil { | ||
return nil, ErrCannotParsePhone | ||
} | ||
|
||
if CountryPhoneCodeDE == parsedPhone.GetCountryCode() { | ||
number, err := getGermanNationalNumber(trimmedPhone, parsedPhone) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
parsedPhone.NationalNumber = &number | ||
} | ||
|
||
if CountryPhoneCodeUZ == parsedPhone.GetCountryCode() { | ||
number, err := getUzbekistanNationalNumber(trimmedPhone, parsedPhone) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
parsedPhone.NationalNumber = &number | ||
} | ||
|
||
if IsMexicoNumber(parsedPhone) { | ||
number, err := getMexicanNationalNumber(parsedPhone) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
parsedPhone.NationalNumber = &number | ||
} | ||
|
||
return parsedPhone, err | ||
} | ||
|
||
func IsRussianNumberWith8Prefix(phone string) bool { | ||
return strings.HasPrefix(phone, "8") && len(phone) == 11 && phoneiso3166.E164.LookupString("7"+phone[1:]) == "RU" | ||
} | ||
|
||
func IsMexicoNumber(parsed *pn.PhoneNumber) bool { | ||
return parsed.GetCountryCode() == CountryPhoneCodeMX | ||
} | ||
|
||
func IsUSNumber(phone string) bool { | ||
return slices.Contains(UndefinedUSCodes, phone[:4]) && | ||
phoneiso3166.E164.LookupString(CountryPhoneCodeUS+phone[4:]) == "US" | ||
} | ||
|
||
func IsPLNumber(phone string) bool { | ||
num, err := pn.Parse(phone, "PS") | ||
return err == nil && num.GetCountryCode() == CountryPhoneCodePS && fmt.Sprintf("%d", CountryPhoneCodePS) == phone[0:3] | ||
} | ||
|
||
func Remove9AGIfNeed(parsedPhone *pn.PhoneNumber) string { | ||
formattedPhone := pn.Format(parsedPhone, pn.E164) | ||
numberWOCountry := fmt.Sprintf("%d", parsedPhone.GetNationalNumber()) | ||
|
||
if len(numberWOCountry) == 11 && string(numberWOCountry[0]) == "9" { | ||
formattedPhone = fmt.Sprintf("+%d%s", CountryPhoneCodeAG, numberWOCountry[1:]) | ||
} | ||
|
||
return formattedPhone | ||
} | ||
|
||
func Add9AGIFNeed(parsedPhone *pn.PhoneNumber) string { | ||
formattedPhone := pn.Format(parsedPhone, pn.E164) | ||
numberWOCountry := fmt.Sprintf("%d", parsedPhone.GetNationalNumber()) | ||
|
||
if len(numberWOCountry) == 10 { // nolint:mnd | ||
formattedPhone = fmt.Sprintf("+%d%s", CountryPhoneCodeAG, "9"+numberWOCountry) | ||
} | ||
|
||
return formattedPhone | ||
} | ||
|
||
// getGermanNationalNumber some German numbers may not be parsed correctly. | ||
// For example, for 491736276098 libphonenumber.PhoneNumber.NationalNumber | ||
// will contain the country code(49). This function fix it and return correct libphonenumber.PhoneNumber. | ||
func getGermanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, error) { | ||
result := parsedPhone.GetNationalNumber() | ||
|
||
if len(fmt.Sprintf("%d", parsedPhone.GetNationalNumber())) == len(phone) { | ||
deduplicateCountryNumber := fmt.Sprintf("%d", parsedPhone.GetNationalNumber())[2:] | ||
|
||
number, err := strconv.Atoi(deduplicateCountryNumber) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
result = uint64(number) //nolint:gosec | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
// For UZ numbers where 8 is deleted after the country code. | ||
func getUzbekistanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, error) { | ||
result := parsedPhone.GetNationalNumber() | ||
numberWithEight := fmt.Sprintf("8%d", parsedPhone.GetNationalNumber()) | ||
|
||
if len(fmt.Sprintf("%d%s", parsedPhone.GetCountryCode(), numberWithEight)) == len(phone) { | ||
number, err := strconv.Atoi(numberWithEight) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
result = uint64(number) //nolint:gosec | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
func getMexicanNationalNumber(parsedPhone *pn.PhoneNumber) (uint64, error) { | ||
phoneWithDigit := fmt.Sprintf("1%d", parsedPhone.GetNationalNumber()) | ||
|
||
num, err := strconv.Atoi(phoneWithDigit) | ||
|
||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
return uint64(num), nil //nolint:gosec | ||
} | ||
|
||
func getCountryCode(phone string) string { | ||
countryCode := phoneiso3166.E164.LookupString(phone) | ||
|
||
if countryCode == "" { | ||
if IsRussianNumberWith8Prefix(phone) { | ||
countryCode = phoneiso3166.E164.LookupString("7" + phone[1:]) | ||
} | ||
|
||
if IsUSNumber(phone) { | ||
countryCode = phoneiso3166.E164.LookupString(CountryPhoneCodeUS + phone[4:]) | ||
} | ||
|
||
if IsPLNumber(phone) { | ||
countryCode = PalestineRegion | ||
} | ||
} | ||
|
||
// For russian numbers as 8800xxxxxxx | ||
if strings.EqualFold(BangladeshRegion, countryCode) && IsRussianNumberWith8Prefix(phone) { | ||
countryCode = phoneiso3166.E164.LookupString("7" + phone[1:]) | ||
} | ||
|
||
return countryCode | ||
} |
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 |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package util | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestParsePhone(t *testing.T) { | ||
t.Run("russian numers", func(t *testing.T) { | ||
n := "+88002541213" | ||
pn, err := ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(8002541213), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(7), pn.GetCountryCode()) | ||
|
||
n = "+78002541213" | ||
pn, err = ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.NotNil(t, pn) | ||
assert.Equal(t, uint64(8002541213), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(7), pn.GetCountryCode()) | ||
|
||
n = "89521548787" | ||
pn, err = ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(9521548787), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(7), pn.GetCountryCode()) | ||
|
||
n = "+7-900-123-45-67" | ||
pn, err = ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(9001234567), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(7), pn.GetCountryCode()) | ||
|
||
}) | ||
|
||
t.Run("german numbers", func(t *testing.T) { | ||
n := "491736276098" | ||
pn, err := ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(1736276098), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeDE), pn.GetCountryCode()) | ||
|
||
n = "4915229457499" | ||
pn, err = ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(15229457499), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeDE), pn.GetCountryCode()) | ||
}) | ||
|
||
t.Run("mexican number", func(t *testing.T) { | ||
n := "5219982418333" | ||
pn, err := ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(19982418333), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) | ||
|
||
n = "+521 (998) 241 83 33" | ||
pn, err = ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(19982418333), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) | ||
|
||
n = "529982418333" | ||
pn, err = ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(19982418333), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) | ||
}) | ||
|
||
t.Run("palestine number", func(t *testing.T) { | ||
n := "970567800663" | ||
pn, err := ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(567800663), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodePS), pn.GetCountryCode()) | ||
}) | ||
|
||
t.Run("argentine number", func(t *testing.T) { | ||
n := "5491131157821" | ||
pn, err := ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(91131157821), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeAG), pn.GetCountryCode()) | ||
}) | ||
|
||
t.Run("uzbekistan number", func(t *testing.T) { | ||
n := "998882207724" | ||
pn, err := ParsePhone(n) | ||
require.NoError(t, err) | ||
assert.Equal(t, uint64(882207724), pn.GetNationalNumber()) | ||
assert.Equal(t, int32(CountryPhoneCodeUZ), pn.GetCountryCode()) | ||
}) | ||
} | ||
|
||
func TestFormatNumberForWA(t *testing.T) { | ||
numbers := map[string]string{ | ||
"79040000000": "+79040000000", | ||
"491736276098": "+491736276098", | ||
"89185553535": "+79185553535", | ||
"4915229457499": "+4915229457499", | ||
"5491131157821": "+5491131157821", | ||
"541131157821": "+5491131157821", | ||
"5219982418333": "+5219982418333", | ||
"529982418333": "+5219982418333", | ||
"14452385043": "+14452385043", | ||
"19452090748": "+19452090748", | ||
"19453003681": "+19453003681", | ||
"19452141217": "+19452141217", | ||
"18407778097": "+18407778097", | ||
"14482074337": "+14482074337", | ||
"18406665259": "+18406665259", | ||
"19455009160": "+19455009160", | ||
"19452381431": "+19452381431", | ||
"12793006305": "+12793006305", | ||
} | ||
|
||
for orig, expected := range numbers { | ||
actual, err := FormatNumberForWA(orig) | ||
require.NoError(t, err) | ||
require.Equal(t, expected, actual) | ||
} | ||
} |
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
Oops, something went wrong.