From 53b5ec7ea35fc4082c51bcb4019b6364dc0412a4 Mon Sep 17 00:00:00 2001 From: Ali Pourhabibi Date: Thu, 13 Jun 2024 11:36:17 +0330 Subject: [PATCH 1/2] feat: add phone-number package --- phone_numbers/operators.go | 67 ++++++++ phone_numbers/phone.go | 97 +++++++++++ phone_numbers/phone_numbers_list.go | 240 ++++++++++++++++++++++++++++ phone_numbers/phone_test.go | 134 ++++++++++++++++ 4 files changed, 538 insertions(+) create mode 100644 phone_numbers/operators.go create mode 100644 phone_numbers/phone.go create mode 100644 phone_numbers/phone_numbers_list.go create mode 100644 phone_numbers/phone_test.go diff --git a/phone_numbers/operators.go b/phone_numbers/operators.go new file mode 100644 index 0000000..fcf5ddd --- /dev/null +++ b/phone_numbers/operators.go @@ -0,0 +1,67 @@ +package phonenumbers + +import "errors" + +type Operator string +type SimType string + +var ErrInvalidFormat = errors.New("invalid phone number format") + +// List of valid prefixes +var Prefixes = []string{"+98", "98", "0098", "0"} + +const ( + ShatelMobile Operator = "ShatelMobile" + MCI Operator = "MCI" + Irancell Operator = "Irancell" + Taliya Operator = "Taliya" + RightTel Operator = "RightTel" + + Permanent SimType = "Permanent" + Credit SimType = "Credit" +) + +type OperatorDetails struct { + base string + province []string + model string + operator Operator + simTypes []SimType +} + +func (o Operator) Details() map[string]OperatorDetails { + switch o { + case MCI: + return MCIMap + case Taliya: + return TALIYA + case RightTel: + return RIGHTTEL + case Irancell: + return IRANCELL + case ShatelMobile: + return SHATELMOBILE + default: + return nil + } +} + +func (od *OperatorDetails) GetProvinceList() []string { + return od.province +} + +func (od *OperatorDetails) GetBase() string { + return od.base +} + +func (od *OperatorDetails) GetModel() string { + return od.model +} + +func (od *OperatorDetails) GetOperator() Operator { + return od.operator +} + +func (od *OperatorDetails) GetSimTypeList() []SimType { + return od.simTypes +} diff --git a/phone_numbers/phone.go b/phone_numbers/phone.go new file mode 100644 index 0000000..e53369a --- /dev/null +++ b/phone_numbers/phone.go @@ -0,0 +1,97 @@ +package phonenumbers + +import ( + "errors" + "strings" +) + +// GetPhoneDetails returns the OperatorDetails for the given phoneNumber +func GetPhoneDetails(phoneNumber string) (*OperatorDetails, error) { + if ok := IsPhoneValid(phoneNumber); !ok { + return nil, ErrInvalidFormat + } + prefix, err := GetOperatorPrefix(phoneNumber) + if err != nil { + return nil, err + } + return GetPrefixDetails(prefix) +} + +// GetPrefixDetails returns the OperatorDetails of the given prefix +func GetPrefixDetails(prefix string) (*OperatorDetails, error) { + for k, v := range MCIMap { + if k == prefix { + return &v, nil + } + } + for k, v := range TALIYA { + if k == prefix { + return &v, nil + } + } + for k, v := range RIGHTTEL { + if k == prefix { + return &v, nil + } + } + for k, v := range IRANCELL { + if k == prefix { + return &v, nil + } + } + for k, v := range SHATELMOBILE { + if k == prefix { + return &v, nil + } + } + return nil, errors.New("Invalid prefix") +} + +// IsPhoneValid returns if the phoneNumber is valid +func IsPhoneValid(phoneNumber string) bool { + prefix := GetPhonePrefix(phoneNumber) + + phoneNumberWithoutPrefix := phoneNumber[len(prefix):] + if len(phoneNumberWithoutPrefix) == 10 && phoneNumberWithoutPrefix[0] == '9' { + return true + } + + return false +} + +// GetPhonePrefix returns the prefix for Iranian phone numbers +// Example +98 98 0098 0 +func GetPhonePrefix(phoneNumber string) string { + for _, prefix := range Prefixes { + if strings.HasPrefix(phoneNumber, prefix) { + return prefix + } + } + return "" +} + +// phoneNumberNormalizer replaces the current phone number prefix with the desired prefix +func PhoneNumberNormalizer(phoneNumber, newPrefix string) (string, error) { + if ok := IsPhoneValid(phoneNumber); !ok { + return "", ErrInvalidFormat + } + + prefix := GetPhonePrefix(phoneNumber) + + return newPrefix + phoneNumber[len(prefix):], nil +} + +// GetOperatorPrefix returns the operator prefix of the phone number +func GetOperatorPrefix(phoneNumber string) (string, error) { + if ok := IsPhoneValid(phoneNumber); !ok { + return "", ErrInvalidFormat + } + + for _, prefix := range Prefixes { + if strings.HasPrefix(phoneNumber, prefix) { + return phoneNumber[len(prefix) : len(prefix)+3], nil + } + } + + return "", ErrInvalidFormat +} diff --git a/phone_numbers/phone_numbers_list.go b/phone_numbers/phone_numbers_list.go new file mode 100644 index 0000000..defa14f --- /dev/null +++ b/phone_numbers/phone_numbers_list.go @@ -0,0 +1,240 @@ +package phonenumbers + +var MCIMap = map[string]OperatorDetails{ + "910": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "914": { + base: "آذربایجان غربی", + province: []string{"آذربایجان شرقی", "اردبیل", "اصفهان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "911": { + base: "مازندران", + province: []string{"گلستان", "گیلان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "912": { + base: "تهران", + province: []string{"البرز", "زنجان", "سمنان", "قزوین", "قم", "برخی از شهرستان های استان مرکزی"}, + simTypes: []SimType{Permanent}, + operator: MCI, + }, + "913": { + base: "اصفهان", + province: []string{"یزد", "چهارمحال و بختیاری", "کرمان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "915": { + base: "خراسان رضوی", + province: []string{"خراسان شمالی", "خراسان جنوبی", "سیستان و بلوچستان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "916": { + base: "خوزستان", + province: []string{"لرستان", "فارس", "اصفهان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "917": { + base: "فارس", + province: []string{"بوشهر", "کهگیلویه و بویر احمد", "هرمزگان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "918": { + base: "کرمانشاه", + province: []string{"کردستان", "ایلام", "همدان"}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "919": { + base: "تهران", + province: []string{"البرز", "سمنان", "قم", "قزوین", "زنجان"}, + simTypes: []SimType{Credit}, + operator: MCI, + }, + "990": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: MCI, + }, + "991": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "992": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: MCI, + }, + "993": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: MCI, + }, + "994": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: MCI, + }, + "995": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, + "996": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + }, +} + +var TALIYA = map[string]OperatorDetails{ + "932": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: Taliya, + }, +} + +var RIGHTTEL = map[string]OperatorDetails{ + "920": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Permanent}, + operator: RightTel, + }, + "921": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: RightTel, + }, + "922": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: RightTel, + }, + "923": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: RightTel, + }, +} + +var IRANCELL = map[string]OperatorDetails{ + "930": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "933": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "935": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "936": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "937": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "938": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "939": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "901": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "902": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "903": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "905": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "900": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit, Permanent}, + operator: Irancell, + }, + "904": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: Irancell, + model: "سیم‌کارت کودک", + }, + "941": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: Irancell, + model: "TD-LTE", + }, +} + +var SHATELMOBILE = map[string]OperatorDetails{ + "998": { + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: ShatelMobile, + }, +} diff --git a/phone_numbers/phone_test.go b/phone_numbers/phone_test.go new file mode 100644 index 0000000..a9592c8 --- /dev/null +++ b/phone_numbers/phone_test.go @@ -0,0 +1,134 @@ +package phonenumbers + +import ( + "reflect" + "testing" +) + +func TestIsPhoneValid(t *testing.T) { + tests := []struct { + phoneNumber string + isValid bool + }{ + {"9122221811", true}, + {"09122221811", true}, + {"+989122221811", true}, + {"12903908", false}, + {"901239812390812908", false}, + } + + for _, tt := range tests { + ok := IsPhoneValid(tt.phoneNumber) + if ok != tt.isValid { + t.Errorf("IsPhoneValid(%s) got %t, expected %t", tt.phoneNumber, ok, tt.isValid) + } + } +} + +func TestPhoneNumberNormalizer(t *testing.T) { + tests := []struct { + phoneNumber string + newPrefix string + want string + wantErr bool + }{ + {"+989373708555", "0", "09373708555", false}, + {"989373708555", "0", "09373708555", false}, + {"00989022002580", "0", "09022002580", false}, + {"09122002580", "0", "09122002580", false}, + {"9322002580", "0", "09322002580", false}, + {"09373708555", "+98", "+989373708555", false}, + {"09022002580", "+98", "+989022002580", false}, + {"09122002580", "+98", "+989122002580", false}, + {"9322002580", "+98", "+989322002580", false}, + {"00989022002580", "+98", "+989022002580", false}, + {"09132222", "+98", "", true}, + {"9191282819921", "0", "", true}, + } + + for _, tt := range tests { + got, err := PhoneNumberNormalizer(tt.phoneNumber, tt.newPrefix) + if (err != nil) != tt.wantErr { + t.Errorf("PhoneNumberNormalizer(%s, %s) error = %v, wantErr %t", tt.phoneNumber, tt.newPrefix, err, tt.wantErr) + continue + } + if got != tt.want { + t.Errorf("PhoneNumberNormalizer(%s, %s) = %s, want %s", tt.phoneNumber, tt.newPrefix, got, tt.want) + } + } +} + +func TestGetOperatorPrefix(t *testing.T) { + tests := []struct { + phoneNumber string + want string + wantErr bool + }{ + {"+989373708555", "937", false}, + {"00989013708555", "901", false}, + {"00988013708555", "", true}, + } + + for _, tt := range tests { + got, err := GetOperatorPrefix(tt.phoneNumber) + if (err != nil) != tt.wantErr { + t.Errorf("GetOperatorPrefix(%s) error = %v, wantErr %t", tt.phoneNumber, err, tt.wantErr) + continue + } + if got != tt.want { + t.Errorf("GetOperatorPrefix(%s) = %s, want %s", tt.phoneNumber, got, tt.want) + } + } +} + +func TestGetPhonePrefixOperator(t *testing.T) { + expectedDetails904 := &OperatorDetails{ + base: "کشوری", + province: []string{}, + simTypes: []SimType{Credit}, + operator: Irancell, + model: "سیم‌کارت کودک", + } + + if details, err := GetPrefixDetails("904"); err != nil { + t.Errorf("expected no error, got %v", err) + } else if !reflect.DeepEqual(details, expectedDetails904) { + t.Errorf("expected %v, got %v", expectedDetails904, details) + } + + expectedDetails910 := &OperatorDetails{ + base: "کشوری", + province: []string{}, + simTypes: []SimType{Permanent, Credit}, + operator: MCI, + } + + if details, err := GetPrefixDetails("910"); err != nil { + t.Errorf("expected no error, got %v", err) + } else if !reflect.DeepEqual(details, expectedDetails910) { + t.Errorf("expected %v, got %v", expectedDetails910, details) + } + + if _, err := GetPrefixDetails("9100"); err == nil { + t.Error("expected error, got none") + } +} + +func TestGetPhoneDetails(t *testing.T) { + expectedDetails := &OperatorDetails{ + base: "تهران", + province: []string{"البرز", "سمنان", "قم", "قزوین", "زنجان"}, + simTypes: []SimType{Credit}, + operator: MCI, + } + + if details, err := GetPhoneDetails("09195431812"); err != nil { + t.Errorf("expected no error, got %v", err) + } else if !reflect.DeepEqual(details, expectedDetails) { + t.Errorf("expected %v, got %v", expectedDetails, details) + } + + if _, err := GetPhoneDetails("009195431812"); err == nil { + t.Error("expected error, got none") + } +} From 3bb9eea8d8d39869faa82f3051923004c431b5f8 Mon Sep 17 00:00:00 2001 From: Ali Pourhabibi Date: Fri, 12 Jul 2024 10:34:01 +0330 Subject: [PATCH 2/2] refactor: make GetPrefixDetails cleaner and DRY --- phone_numbers/phone.go | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/phone_numbers/phone.go b/phone_numbers/phone.go index e53369a..fef0641 100644 --- a/phone_numbers/phone.go +++ b/phone_numbers/phone.go @@ -17,33 +17,23 @@ func GetPhoneDetails(phoneNumber string) (*OperatorDetails, error) { return GetPrefixDetails(prefix) } -// GetPrefixDetails returns the OperatorDetails of the given prefix func GetPrefixDetails(prefix string) (*OperatorDetails, error) { - for k, v := range MCIMap { - if k == prefix { - return &v, nil - } - } - for k, v := range TALIYA { - if k == prefix { - return &v, nil - } - } - for k, v := range RIGHTTEL { - if k == prefix { - return &v, nil - } + // Define a slice of maps to iterate over + operatorsMap := []map[string]OperatorDetails{ + MCIMap, + TALIYA, + RIGHTTEL, + IRANCELL, + SHATELMOBILE, } - for k, v := range IRANCELL { - if k == prefix { - return &v, nil - } - } - for k, v := range SHATELMOBILE { - if k == prefix { - return &v, nil + + // Iterate over each map and check for the prefix + for _, m := range operatorsMap { + if details, found := m[prefix]; found { + return &details, nil } } + return nil, errors.New("Invalid prefix") }