Update libPhoneNumber-unsupported country codes

This commit is contained in:
Max Radermacher 2024-12-11 14:56:06 -06:00 committed by GitHub
parent f4e8ccd3bd
commit 5c38ebf6a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 176 additions and 152 deletions

View File

@ -645,6 +645,7 @@
5073EACF2C4F469A001FBB3E /* CallLinkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073EACE2C4F469A001FBB3E /* CallLinkFetcher.swift */; };
5075C21729CA1EE700A260D2 /* GroupMemberUpdaterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5075C21629CA1EE700A260D2 /* GroupMemberUpdaterTest.swift */; };
5077B5B82BBC7FE600EF399E /* ContactTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5077B5B72BBC7FE600EF399E /* ContactTest.swift */; };
50791B1D2D037A9800D747F8 /* PhoneNumberCountryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50791B1C2D037A9800D747F8 /* PhoneNumberCountryTest.swift */; };
507B69112C503FA800F1C6D7 /* CallLinkTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50552C302BAC079A00815474 /* CallLinkTest.swift */; };
507B69122C5044F800F1C6D7 /* LinkPreviewGroupLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A95517271B510400B05242 /* LinkPreviewGroupLink.swift */; };
507CD5E529660D5100E47DAC /* ServiceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CD5E429660D5100E47DAC /* ServiceId.swift */; };
@ -4392,6 +4393,7 @@
5073EACE2C4F469A001FBB3E /* CallLinkFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CallLinkFetcher.swift; path = SignalUI/Calls/CallLinkFetcher.swift; sourceTree = SOURCE_ROOT; };
5075C21629CA1EE700A260D2 /* GroupMemberUpdaterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberUpdaterTest.swift; sourceTree = "<group>"; };
5077B5B72BBC7FE600EF399E /* ContactTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactTest.swift; sourceTree = "<group>"; };
50791B1C2D037A9800D747F8 /* PhoneNumberCountryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberCountryTest.swift; sourceTree = "<group>"; };
507CD5E429660D5100E47DAC /* ServiceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceId.swift; sourceTree = "<group>"; };
507D614B2BE433EE00DA7BA3 /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = translations/be.lproj/InfoPlist.strings; sourceTree = "<group>"; };
507D614C2BE433EE00DA7BA3 /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = translations/be.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -8663,6 +8665,14 @@
path = GroupMembers;
sourceTree = "<group>";
};
50791B1B2D037A7800D747F8 /* RecipientPickers */ = {
isa = PBXGroup;
children = (
50791B1C2D037A9800D747F8 /* PhoneNumberCountryTest.swift */,
);
path = RecipientPickers;
sourceTree = "<group>";
};
50A76F512AF565A300FCF02D /* Preconditions */ = {
isa = PBXGroup;
children = (
@ -10599,6 +10609,7 @@
3471210D25ED5F850037CD1F /* Payments */,
66CDB75A2AFC42B6009A36EC /* Provisioning */,
D99ABC712A3D0BAA0034CD3B /* QRCodes */,
50791B1B2D037A7800D747F8 /* RecipientPickers */,
661278052996BA6700A1D5A1 /* Registration */,
4C3EF8002109184A0007EBF7 /* SSKTests */,
E75DD3DC2810CD3500E32C36 /* subscriptions */,
@ -16710,6 +16721,7 @@
3452851D26DFD12300824983 /* PaymentsFormatTest.swift in Sources */,
3471211025ED5F910037CD1F /* PaymentsReconciliationTest.swift in Sources */,
346EFC3225FD051400F493C7 /* PaymentsTest.swift in Sources */,
50791B1D2D037A9800D747F8 /* PhoneNumberCountryTest.swift in Sources */,
4C5250D421E7C51900CE3D95 /* PhoneNumberValidatorTest.swift in Sources */,
66CDB7602AFC45CF009A36EC /* ProvisioningCoordinator+Mocks.swift in Sources */,
66CDB75D2AFC42FC009A36EC /* ProvisioningCoordinatorTest.swift in Sources */,

View File

@ -0,0 +1,52 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import XCTest
@testable import SignalUI
final class PhoneNumberCountryTest: XCTestCase {
func testCountryCodesForSearchTerm() {
func countryCodes(forSearchTerm searchTerm: String?) -> [String] {
return PhoneNumberCountry.buildCountries(searchText: searchTerm).map(\.countryCode)
}
// Empty search.
XCTAssertGreaterThan(countryCodes(forSearchTerm: nil).count, 30)
XCTAssertGreaterThan(countryCodes(forSearchTerm: "").count, 30)
XCTAssertGreaterThan(countryCodes(forSearchTerm: " ").count, 30)
// Searches with no results.
XCTAssertEqual(countryCodes(forSearchTerm: " . ").count, 0)
XCTAssertEqual(countryCodes(forSearchTerm: " XXXXX ").count, 0)
XCTAssertEqual(countryCodes(forSearchTerm: " ! ").count, 0)
// Search by country code.
XCTAssertEqual(countryCodes(forSearchTerm: "GB"), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: "gb"), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: "GB "), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: " GB"), ["GB"])
XCTAssert(countryCodes(forSearchTerm: " G").contains("GB"))
XCTAssertFalse(countryCodes(forSearchTerm: " B").contains("GB"))
// Search by country name.
XCTAssertEqual(countryCodes(forSearchTerm: "united kingdom"), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: " UNITED KINGDOM "), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: " UNITED KING "), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: " UNI KING "), ["GB"])
XCTAssertEqual(countryCodes(forSearchTerm: " u k "), ["GB"])
XCTAssert(countryCodes(forSearchTerm: " u").contains("GB"))
XCTAssert(countryCodes(forSearchTerm: " k").contains("GB"))
XCTAssertFalse(countryCodes(forSearchTerm: " m").contains("GB"))
// Search by calling code.
XCTAssert(countryCodes(forSearchTerm: " +44 ").contains("GB"))
XCTAssert(countryCodes(forSearchTerm: " 44 ").contains("GB"))
XCTAssert(countryCodes(forSearchTerm: " +4 ").contains("GB"))
XCTAssert(countryCodes(forSearchTerm: " 4 ").contains("GB"))
XCTAssertFalse(countryCodes(forSearchTerm: " +123 ").contains("GB"))
XCTAssertFalse(countryCodes(forSearchTerm: " +444 ").contains("GB"))
}
}

View File

@ -107,54 +107,36 @@ public class PhoneNumberUtil: NSObject {
}
extension PhoneNumberUtil {
// country code -> calling code
public func plusPrefixedCallingCode(fromCountryCode countryCode: String) -> String? {
guard let countryCode = countryCode.nilIfEmpty else {
return "+0"
/// Returns calling codes for libPhoneNumber-unsupported country codes.
///
/// These are country codes that NSLocale.isoCountryCodes contains but
/// libPhoneNumber doesn't support. In every case, these countries share a
/// calling code with a country that libPhoneNumber *does* support. We show
/// these unsupported countries in the UI and convert them to a supported
/// country when parsing the number.
public static func callingCodeForUnsupportedCountryCode(_ countryCode: String) -> Int? {
switch countryCode {
case "AQ": /* Antarctica */ return 672
case "BV": /* Bouvet Island */ return 55
case "IC": /* Canary Islands */ return 34
case "EA": /* Ceuta & Melilla */ return 34
case "DG": /* Diego Garcia */ return 246
case "TF": /* French Southern Territories */ return 262
case "HM": /* Heard & McDonald Islands */ return 672
case "PN": /* Pitcairn Islands */ return 64
case "CQ": /* Sark */ return 44
case "GS": /* So. Georgia & So. Sandwich Isl. */ return 500
case "UM": /* U.S. Outlying Islands */ return 1
default: return nil
}
}
if countryCode == "AQ" {
// Antarctica
return "+672"
} else if countryCode == "BV" {
// Bouvet Island
return "+55"
} else if countryCode == "IC" {
// Canary Islands
return "+34"
} else if countryCode == "EA" {
// Ceuta & Melilla
return "+34"
} else if countryCode == "CP" {
// Clipperton Island
//
// This country code should be filtered - it does not appear to have a calling code.
return nil
} else if countryCode == "DG" {
// Diego Garcia
return "+246"
} else if countryCode == "TF" {
// French Southern Territories
return "+262"
} else if countryCode == "HM" {
// Heard & McDonald Islands
return "+672"
} else if countryCode == "XK" {
// Kosovo
return "+383"
} else if countryCode == "PN" {
// Pitcairn Islands
return "+64"
} else if countryCode == "GS" {
// So. Georgia & So. Sandwich Isl.
return "+500"
} else if countryCode == "UM" {
// U.S. Outlying Islands
return "+1"
public func countryCodeForParsing(fromCountryCode countryCode: String) -> String {
if let callingCode = Self.callingCodeForUnsupportedCountryCode(countryCode) {
// Force unwrap is covered by unit tests.
return getFilteredRegionCodeForCallingCode(callingCode)!
}
let callingCode = getCallingCode(forRegion: countryCode)
return PhoneNumber.countryCodePrefix + "\(callingCode)"
return countryCode
}
public static func defaultCountryCode() -> String {
@ -245,52 +227,9 @@ extension PhoneNumberUtil {
return try? format(phoneNumber.nbPhoneNumber, numberFormat: .NATIONAL)
}
private class func does(_ string: String, matchQuery query: String) -> Bool {
let searchOptions: String.CompareOptions = [.caseInsensitive, .anchored]
let stringTokens = string.components(separatedBy: .whitespaces)
let queryTokens = query.components(separatedBy: .whitespaces)
return queryTokens.allSatisfy { queryToken in
if queryToken.isEmpty {
return true
}
return stringTokens.contains { stringToken in
stringToken.range(of: queryToken, options: searchOptions) != nil
}
}
}
/// Get country codes from a search term.
public func countryCodes(forSearchTerm searchTerm: String?) -> [String] {
let cleanedSearch = (searchTerm ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let codes = NSLocale.isoCountryCodes.filter { countryCode in
guard
let callingCode = plusPrefixedCallingCode(fromCountryCode: countryCode),
callingCode != "+0"
else {
return false
}
let countryName = Self.countryName(fromCountryCode: countryCode)
return (
cleanedSearch.isEmpty ||
Self.does(countryName, matchQuery: cleanedSearch) ||
Self.does(countryCode, matchQuery: cleanedSearch) ||
callingCode.contains(cleanedSearch)
)
}
return codes.sorted { lhs, rhs in
let lhsCountry = Self.countryName(fromCountryCode: lhs)
let rhsCountry = Self.countryName(fromCountryCode: rhs)
return lhsCountry.localizedCaseInsensitiveCompare(rhsCountry) == .orderedAscending
}
}
/// Convert country code to country name.
public class func countryName(fromCountryCode countryCode: String) -> String {
lazy var unknownValue = OWSLocalizedString(
public static func countryName(fromCountryCode countryCode: String) -> String {
lazy var unknownValue = OWSLocalizedString(
"UNKNOWN_VALUE",
comment: "Indicates an unknown or unrecognizable value."
)
@ -331,7 +270,10 @@ extension PhoneNumberUtil {
}
public func parsePhoneNumber(countryCode: String, nationalNumber: String) -> PhoneNumber? {
return _parsePhoneNumber(filteredValue: nationalNumber.filteredAsE164, countryCode: countryCode)
return _parsePhoneNumber(
filteredValue: nationalNumber.filteredAsE164,
countryCode: countryCodeForParsing(fromCountryCode: countryCode)
)
}
public func parsePhoneNumbers(userSpecifiedText: String, localPhoneNumber: String?) -> [PhoneNumber] {

View File

@ -15,17 +15,30 @@ class PhoneNumberUtilTestSwift: XCTestCase {
self.phoneNumberUtilRef = PhoneNumberUtil()
}
func testCountryCodeForParsing() {
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for ch1 in alphabet {
for ch2 in alphabet {
_ = self.phoneNumberUtilRef.countryCodeForParsing(fromCountryCode: String(ch1) + String(ch2))
}
}
}
func test_callingCodeFromCountryCode() {
XCTAssertEqual("+1", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "US"))
XCTAssertEqual("+44", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "GB"))
XCTAssertEqual("+598", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "UY"))
XCTAssertEqual("+0", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "QG"))
XCTAssertEqual("+0", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "EK"))
XCTAssertEqual("+0", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "ZZZ"))
XCTAssertEqual("+0", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: ""))
XCTAssertEqual("+0", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "+"))
XCTAssertEqual("+0", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "9"))
XCTAssertEqual("+1", phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: "US "))
func plusPrefixedCallingCode(fromCountryCode countryCode: String) -> String {
let callingCode = phoneNumberUtilRef.getCallingCode(forRegion: countryCode)
return "+\(callingCode)"
}
XCTAssertEqual("+1", plusPrefixedCallingCode(fromCountryCode: "US"))
XCTAssertEqual("+44", plusPrefixedCallingCode(fromCountryCode: "GB"))
XCTAssertEqual("+598", plusPrefixedCallingCode(fromCountryCode: "UY"))
XCTAssertEqual("+0", plusPrefixedCallingCode(fromCountryCode: "QG"))
XCTAssertEqual("+0", plusPrefixedCallingCode(fromCountryCode: "EK"))
XCTAssertEqual("+0", plusPrefixedCallingCode(fromCountryCode: "ZZZ"))
XCTAssertEqual("+0", plusPrefixedCallingCode(fromCountryCode: ""))
XCTAssertEqual("+0", plusPrefixedCallingCode(fromCountryCode: "+"))
XCTAssertEqual("+0", plusPrefixedCallingCode(fromCountryCode: "9"))
XCTAssertEqual("+1", plusPrefixedCallingCode(fromCountryCode: "US "))
}
func test_examplePhoneNumberForCountryCode() {
@ -58,44 +71,6 @@ class PhoneNumberUtilTestSwift: XCTestCase {
XCTAssertEqual(0, phoneNumberUtilRef.getCallingCode(forRegion: "ZQ"))
}
func testCountryCodesForSearchTerm() {
// Empty search.
XCTAssertGreaterThan(phoneNumberUtilRef.countryCodes(forSearchTerm: nil).count, 30)
XCTAssertGreaterThan(phoneNumberUtilRef.countryCodes(forSearchTerm: "").count, 30)
XCTAssertGreaterThan(phoneNumberUtilRef.countryCodes(forSearchTerm: " ").count, 30)
// Searches with no results.
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " . ").count, 0)
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " XXXXX ").count, 0)
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " ! ").count, 0)
// Search by country code.
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: "GB"), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: "gb"), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: "GB "), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " GB"), ["GB"])
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " G").contains("GB"))
XCTAssertFalse(phoneNumberUtilRef.countryCodes(forSearchTerm: " B").contains("GB"))
// Search by country name.
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: "united kingdom"), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " UNITED KINGDOM "), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " UNITED KING "), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " UNI KING "), ["GB"])
XCTAssertEqual(phoneNumberUtilRef.countryCodes(forSearchTerm: " u k "), ["GB"])
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " u").contains("GB"))
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " k").contains("GB"))
XCTAssertFalse(phoneNumberUtilRef.countryCodes(forSearchTerm: " m").contains("GB"))
// Search by calling code.
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " +44 ").contains("GB"))
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " 44 ").contains("GB"))
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " +4 ").contains("GB"))
XCTAssert(phoneNumberUtilRef.countryCodes(forSearchTerm: " 4 ").contains("GB"))
XCTAssertFalse(phoneNumberUtilRef.countryCodes(forSearchTerm: " +123 ").contains("GB"))
XCTAssertFalse(phoneNumberUtilRef.countryCodes(forSearchTerm: " +444 ").contains("GB"))
}
func testTranslateCursorPosition() {
XCTAssertEqual(0, PhoneNumberUtil.translateCursorPosition(0, from: "", to: "", stickingRightward: true))

View File

@ -33,9 +33,60 @@ public struct PhoneNumberCountry: Equatable {
// MARK: -
private static func bestEffortCallingCode(fromCountryCode countryCode: String) -> Int? {
if let result = PhoneNumberUtil.callingCodeForUnsupportedCountryCode(countryCode) {
return result
}
let result = SSKEnvironment.shared.phoneNumberUtilRef.getCallingCode(forRegion: countryCode)
return result == 0 ? nil : result
}
private static func does(_ string: String, matchQuery query: String) -> Bool {
let searchOptions: String.CompareOptions = [.caseInsensitive, .anchored]
let stringTokens = string.components(separatedBy: .whitespaces)
let queryTokens = query.components(separatedBy: .whitespaces)
return queryTokens.allSatisfy { queryToken in
if queryToken.isEmpty {
return true
}
return stringTokens.contains { stringToken in
stringToken.range(of: queryToken, options: searchOptions) != nil
}
}
}
/// Get country codes from a search term.
private static func countryCodes(forSearchTerm searchTerm: String?) -> [String] {
let cleanedSearch = (searchTerm ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
var countryCodesAndNames = NSLocale.isoCountryCodes.compactMap { countryCode -> (countryCode: String, countryName: String)? in
guard let callingCode = bestEffortCallingCode(fromCountryCode: countryCode) else {
// Clipperton Island is filtered intentionally.
owsAssertDebug(countryCode == "CP")
return nil
}
let countryName = PhoneNumberUtil.countryName(fromCountryCode: countryCode)
let isMatch = (
cleanedSearch.isEmpty ||
Self.does(countryName, matchQuery: cleanedSearch) ||
Self.does(countryCode, matchQuery: cleanedSearch) ||
"+\(callingCode)".contains(cleanedSearch)
)
return isMatch ? (countryCode, countryName) : nil
}
countryCodesAndNames.sort(by: { lhs, rhs in
return lhs.countryName.localizedCaseInsensitiveCompare(rhs.countryName) == .orderedAscending
})
return countryCodesAndNames.map(\.countryCode)
}
public static func buildCountries(searchText: String?) -> [PhoneNumberCountry] {
let searchText = searchText?.strippedOrNil
let countryCodes: [String] = SSKEnvironment.shared.phoneNumberUtilRef.countryCodes(forSearchTerm: searchText)
let countryCodes: [String] = countryCodes(forSearchTerm: searchText)
return PhoneNumberCountry.buildCountries(forCountryCodes: countryCodes)
}
@ -50,11 +101,11 @@ public struct PhoneNumberCountry: Equatable {
}
public static func buildCountry(forCountryCode countryCode: String) -> PhoneNumberCountry? {
guard let plusPrefixedCallingCode = SSKEnvironment.shared.phoneNumberUtilRef.plusPrefixedCallingCode(fromCountryCode: countryCode) else {
guard let callingCode = bestEffortCallingCode(fromCountryCode: countryCode) else {
owsFailDebug("Invalid countryCode.")
return nil
}
return buildCountry(countryCode: countryCode, plusPrefixedCallingCode: plusPrefixedCallingCode)
return buildCountry(countryCode: countryCode, plusPrefixedCallingCode: "+\(callingCode)")
}
public static func buildCountry(forCallingCode callingCode: Int) -> PhoneNumberCountry? {
@ -68,17 +119,9 @@ public struct PhoneNumberCountry: Equatable {
return buildCountry(countryCode: countryCode, plusPrefixedCallingCode: "+\(callingCode)")
}
private static func buildCountry(countryCode: String, plusPrefixedCallingCode: String) -> PhoneNumberCountry? {
guard let countryName = PhoneNumberUtil.countryName(fromCountryCode: countryCode).strippedOrNil else {
owsFailDebug("Invalid countryName.")
return nil
}
guard plusPrefixedCallingCode != "+0" else {
owsFailDebug("Invalid callingCode.")
return nil
}
private static func buildCountry(countryCode: String, plusPrefixedCallingCode: String) -> PhoneNumberCountry {
return PhoneNumberCountry(
countryName: countryName,
countryName: PhoneNumberUtil.countryName(fromCountryCode: countryCode),
plusPrefixedCallingCode: plusPrefixedCallingCode,
countryCode: countryCode
)