Skip to content

Instantly share code, notes, and snippets.

@dineybomfim
Last active January 16, 2024 11:56
Show Gist options
  • Save dineybomfim/86bb72209ee525a9014c5ed6719ac489 to your computer and use it in GitHub Desktop.
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.
/*
* 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