Skip to content

Commit 6050cc2

Browse files
committed
relax Collection index requirement
1 parent bbfc00f commit 6050cc2

File tree

1 file changed

+85
-71
lines changed

1 file changed

+85
-71
lines changed

Sources/Fuzzy/Fuzzy.swift

Lines changed: 85 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
public protocol SourceElement {
1+
public protocol SearchElement {
22
var searchRepresentation: [Character] { get }
33
}
44

5-
public struct Match<Collection>: CustomDebugStringConvertible where Collection: RandomAccessCollection, Collection.Element: SourceElement {
5+
public struct Match<Collection>: CustomDebugStringConvertible where Collection: RandomAccessCollection, Collection.Element: SearchElement {
66
public typealias Element = Collection.Element
77
public typealias Index = Collection.Index
8-
public init(value: Element, index: Index, matchedIndices: [Index], score: Int) {
8+
public init(value: Element, index: Index, matchedIndices: [Int], score: Int) {
99
self.value = value
1010
self.index = index
1111
self.matchedIndices = matchedIndices
1212
self.score = score
1313
}
1414
public var value: Element
1515
public var index: Index
16-
public var matchedIndices: [Index]
16+
public var matchedIndices: [Int]
1717
public var score: Int
1818

1919
public var debugDescription: String {
@@ -35,14 +35,14 @@ fileprivate enum Penalty {
3535
static let maxUnmatchedLeadingChar = -15
3636
}
3737

38-
extension String: SourceElement {
38+
extension String: SearchElement {
3939
@inlinable
4040
public var searchRepresentation: [Character] {
4141
Array(self)
4242
}
4343
}
4444

45-
extension Substring: SourceElement {
45+
extension Substring: SearchElement {
4646
@inlinable
4747
public var searchRepresentation: [Character] {
4848
Array(self)
@@ -54,8 +54,7 @@ public func find<Collection>(
5454
in data: Collection
5555
) -> [Match<Collection>]
5656
where Collection: RandomAccessCollection,
57-
Collection.Element: SourceElement,
58-
Collection.Index == Int
57+
Collection.Element: SearchElement
5958
{
6059
var matches = findNoSort(pattern: pattern, in: data)
6160
matches.sort { $0.score >= $1.score }
@@ -67,83 +66,98 @@ public func findNoSort<Collection>(
6766
in data: Collection
6867
) -> [Match<Collection>]
6968
where Collection: RandomAccessCollection,
70-
Collection.Element: SourceElement,
71-
Collection.Index == Int
69+
Collection.Element: SearchElement
7270
{
7371
guard !pattern.isEmpty else { return [] }
7472

7573
let patternRunes: [Character] = Array(pattern)
74+
7675
var matches = [Match<Collection>]()
77-
7876
for i in data.indices {
79-
let dataRow = data[i]
80-
var matchedIndices: [Int] = []
81-
var totalScore = 0
82-
var patternIndex = patternRunes.startIndex
83-
var bestScore = -1
84-
var matchedIndex: Int?
85-
var currentAdjacentMatchBonus = 0
86-
87-
let candidateRunes = dataRow.searchRepresentation
88-
var lastIndex = candidateRunes.startIndex
89-
var lastRune = Character("\0")
90-
91-
for candidateIndex in candidateRunes.indices {
92-
let candidateRune = candidateRunes[candidateIndex]
93-
if equalFold(candidateRune, patternRunes[patternIndex]) {
94-
var score = 0
95-
if candidateIndex == candidateRunes.startIndex {
96-
score += Bounus.firstCharMatch
97-
}
98-
if lastRune.isLowercase && candidateRune.isUppercase {
99-
score += Bounus.camelCaseMatch
100-
}
101-
if candidateIndex != candidateRunes.startIndex && isSeparator(lastRune) {
102-
score += Bounus.matchFollowingSeparator
103-
}
104-
if let lastMatch = matchedIndices.last {
105-
let bonus = adjacentCharBonus(
106-
index: lastIndex,
107-
lastMatch: lastMatch,
108-
currentBonus: currentAdjacentMatchBonus
109-
)
110-
score += bonus
111-
currentAdjacentMatchBonus += bonus
112-
}
113-
if score > bestScore {
114-
bestScore = score
115-
matchedIndex = candidateIndex
116-
}
77+
if let match = matchTest(
78+
patternRunes: patternRunes,
79+
candidateRunes: data[i].searchRepresentation
80+
) {
81+
matches.append(.init(
82+
value: data[i],
83+
index: i,
84+
matchedIndices: match.matchedIndices,
85+
score: match.score
86+
))
87+
}
88+
}
89+
90+
return matches
91+
}
92+
93+
fileprivate func matchTest(
94+
patternRunes: [Character],
95+
candidateRunes: [Character]
96+
) -> (matchedIndices: [Int], score: Int)? {
97+
var matchedIndices: [Int] = []
98+
var totalScore = 0
99+
var patternIndex = patternRunes.startIndex
100+
var bestScore = -1
101+
var matchedIndex: Int?
102+
var currentAdjacentMatchBonus = 0
103+
104+
var lastIndex = candidateRunes.startIndex
105+
var lastRune = Character("\0")
106+
107+
for candidateIndex in candidateRunes.indices {
108+
let candidateRune = candidateRunes[candidateIndex]
109+
if equalFold(candidateRune, patternRunes[patternIndex]) {
110+
var score = 0
111+
if candidateIndex == candidateRunes.startIndex {
112+
score += Bounus.firstCharMatch
117113
}
114+
if lastRune.isLowercase && candidateRune.isUppercase {
115+
score += Bounus.camelCaseMatch
116+
}
117+
if candidateIndex != candidateRunes.startIndex && isSeparator(lastRune) {
118+
score += Bounus.matchFollowingSeparator
119+
}
120+
if let lastMatch = matchedIndices.last {
121+
let bonus = adjacentCharBonus(
122+
index: lastIndex,
123+
lastMatch: lastMatch,
124+
currentBonus: currentAdjacentMatchBonus
125+
)
126+
score += bonus
127+
currentAdjacentMatchBonus += bonus
128+
}
129+
if score > bestScore {
130+
bestScore = score
131+
matchedIndex = candidateIndex
132+
}
133+
}
118134

119-
if let matchedIndex {
120-
let nextPattern = patternRunes[safe: patternIndex + 1]
121-
let nextCandidate = candidateRunes[safe: candidateIndex + 1]
122-
if equalFold(nextPattern, nextCandidate) || nextCandidate == nil {
123-
if matchedIndices.isEmpty {
124-
let penalty = matchedIndex * Penalty.unmatchedLeadingChar
125-
bestScore += max(penalty, Penalty.maxUnmatchedLeadingChar)
126-
}
127-
totalScore += bestScore
128-
matchedIndices.append(matchedIndex)
129-
bestScore = -1
130-
patternIndex += 1
135+
if let matchedIndex {
136+
let nextPattern = patternRunes[safe: patternIndex + 1]
137+
let nextCandidate = candidateRunes[safe: candidateIndex + 1]
138+
if equalFold(nextPattern, nextCandidate) || nextCandidate == nil {
139+
if matchedIndices.isEmpty {
140+
let penalty = matchedIndex * Penalty.unmatchedLeadingChar
141+
bestScore += max(penalty, Penalty.maxUnmatchedLeadingChar)
131142
}
143+
totalScore += bestScore
144+
matchedIndices.append(matchedIndex)
145+
bestScore = -1
146+
patternIndex += 1
132147
}
133-
134-
lastIndex = candidateIndex
135-
lastRune = candidateRune
136148
}
137149

138-
let unmatchedCharactersPenalty = matchedIndices.count - candidateRunes.count
139-
totalScore += unmatchedCharactersPenalty
140-
141-
if matchedIndices.count == patternRunes.count {
142-
matches.append(.init(value: dataRow, index: i, matchedIndices: matchedIndices, score: totalScore))
143-
}
150+
lastIndex = candidateIndex
151+
lastRune = candidateRune
144152
}
145153

146-
return matches
154+
let unmatchedCharactersPenalty = matchedIndices.count - candidateRunes.count
155+
totalScore += unmatchedCharactersPenalty
156+
157+
if matchedIndices.count == patternRunes.count {
158+
return (matchedIndices: matchedIndices, score: totalScore)
159+
}
160+
return nil
147161
}
148162

149163
fileprivate func equalFold(_ lhs: Character, _ rhs: Character) -> Bool {

0 commit comments

Comments
 (0)