← All 41 libraries

CandyFuzzy

CandyFuzzy

Fuzzy string matching with scored matched indices

foundation-package fuzzy filter

CandyFuzzy code coverage

Fuzzy string matching that returns which characters matched and their score. Two algorithms: Smith-Waterman (local alignment) and Sahilm (gum-style scoring with separator/camel-case bonuses). The matched indices enable filter-highlighting UIs that were impossible with score-only matchers. Extracted from candy-forms; back-compat shims left in candy-forms and candy-lister for migration.

Install

composer require sugarcraft/candy-fuzzy:@dev

Quickstart

use SugarCraft\Fuzzy\FuzzyMatcher;
use SugarCraft\Fuzzy\Matcher\SmithWatermanMatcher;
use SugarCraft\Fuzzy\MatchResult;
use SugarCraft\Fuzzy\Highlighter;

$matcher = new SmithWatermanMatcher();

// Single match — returns null when no match
$result = $matcher->match('foo', 'foobar');
// $result->score     === 19   (3 × matchScore + adjacency bonuses)
// $result->indices() === [0, 1, 2]  (0-based character positions)
// $result->haystack  === 'foobar'

// Match all candidates, sorted by score desc then candidate asc
$results = $matcher->matchAll('ab', ['apple', 'banana', 'cabbage']);
// cabbage (11, the adjacent "ab") before apple (3) and banana (3)

// Opt in to full-query mode for type-to-filter pickers: every query
// character must land, in order
$picker = SmithWatermanMatcher::new()->withRequireFullQuery();
$picker->match('bxz', 'abcdef');               // null — "x" and "z" never land
$picker->match('gst', 'git status')->indices(); // [0, 4, 5]

// Use Highlighter to mark matched runs with ANSI codes
$highlighter = new Highlighter();
echo $highlighter->highlight($result, fn($s) => "\e[1;31m{$s}\e[0m");
// Output: 'foobar' with positions [0,1,2] highlighted in bold red

FuzzyMatcher interface

match(string $query, string $candidate): ?MatchResultReturns null when no match, MatchResult with score + indices otherwise.
matchAll(string $query, iterable $candidates, ?int $limit = null, int $minScore = 1): list<MatchResult>Returns all matches sorted by score desc, then candidate asc as tiebreak; matchAllGenerator() streams the same results lazily.

MatchResult

$score: intReadonly match quality score — higher is better; isMatched() is score > 0.
indices(): list<int>0-based code-point offsets into the original haystack (UTF-8 safe — indices are at character boundaries, not bytes).
$needle / $haystack: stringReadonly query and candidate the result was computed for.

Algorithms

SmithWatermanMatcherLocal sequence alignment — optimal for finding best subsequence match within a candidate. Weights come from an immutable ScoringProfile; ScoringProfile::canonical() (the default — default() is a deprecated alias) is bit-equivalent to the original candy-forms FuzzyMatcher, with strict() / lenient() presets. withRequireFullQuery() keeps only candidates that contain the whole query as an in-order subsequence, scoring the best alignment forced to cover every query character (floored at 1). Over-cap input is truncated, never handed to another algorithm: only the first maxQueryLength (128) query and maxCandidateLength (1000) candidate characters take part.
SahilmMatcherPorts the sahilm/fuzzy Go algorithm — separator bonus (/, _, -, space, .), camel-case bonus, exact-prefix bonus. Good for file/path matching. Weights are injectable via the immutable SahilmScoring; SahilmScoring::canonical() is the historical sahilm/fuzzy constant set.

Highlighter

Takes a MatchResult and a styler \Closure(string): string, returns the candidate string with matched runs passed through the styler. Non-matched runs are returned unchanged.

Use it for

Source & demos

Try the quickstart →

API

ClassMethodDescription
FuzzyMatchermatch(string $query, string $candidate): ?MatchResultSingle match or null
FuzzyMatchermatchAll(string $query, iterable $candidates, ?int $limit = null, int $minScore = 1): list<MatchResult>All matches sorted by score desc
FuzzyMatchermatchAllGenerator(string $query, iterable $candidates, ?int $limit = null, int $minScore = 1): GeneratorLazy variant of matchAll()
MatchResult$score / $needle / $haystack (readonly)Match quality score (higher = better), the query and the candidate
MatchResultindices(): list<int>0-based matched code-point offsets into the original haystack
MatchResultisMatched(): bool / isEmpty(): boolscore > 0 / no matched indices
SmithWatermanMatchernew(?ScoringProfile $profile = null, int $maxQueryLength = 128, int $maxCandidateLength = 1000, bool $requireFullQuery = false)Local sequence alignment; over-cap input is truncated
SmithWatermanMatcherwithRequireFullQuery(bool $require = true): self / requiresFullQuery(): boolFull-query mode: every query character must land, in order
SmithWatermanMatcherprofile() / maxQueryLength() / maxCandidateLength()Configured weights and length caps
ScoringProfilecanonical() / strict() / lenient()Smith-Waterman weight presets; default() is a deprecated alias of canonical()
ScoringProfilewithMatchScore() / withMismatchPenalty() / withGapOpen() / withGapExtend() / withAdjacentBonus()Immutable weight setters
SahilmMatchernew(bool $caseSensitive = false, ?SahilmScoring $scoring = null)Separator/camelCase bonus algorithm; scoring() / caseSensitive() accessors
SahilmScoringcanonical() / withMatchScore() / withConsecutiveBonus() / withSeparatorBonus() / withCamelBonus() / withFirstCharBonus() / withLowerCaseBonus()Immutable Sahilm weights (canonical 1 / 5 / 10 / 10 / 15 / 1)
FuzzyMatcherFactorynamed(string $type, ScoringProfile|SahilmScoring|null $scoring = null): FuzzyMatcher'smith-waterman' or 'sahilm'; a mismatched weight type or unknown name throws. create(string $type, $profile = null) is a deprecated alias that forwards to named() and keeps its $profile parameter name for named-argument callers
Highlighterhighlight(MatchResult, \Closure(string): string): stringApply styler to matched runs