Passed
Push — main ( 8d1023...74092d )
by Chema
53s
created

FuzzyMatcher::findSimilar()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 15
c 1
b 0
f 1
dl 0
loc 29
ccs 20
cts 20
cp 1
rs 9.7666
cc 2
nc 2
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Container;
6
7
use function array_filter;
8
use function array_map;
9
use function array_slice;
10
use function count;
11
use function levenshtein;
12
use function max;
13
use function min;
14
use function strlen;
15
use function usort;
16
17
/**
18
 * Provides fuzzy matching for service names to suggest alternatives.
19
 */
20
final class FuzzyMatcher
21
{
22
    private const MAX_SUGGESTIONS = 3;
23
    private const SIMILARITY_THRESHOLD = 0.6;
24
25
    /**
26
     * Find similar strings from a list of candidates.
27
     *
28
     * @param list<string> $candidates
29
     *
30
     * @return list<string>
31
     */
32 11
    public static function findSimilar(string $target, array $candidates): array
33
    {
34 11
        if (count($candidates) === 0) {
35 3
            return [];
36
        }
37
38 8
        $scores = array_map(
39 8
            static fn (string $candidate): array => [
40 8
                'name' => $candidate,
41 8
                'score' => self::calculateSimilarity($target, $candidate),
42 8
            ],
43 8
            $candidates,
44 8
        );
45
46
        // Sort by score descending
47 8
        usort($scores, static fn (array $a, array $b): int => $b['score'] <=> $a['score']);
48
49
        // Filter by threshold and limit results
50 8
        $filtered = array_filter(
51 8
            $scores,
52 8
            static fn (array $item): bool => $item['score'] >= self::SIMILARITY_THRESHOLD,
53 8
        );
54
55 8
        $suggestions = array_map(
56 8
            static fn (array $item): string => $item['name'],
57 8
            $filtered,
58 8
        );
59
60 8
        return array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
61
    }
62
63
    /**
64
     * Calculate similarity between two strings (0.0 to 1.0).
65
     */
66 8
    private static function calculateSimilarity(string $a, string $b): float
67
    {
68 8
        $maxLength = max(strlen($a), strlen($b));
69 8
        if ($maxLength === 0) {
70
            return 1.0;
71
        }
72
73 8
        $distance = levenshtein($a, $b);
74 8
        if ($distance === -1) {
75
            // Strings too long for levenshtein, use simple approach
76
            return self::simpleSimilarity($a, $b);
77
        }
78
79 8
        return 1.0 - ($distance / $maxLength);
80
    }
81
82
    /**
83
     * Fallback similarity calculation for very long strings.
84
     */
85
    private static function simpleSimilarity(string $a, string $b): float
86
    {
87
        $maxLength = max(strlen($a), strlen($b));
88
        $minLength = min(strlen($a), strlen($b));
89
90
        if ($maxLength === 0) {
91
            return 1.0;
92
        }
93
94
        // Simple length-based similarity
95
        return $minLength / $maxLength;
96
    }
97
}
98