Last active
January 16, 2024 11:56
-
-
Save dineybomfim/86bb72209ee525a9014c5ed6719ac489 to your computer and use it in GitHub Desktop.
The ultimate, free to use, no dependency, runtime automatically update, worldwide global Phone Number Formatter, works for any country and every single particular national phone number pattern, including carrier codes, international codes and country codes.
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
/* | |
* PhoneNumberFormatter.swift | |
* | |
* Created by Diney Bomfim on 12/12/22. | |
* | |
* Swift code that automatically loads the global phone format list, parses it and formats a given input. | |
* For everyone that believes Phone Number Formatting should be open source, global standard and easy to use. | |
* This is based on Google Project https://github.com/google/libphonenumber. Updated constantly by Google engineers. | |
*/ | |
import Foundation | |
// MARK: - Definitions - | |
private enum StringDigits: Codable { | |
case string(String) | |
case array([String]) | |
var stringValue: String? { | |
switch self { | |
case .string(let value): | |
return value | |
default: | |
return nil | |
} | |
} | |
var arrayValue: [String] { | |
switch self { | |
case .array(let value): | |
return value | |
default: | |
return [] | |
} | |
} | |
init(from decoder: Decoder) throws { | |
if let value = try? decoder.singleValueContainer().decode(String.self) { | |
self = .string(value) | |
return | |
} else if let value = try? decoder.singleValueContainer().decode([String].self) { | |
self = .array(value) | |
return | |
} | |
self = .string("") | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .string(let value): | |
try container.encode(value) | |
case .array(let value): | |
try container.encode(value) | |
} | |
} | |
} | |
private struct PhoneNumberMetadata : Codable { | |
struct PhoneLengths : Codable { | |
let national: String? | |
let localOnly: String? | |
} | |
struct PhonePattern : Codable { | |
let exampleNumber: String | |
let nationalNumberPattern: String | |
let possibleLengths: PhoneLengths | |
} | |
struct GeneralDescription : Codable { | |
let nationalNumberPattern: String | |
} | |
struct NumberFormat : Codable { | |
let pattern: String | |
let format: String | |
let nationalPrefixFormattingRule: String? | |
let leadingDigits: StringDigits? | |
} | |
struct AvailableFormats : Codable { | |
let numberFormat: [NumberFormat] | |
} | |
struct Territory : Codable { | |
let id: String | |
let countryCode: String | |
let leadingDigits: String? | |
let nationalPrefix: String? | |
let internationalPrefix: String? | |
let availableFormats: AvailableFormats? | |
let generalDesc: GeneralDescription? | |
let fixedLine: PhonePattern? | |
let mobile: PhonePattern? | |
} | |
struct Territories : Codable { | |
let territory: [Territory] | |
} | |
struct Metadata : Codable { | |
let territories: Territories | |
} | |
let phoneNumberMetadata: Metadata | |
} | |
private extension String { | |
func hasMatch(_ pattern: String?) -> Bool { | |
guard let validPattern = pattern else { return false } | |
return range(of: validPattern, options: .regularExpression) != nil | |
} | |
func removingPrefix(_ prefix: String?) -> String { | |
guard let validPrefix = prefix else { return self } | |
return hasPrefix(validPrefix) ? replacingOccurrences(of: "^\(validPrefix)", with: "", options: .regularExpression) : self | |
} | |
func replacing(_ pattern: String, with format: String) -> String { | |
replacingOccurrences(of: pattern, with: format, options: .regularExpression) | |
} | |
func formating(with territory: PhoneNumberMetadata.Territory) -> String { | |
var result = self | |
territory.availableFormats?.numberFormat.first(where: { format in | |
guard | |
let leading = format.leadingDigits?.stringValue, | |
hasMatch("^(\(leading))") | |
else { return false } | |
result = replacing(format.pattern, with: format.format) | |
return true | |
}) | |
return result | |
} | |
} | |
// MARK: - Type - | |
public struct PhoneNumberFormatter { | |
// MARK: - Properties | |
private static var url: URL? { .init(string: "https://tinyurl.com/phonenumbermetadata") } | |
private static var territories: [String : PhoneNumberMetadata.Territory] = [:] | |
private static var codes: [String : String] = [:] | |
public let countryCode: String | |
public let internationalCode: String | |
public let digits: String | |
public let example: String | |
public let mask: String | |
public let formatted: String | |
public var formattedWithCountry: String { "+\(internationalCode) \(formatted)" } | |
public var formattedByMask: String { | |
var formatted = "" | |
var index = digits.startIndex | |
var end = digits.endIndex | |
for character in mask where index < end { | |
if character == "X" { | |
formatted.append(digits[index]) | |
index = digits.index(after: index) | |
} else { | |
formatted.append(character) | |
} | |
} | |
if index < end { | |
formatted.append(contentsOf: digits.suffix(from: index)) | |
} | |
return formatted | |
} | |
// MARK: - Constructors | |
public init(inferringCountryFrom number: String) { | |
let normalized = number.replacing("\\D", with: "") | |
for index in (1...3).reversed() { | |
guard let country = Self.codes[String(normalized.prefix(index))] else { continue } | |
self.init(country: country, number: number) | |
return | |
} | |
self.init(country: "", number: number) | |
} | |
public init(country: String, number: String) { | |
let id = country.uppercased() | |
guard let territory = Self.territories[id] else { | |
self.countryCode = "" | |
self.internationalCode = "" | |
self.digits = number | |
self.example = "" | |
self.mask = "" | |
self.formatted = "" | |
return | |
} | |
let normalized = number.replacing("\\D", with: "") | |
let unprefixed = normalized.removingPrefix(territory.countryCode) | |
let digitsOnly = unprefixed.removingPrefix(territory.internationalPrefix).removingPrefix(territory.nationalPrefix) | |
let mobileExample = territory.mobile?.exampleNumber ?? "" | |
let maskFallback = mobileExample.formating(with: territory) | |
var formatted = digitsOnly.formating(with: territory) | |
self.countryCode = territory.id | |
self.internationalCode = territory.countryCode | |
self.digits = digitsOnly | |
self.example = mobileExample | |
self.mask = (formatted != digitsOnly ? formatted : maskFallback).replacing("\\d", with: "X") | |
self.formatted = formatted | |
} | |
// MARK: - Protected Methods | |
// MARK: - Exposed Methods | |
public static func load(completion: @escaping () -> Void) { | |
guard territories.isEmpty || codes.isEmpty else { | |
completion() | |
return | |
} | |
DispatchQueue.global().async { | |
guard | |
let validURL = url, | |
let data = try? Data(contentsOf: validURL) | |
else { return } | |
do { | |
let formatter = try JSONDecoder().decode(PhoneNumberMetadata.self, from: data) | |
formatter.phoneNumberMetadata.territories.territory.forEach { | |
territories[$0.id] = $0 | |
codes[$0.countryCode] = $0.id | |
} | |
} catch { | |
print(error) | |
} | |
DispatchQueue.main.async { | |
completion() | |
} | |
} | |
} | |
} | |
// MARK: - For Playground purposes | |
import PlaygroundSupport | |
let page = PlaygroundPage.current | |
page.needsIndefiniteExecution = true | |
// For easy to use on phone number text fields, this is how I recommend to use it: | |
// Load runs once in background. | |
PhoneNumberFormatter.load { | |
// Once loaded, a formating attempting can start happening as soon as the user starts typing. | |
print(PhoneNumberFormatter(inferringCountryFrom: "9").countryCode) | |
// User continues to type | |
print(PhoneNumberFormatter(inferringCountryFrom: "971").countryCode) | |
// User goes on | |
print(PhoneNumberFormatter(inferringCountryFrom: "97154").formattedByMask) | |
print(PhoneNumberFormatter(inferringCountryFrom: "971541234").formattedByMask) | |
print(PhoneNumberFormatter(inferringCountryFrom: "971541234567").formattedByMask) | |
// Some more Random examples | |
let phone1 = PhoneNumberFormatter(country: "ae", number: "+9710541234567") | |
print(phone1.countryCode, ":", phone1.example, ":", phone1.mask) | |
print(phone1.formatted) | |
print(phone1.formattedWithCountry) | |
print(phone1.formattedByMask) | |
let phone2 = PhoneNumberFormatter(inferringCountryFrom: "+9710541234567") | |
print(phone2.countryCode, ":", phone2.example, ":", phone2.mask) | |
print(phone2.formatted) | |
print(phone2.formattedWithCountry) | |
print(phone2.formattedByMask) | |
let phone3 = PhoneNumberFormatter(country: "us", number: "650 5131234") | |
print(phone3.countryCode, ":", phone3.example, ":", phone3.mask) | |
print(phone3.formatted) | |
print(phone3.formattedWithCountry) | |
print(phone3.formattedByMask) | |
let phone4 = PhoneNumberFormatter(inferringCountryFrom: "55 11 96531 8943") | |
print(phone4.countryCode, ":", phone4.example, ":", phone4.mask) | |
print(phone4.formatted) | |
print(phone4.formattedWithCountry) | |
print(phone4.formattedByMask) | |
page.finishExecution() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment