Module: Clack::Core::FuzzyMatcher
- Defined in:
- lib/clack/core/fuzzy_matcher.rb
Overview
Simple scored fuzzy matcher for autocomplete prompts.
Matches query characters in order within the target string, scoring higher for consecutive matches and matches at word boundaries. Dependency-free alternative to Levenshtein distance that’s fast enough for interactive use.
Constant Summary collapse
- START_BONUS =
Bonus for match at the very start of the string
3- CONSECUTIVE_BONUS =
Bonus for each consecutive character matched
2- BOUNDARY_BONUS =
Bonus for match at a word boundary (after space, _, -)
2- BASE_SCORE =
Base score per matched character
1
Class Method Summary collapse
-
.filter(options, query) ⇒ Array<Hash>
Filter and sort option hashes by fuzzy relevance.
-
.match?(query, target) ⇒ Boolean
Check if query fuzzy-matches the target string.
-
.score(query, target, q_down: nil) ⇒ Integer
Score a fuzzy match.
Class Method Details
.filter(options, query) ⇒ Array<Hash>
Filter and sort option hashes by fuzzy relevance.
Matches against label, value (as string), and hint fields. Returns options sorted by best match score (descending).
91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/clack/core/fuzzy_matcher.rb', line 91 def filter(, query) return if query.empty? q_down = query.downcase scored = .filter_map do |opt| s = best_score(opt, query, q_down) [opt, s] if s.positive? end scored.sort_by { |_, s| -s }.map(&:first) end |
.match?(query, target) ⇒ Boolean
Check if query fuzzy-matches the target string.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/clack/core/fuzzy_matcher.rb', line 38 def match?(query, target) return true if query.empty? qi = 0 q_chars = query.downcase t_chars = target.downcase t_chars.each_char do |tc| qi += 1 if tc == q_chars[qi] return true if qi >= q_chars.length end false end |
.score(query, target, q_down: nil) ⇒ Integer
Score a fuzzy match. Higher is better. Returns 0 if no match.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/clack/core/fuzzy_matcher.rb', line 59 def score(query, target, q_down: nil) return 0 if query.empty? q_down ||= query.downcase t_down = target.downcase qi = 0 total = 0 prev_match_idx = -2 # -2 so first match at 0 isn't consecutive t_down.each_char.with_index do |tc, ti| next unless qi < q_down.length && tc == q_down[qi] total += BASE_SCORE total += START_BONUS if ti.zero? total += CONSECUTIVE_BONUS if ti == prev_match_idx + 1 total += BOUNDARY_BONUS if ti.positive? && boundary?(t_down[ti - 1]) prev_match_idx = ti qi += 1 end (qi >= q_down.length) ? total : 0 end |