matching.go
1875 bytes
1package main
2
3import (
4 "strings"
5 "unicode"
6
7 "golang.org/x/text/unicode/norm"
8)
9
10const (
11 matchThreshold = 0.8
12 lowerThreshold = 0.3
13
14 pointsTitle = 4
15 pointsArtist = 2
16 pointsAlbum = 4
17 pointsPerRound = pointsTitle + pointsArtist + pointsAlbum
18)
19
20func fuzzyScore(guess, answer string) float64 {
21 a := similarity(normalize(guess), normalize(answer))
22 b := similarity(normalize(withoutParentheses(guess)), normalize(withoutParentheses(answer)))
23 return max(a, b)
24}
25
26func scoreFor(similarity float64, weight int) int {
27 if similarity >= matchThreshold {
28 return weight
29 }
30 if similarity < lowerThreshold {
31 return 0
32 }
33 return int(float64(weight) * similarity + 0.5)
34}
35
36func withoutParentheses(s string) string {
37 var b strings.Builder
38 depth := 0
39 for _, r := range s {
40 switch r {
41 case '(':
42 depth++
43 b.WriteRune(' ')
44 case ')':
45 if depth > 0 {
46 depth--
47 }
48 b.WriteRune(' ')
49 default:
50 if depth == 0 {
51 b.WriteRune(r)
52 }
53 }
54 }
55 return b.String()
56}
57
58var ligatures = map[rune]string{
59 'Æ': "ae", 'æ': "ae",
60 'Œ': "oe", 'œ': "oe",
61}
62
63func normalize(s string) string {
64 s = strings.ReplaceAll(s, "&", "and")
65 s = norm.NFKD.String(strings.ToLower(s))
66 var b strings.Builder
67 for _, r := range s {
68 if l, ok := ligatures[r]; ok {
69 b.WriteString(l)
70 continue
71 }
72 if unicode.IsLetter(r) || unicode.IsNumber(r) {
73 b.WriteRune(r)
74 }
75 }
76 return b.String()
77}
78
79func similarity(a, b string) float64 {
80 x, y := []rune(a), []rune(b)
81 if len(x) == 0 || len(y) == 0 {
82 return 0
83 }
84 row := make([]int, len(y)+1)
85 for j := range row {
86 row[j] = j
87 }
88 for i, r := range x {
89 previous := row[0]
90 row[0] = i + 1
91 for j, s := range y {
92 cost := 0
93 if r != s {
94 cost = 1
95 }
96 old := row[j+1]
97 row[j+1] = min(row[j]+1, old+1, previous+cost)
98 previous = old
99 }
100 }
101 return 1 - float64(row[len(y)])/float64(max(len(x), len(y)))
102}