2016-08-30 21:44:58 +03:00
|
|
|
|
/**
|
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
|
* See LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
class Matcher {
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
static let uppercaseCharSet = CharacterSet.uppercaseLetters
|
2016-08-30 21:44:58 +03:00
|
|
|
|
|
|
|
|
|
enum ExactMatchResult {
|
|
|
|
|
case none
|
|
|
|
|
case exact
|
|
|
|
|
case prefix
|
|
|
|
|
case suffix
|
|
|
|
|
case contains
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
static func exactMatchIgnoringCase(_ target: String, pattern: String) -> ExactMatchResult {
|
|
|
|
|
let ltarget = target.lowercased()
|
|
|
|
|
let lpattern = pattern.lowercased()
|
2016-08-30 21:44:58 +03:00
|
|
|
|
if ltarget == lpattern {
|
|
|
|
|
return .exact
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ltarget.hasPrefix(lpattern) {
|
|
|
|
|
return .prefix
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ltarget.hasSuffix(lpattern) {
|
|
|
|
|
return .suffix
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
if ltarget.contains(lpattern) {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
return .contains
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return .none
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
static func numberOfUppercaseMatches(_ target: String, pattern: String) -> Int {
|
|
|
|
|
var tscalars = target.unicodeScalars.filter { uppercaseCharSet.contains(UnicodeScalar($0.value)!) }
|
2016-08-30 21:44:58 +03:00
|
|
|
|
|
2016-09-08 21:16:37 +03:00
|
|
|
|
let count = tscalars.count
|
|
|
|
|
guard count > 0 else {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let pscalars = pattern.uppercased().unicodeScalars
|
2016-09-08 21:16:37 +03:00
|
|
|
|
|
|
|
|
|
pscalars.forEach { scalar in
|
2016-09-25 18:50:33 +03:00
|
|
|
|
if let idx = tscalars.index(of: scalar) {
|
|
|
|
|
tscalars.remove(at: idx)
|
2016-09-08 21:16:37 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return count - tscalars.count
|
2016-08-30 21:44:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Matches `pattern` to `target` in a fuzzy way.
|
|
|
|
|
/// - returns: `Array` of `Range<String.UnicodeScalarIndex>`
|
2016-09-25 19:53:46 +03:00
|
|
|
|
static func fuzzyIgnoringCase(_ target: String,
|
|
|
|
|
pattern: String) -> (matches: Int, ranges: [CountableClosedRange<Int>]) {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let tlower = target.lowercased()
|
|
|
|
|
let plower = pattern.lowercased()
|
2016-08-30 21:44:58 +03:00
|
|
|
|
|
|
|
|
|
let tchars = tlower.unicodeScalars
|
|
|
|
|
let pchars = plower.unicodeScalars
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
var flags = Array(repeating: false, count: tchars.count)
|
2016-08-30 21:44:58 +03:00
|
|
|
|
|
|
|
|
|
var pidx = pchars.startIndex
|
2016-09-25 18:50:33 +03:00
|
|
|
|
for (i, tchar) in tchars.enumerated() {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
if pchars[pidx] == tchar {
|
|
|
|
|
flags[i] = true
|
2016-09-25 18:50:33 +03:00
|
|
|
|
pidx = pchars.index(after: pidx)
|
2016-08-30 21:44:58 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 19:53:46 +03:00
|
|
|
|
var ranges: [CountableClosedRange<Int>] = []
|
2016-08-30 21:44:58 +03:00
|
|
|
|
var matches = 0
|
|
|
|
|
|
|
|
|
|
var lastTrue = -1
|
|
|
|
|
var curTrue = -1
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
for (i, isTrue) in flags.enumerated() {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
if isTrue {
|
|
|
|
|
matches = matches &+ 1
|
|
|
|
|
if lastTrue == -1 {
|
|
|
|
|
lastTrue = i
|
|
|
|
|
}
|
|
|
|
|
curTrue = i
|
|
|
|
|
|
|
|
|
|
if i == flags.count &- 1 {
|
|
|
|
|
if lastTrue > -1 && curTrue > -1 {
|
2016-09-25 19:53:46 +03:00
|
|
|
|
ranges.append(lastTrue...curTrue)
|
2016-08-30 21:44:58 +03:00
|
|
|
|
lastTrue = -1
|
|
|
|
|
curTrue = -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if lastTrue > -1 && curTrue > -1 {
|
2016-09-25 19:53:46 +03:00
|
|
|
|
ranges.append(lastTrue...curTrue)
|
2016-08-30 21:44:58 +03:00
|
|
|
|
lastTrue = -1
|
|
|
|
|
curTrue = -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (matches, ranges)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wagner-Fischer algorithm.
|
|
|
|
|
/// We use the 32 bit representation (`String.unicodeScalars`) of both parameters to compare them.
|
|
|
|
|
///
|
|
|
|
|
/// - returns: the distance of pattern from target
|
|
|
|
|
/// - seealso: https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm
|
2016-09-25 18:50:33 +03:00
|
|
|
|
static func wagnerFisherDistance(_ target: String, pattern: String) -> Int {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
let s = target.unicodeScalars
|
|
|
|
|
let t = pattern.unicodeScalars
|
|
|
|
|
|
|
|
|
|
let m = s.count
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
var prevRow = Array(repeating: 0, count: m &+ 1)
|
|
|
|
|
var curRow = Array(repeating: 0, count: m &+ 1)
|
2016-08-30 21:44:58 +03:00
|
|
|
|
|
|
|
|
|
for i in 0...m {
|
|
|
|
|
prevRow[i] = i
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
for (j, tchar) in t.enumerated() {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
curRow[0] = j &+ 1
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
for (i, schar) in s.enumerated() {
|
2016-08-30 21:44:58 +03:00
|
|
|
|
if schar == tchar {
|
|
|
|
|
curRow[i &+ 1] = prevRow[i]
|
|
|
|
|
} else {
|
|
|
|
|
curRow[i &+ 1] = min(curRow[i] &+ 1, prevRow[i &+ 1] &+ 1, prevRow[i] &+ 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prevRow = curRow
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return curRow[m]
|
|
|
|
|
}
|
|
|
|
|
}
|