From f57a5dbc77a9e1119f69f589ab975ece3c5c0b49 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 12 Jun 2018 15:26:52 -0400 Subject: [PATCH] Match searches for national number format // FREEBIE --- Signal/test/util/SearcherTest.swift | 18 +++--- SignalServiceKit/src/Contacts/PhoneNumber.h | 3 +- SignalServiceKit/src/Contacts/PhoneNumber.m | 30 +++++---- .../src/Storage/FullTextSearchFinder.swift | 61 +++++++++++++++---- 4 files changed, 72 insertions(+), 40 deletions(-) diff --git a/Signal/test/util/SearcherTest.swift b/Signal/test/util/SearcherTest.swift index e8f7a51a7..0fcba4d57 100644 --- a/Signal/test/util/SearcherTest.swift +++ b/Signal/test/util/SearcherTest.swift @@ -210,18 +210,16 @@ class ConversationSearcherTest: XCTestCase { XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads) } - // TODO - func pending_testSearchContactByNumber() { - var resultSet: SearchResultSet = .empty - + func testSearchContactByNumberWithoutCountryCode() { + var threads: [ThreadViewModel] = [] // Phone Number formatting should be forgiving - resultSet = getResultSet(searchText: "234.56") - XCTAssertEqual(1, resultSet.conversations.count) - XCTAssertEqual(aliceThread, resultSet.conversations.first?.thread) + threads = searchConversations(searchText: "234.56") + XCTAssertEqual(3, threads.count) + XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads) - resultSet = getResultSet(searchText: "234 56") - XCTAssertEqual(1, resultSet.conversations.count) - XCTAssertEqual(aliceThread, resultSet.conversations.first?.thread) + threads = searchConversations(searchText: "234 56") + XCTAssertEqual(3, threads.count) + XCTAssertEqual([bookClubThread, snackClubThread, aliceThread], threads) } func testSearchConversationByContactByName() { diff --git a/SignalServiceKit/src/Contacts/PhoneNumber.h b/SignalServiceKit/src/Contacts/PhoneNumber.h index d98b814cf..b83ce28a0 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumber.h +++ b/SignalServiceKit/src/Contacts/PhoneNumber.h @@ -35,10 +35,9 @@ - (NSURL *)toSystemDialerURL; - (NSString *)toE164; -- (NSString *)localizedDescriptionForUser; - (NSNumber *)getCountryCode; +@property (nonatomic, readonly, nullable) NSString *nationalNumber; - (BOOL)isValid; -- (BOOL)resolvesInternationallyTo:(PhoneNumber *)otherPhoneNumber; - (NSComparisonResult)compare:(PhoneNumber *)other; diff --git a/SignalServiceKit/src/Contacts/PhoneNumber.m b/SignalServiceKit/src/Contacts/PhoneNumber.m index 83b1cf8ac..767416dda 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumber.m +++ b/SignalServiceKit/src/Contacts/PhoneNumber.m @@ -372,25 +372,23 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return self.phoneNumber.countryCode; } -- (BOOL)isValid { - return [[PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil isValidNumber:self.phoneNumber]; -} - -- (NSString *)localizedDescriptionForUser { - NBPhoneNumberUtil *phoneUtil = [PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil; - - NSError *formatError = nil; - NSString *pretty = - [phoneUtil format:self.phoneNumber numberFormat:NBEPhoneNumberFormatINTERNATIONAL error:&formatError]; - - if (formatError != nil) { - return self.e164; +- (nullable NSString *)nationalNumber +{ + NSError *error; + NSString *nationalNumber = [[PhoneNumberUtil sharedThreadLocal] format:self.phoneNumber + numberFormat:NBEPhoneNumberFormatNATIONAL + error:&error]; + if (error) { + DDLogVerbose(@"%@ error parsing number into national format: %@", self.logTag, error); + return nil; } - return pretty; + + return nationalNumber; } -- (BOOL)resolvesInternationallyTo:(PhoneNumber *)otherPhoneNumber { - return [self.toE164 isEqualToString:otherPhoneNumber.toE164]; +- (BOOL)isValid +{ + return [[PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil isValidNumber:self.phoneNumber]; } - (NSString *)description { diff --git a/SignalServiceKit/src/Storage/FullTextSearchFinder.swift b/SignalServiceKit/src/Storage/FullTextSearchFinder.swift index c1db9378b..09ad092f7 100644 --- a/SignalServiceKit/src/Storage/FullTextSearchFinder.swift +++ b/SignalServiceKit/src/Storage/FullTextSearchFinder.swift @@ -27,11 +27,11 @@ public class FullTextSearchFinder: NSObject { return } - let normalized = FullTextSearchFinder.normalize(text: searchText) + let normalized = FullTextSearchFinder.normalize(queryText: searchText) - // We want a forgiving query for phone numbers - // TODO a stricter "whole word" query for body text? - let prefixQuery = "*\(normalized)*" + // We want to match by prefix for "search as you type" functionality. + // SQLite does not support suffix or contains matches. + let prefixQuery = "\(normalized)*" let maxSearchResults = 500 var searchResultCount = 0 @@ -57,10 +57,10 @@ public class FullTextSearchFinder: NSObject { return TextSecureKitEnv.shared().contactsManager } - private class func normalize(text: String) -> String { - var normalized: String = text.trimmingCharacters(in: .whitespacesAndNewlines) + private class func normalize(indexingText: String) -> String { + var normalized: String = indexingText.trimmingCharacters(in: .whitespacesAndNewlines) - // Remove any phone number formatting from the search terms + // Remove any formatting from the search terms let nonformattingScalars = normalized.unicodeScalars.lazy.filter { !CharacterSet.punctuationCharacters.contains($0) } @@ -70,6 +70,27 @@ public class FullTextSearchFinder: NSObject { return normalized } + private class func normalize(queryText: String) -> String { + var normalized: String = queryText.trimmingCharacters(in: .whitespacesAndNewlines) + + // Remove any formatting from the search terms + let nonformattingScalars = normalized.unicodeScalars.lazy.filter { + !CharacterSet.punctuationCharacters.contains($0) + } + let normalizedChars = String(String.UnicodeScalarView(nonformattingScalars)) + + let digitsOnlyScalars = normalized.unicodeScalars.lazy.filter { + CharacterSet.decimalDigits.contains($0) + } + let normalizedDigits = String(String.UnicodeScalarView(digitsOnlyScalars)) + + if normalizedDigits.count > 0 { + return "\(normalizedChars) OR \(normalizedDigits)" + } else { + return "\(normalizedChars)" + } + } + private static let groupThreadIndexer: SearchIndexer = SearchIndexer { (groupThread: TSGroupThread) in let groupName = groupThread.groupModel.groupName ?? "" @@ -79,27 +100,43 @@ public class FullTextSearchFinder: NSObject { let searchableContent = "\(groupName) \(memberStrings)" - return normalize(text: searchableContent) + return normalize(indexingText: searchableContent) } private static let contactThreadIndexer: SearchIndexer = SearchIndexer { (contactThread: TSContactThread) in let recipientId = contactThread.contactIdentifier() let searchableContent = recipientIndexer.index(recipientId) - return normalize(text: searchableContent) + return normalize(indexingText: searchableContent) } private static let recipientIndexer: SearchIndexer = SearchIndexer { (recipientId: String) in let displayName = contactsManager.displayName(forPhoneIdentifier: recipientId) - let searchableContent = "\(recipientId) \(displayName)" - return normalize(text: searchableContent) + let nationalNumber: String = { (recipientId: String) -> String in + + guard let phoneNumber = PhoneNumber(fromE164: recipientId) else { + assertionFailure("unexpected unparseable recipientId: \(recipientId)") + return "" + } + + guard let digitScalars = phoneNumber.nationalNumber?.unicodeScalars.filter({ CharacterSet.decimalDigits.contains($0) }) else { + assertionFailure("unexpected unparseable recipientId: \(recipientId)") + return "" + } + + return String(String.UnicodeScalarView(digitScalars)) + }(recipientId) + + let searchableContent = "\(recipientId) \(nationalNumber) \(displayName)" + + return normalize(indexingText: searchableContent) } private static let messageIndexer: SearchIndexer = SearchIndexer { (message: TSMessage) in let searchableContent = message.body ?? "" - return normalize(text: searchableContent) + return normalize(indexingText: searchableContent) } private class func indexContent(object: Any) -> String? {