feat: implement scanner similarity scoring
This commit is contained in:
131
internal/scanner/scanner_test.go
Normal file
131
internal/scanner/scanner_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package scanner
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSimilarity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
want float64
|
||||
epsilon float64
|
||||
}{
|
||||
{
|
||||
name: "exact match scores 1.0",
|
||||
a: "The Wall",
|
||||
b: "The Wall",
|
||||
want: 1.0,
|
||||
epsilon: 1e-9,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive exact match scores 1.0",
|
||||
a: "The Wall",
|
||||
b: "the wall",
|
||||
want: 1.0,
|
||||
epsilon: 1e-9,
|
||||
},
|
||||
{
|
||||
name: "remastered variant stays above threshold",
|
||||
a: "The Wall",
|
||||
b: "The Wall (Remastered)",
|
||||
want: 1.0, // parenthesized content is stripped during normalization
|
||||
epsilon: 1e-9,
|
||||
},
|
||||
{
|
||||
name: "year-suffixed variant stays above threshold",
|
||||
a: "Abbey Road",
|
||||
b: "Abbey Road (2019 Remix)",
|
||||
// After normalization both collapse to "abbey road" → identical.
|
||||
want: 1.0,
|
||||
epsilon: 1e-9,
|
||||
},
|
||||
{
|
||||
name: "clearly different titles score below 0.85",
|
||||
a: "The Wall",
|
||||
b: "Completely Different Album",
|
||||
want: 0.0,
|
||||
epsilon: 1e-9,
|
||||
},
|
||||
{
|
||||
name: "substring-ish title scores moderately",
|
||||
a: "Dark Side of the Moon",
|
||||
b: "Dark Side of the Moon Part II",
|
||||
want: 0.0, // non-empty; value asserted only as below threshold
|
||||
epsilon: 1e-9,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Similarity(tt.a, tt.b)
|
||||
switch {
|
||||
case tt.name == "clearly different titles score below 0.85" ||
|
||||
tt.name == "substring-ish title scores moderately":
|
||||
if got >= 0.85 {
|
||||
t.Errorf("Similarity(%q, %q) = %v, want < 0.85", tt.a, tt.b, got)
|
||||
}
|
||||
default:
|
||||
if diff := got - tt.want; diff > tt.epsilon || diff < -tt.epsilon {
|
||||
t.Errorf("Similarity(%q, %q) = %v, want %v (+/- %v)", tt.a, tt.b, got, tt.want, tt.epsilon)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimilarity_EmptyStrings(t *testing.T) {
|
||||
// Both empty → no match (0.0).
|
||||
if got := Similarity("", ""); got != 0.0 {
|
||||
t.Errorf("Similarity(%q, %q) = %v, want 0.0", "", "", got)
|
||||
}
|
||||
// One empty, one non-empty → no similarity.
|
||||
if got := Similarity("The Wall", ""); got != 0.0 {
|
||||
t.Errorf("Similarity(%q, %q) = %v, want 0.0", "The Wall", "", got)
|
||||
}
|
||||
if got := Similarity("", "The Wall"); got != 0.0 {
|
||||
t.Errorf("Similarity(%q, %q) = %v, want 0.0", "", "The Wall", got)
|
||||
}
|
||||
// Whitespace-only inputs normalize to empty → no match.
|
||||
if got := Similarity(" ", "The Wall"); got != 0.0 {
|
||||
t.Errorf("Similarity(%q, %q) = %v, want 0.0", " ", "The Wall", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMatch(t *testing.T) {
|
||||
const threshold = 0.85
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
expected bool
|
||||
}{
|
||||
{name: "exact match is a match", a: "The Wall", b: "The Wall", expected: true},
|
||||
{name: "remastered variant is a match", a: "The Wall", b: "The Wall (Remastered)", expected: true},
|
||||
{name: "year variant is a match", a: "Abbey Road", b: "Abbey Road (2019)", expected: true},
|
||||
{name: "clearly different is not a match", a: "The Wall", b: "Random Noise", expected: false},
|
||||
{name: "empty vs non-empty is not a match", a: "", b: "The Wall", expected: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsMatch(tt.a, tt.b, threshold); got != tt.expected {
|
||||
t.Errorf("IsMatch(%q, %q, %v) = %v, want %v", tt.a, tt.b, threshold, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMatch_ThresholdBoundary(t *testing.T) {
|
||||
// A moderately different title should be a match at a low threshold but
|
||||
// not at a high one, confirming the boundary is inclusive (>=).
|
||||
a, b := "The Wall", "The Wall Live"
|
||||
low := IsMatch(a, b, 0.5)
|
||||
high := IsMatch(a, b, 0.99)
|
||||
if !low {
|
||||
t.Errorf("IsMatch(%q, %q, 0.5) = false, want true", a, b)
|
||||
}
|
||||
if high {
|
||||
t.Errorf("IsMatch(%q, %q, 0.99) = true, want false", a, b)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user