import UIKit public func findSubstringRanges(in string: String, query: String) -> ([Range<String.Index>], String) { var ranges: [Range<String.Index>] = [] let queryWords = query.split { !$0.isLetter && !$0.isNumber && $0 != "#" && $0 != "@" }.filter { !$0.isEmpty && !["#", "@"].contains($0) }.map { $0.lowercased() } let text = string.lowercased() let searchRange = text.startIndex ..< text.endIndex text.enumerateSubstrings(in: searchRange, options: .byWords) { (rawSubstring, rawRange, _, _) in guard let rawSubstring = rawSubstring else { return } var substrings: [(String, Range<String.Index>)] = [] if let index = rawSubstring.firstIndex(of: "'") { let leftString = String(rawSubstring[..<index]) let rightString = String(rawSubstring[rawSubstring.index(after: index)...]) if !leftString.isEmpty { substrings.append((leftString, rawRange.lowerBound ..< text.index(rawRange.lowerBound, offsetBy: leftString.count))) } if !rightString.isEmpty { substrings.append((rightString, text.index(rawRange.lowerBound, offsetBy: leftString.count + 1) ..< rawRange.upperBound)) } } else { substrings.append((rawSubstring, rawRange)) } for (substring, range) in substrings { for var word in queryWords { var count = 0 var hasLeadingSymbol = false if word.hasPrefix("#") || word.hasPrefix("@") { hasLeadingSymbol = true word.removeFirst() } inner: for (c1, c2) in zip(word, substring) { if c1 != c2 { break inner } count += 1 } if count > 0 { let length = Double(max(word.count, substring.count)) if length > 0 { let difference = abs(length - Double(count)) let rating = difference / length if rating < 0.37 { var range = range if hasLeadingSymbol && range.lowerBound > searchRange.lowerBound { range = text.index(before: range.lowerBound)..<range.upperBound } ranges.append(range) } } } } } } return (ranges, text) }