Skip to content

Commit 3471da9

Browse files
authored
perf: use cimap to case insensitively lookup using a map (#630)
convert many O(n) lookups (with allocations) into O(1) lookups (with 0 allocations)
1 parent d2df2d6 commit 3471da9

7 files changed

Lines changed: 79 additions & 57 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ require (
4343
github.com/modern-go/reflect2 v1.0.2 // indirect
4444
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
4545
github.com/pmezard/go-difflib v1.0.0 // indirect
46+
github.com/projectbarks/cimap v0.1.1 // indirect
4647
github.com/quic-go/qpack v0.6.0 // indirect
4748
github.com/quic-go/quic-go v0.59.0 // indirect
4849
github.com/tibiadata/tibiadata-api-go/src/tibiamapping v0.0.0-20250818132205-2b0f4da1df36 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
6262
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
6363
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6464
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
65+
github.com/projectbarks/cimap v0.1.1 h1:F9C2UvcjrbRifzcABVZ1tPMSuWq9iZV4NSPMfEmAeQg=
66+
github.com/projectbarks/cimap v0.1.1/go.mod h1:CuebbhEuH1t2Iktn7QYFCstI6Kb3BAM1cQ/ozp393QE=
6567
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
6668
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
6769
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=

src/validation/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/davecgh/go-spew v1.1.1 // indirect
1414
github.com/go-resty/resty/v2 v2.17.2 // indirect
1515
github.com/pmezard/go-difflib v1.0.0 // indirect
16+
github.com/projectbarks/cimap v0.1.1 // indirect
1617
golang.org/x/net v0.43.0 // indirect
1718
gopkg.in/yaml.v3 v3.0.1 // indirect
1819
)

src/validation/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAy
44
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
55
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
66
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/projectbarks/cimap v0.1.1 h1:F9C2UvcjrbRifzcABVZ1tPMSuWq9iZV4NSPMfEmAeQg=
8+
github.com/projectbarks/cimap v0.1.1/go.mod h1:CuebbhEuH1t2Iktn7QYFCstI6Kb3BAM1cQ/ozp393QE=
79
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
810
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
911
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=

src/validation/highscore.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,26 @@ package validation
33
import (
44
"errors"
55
"strings"
6+
7+
"github.com/projectbarks/cimap"
68
)
79

810
var (
9-
// validHighscoreCatregories stores all valid highscore categories
10-
validHighscoreCategories = []string{"achievements", "achievement", "axe", "axefighting", "charm", "charms", "charmpoints", "charmspoints", "club", "clubfighting", "distance", "distancefighting", "fishing", "fist", "fistfighting", "goshnar", "goshnars", "goshnarstaint", "loyalty", "loyaltypoints", "magic", "mlvl", "magiclevel", "shielding", "shield", "sword", "swordfighting", "drome", "dromescore", "experience", "boss", "bosses", "bosspoints", "bountypoints", "bountypoint", "bountypointsearned", "weeklytasks", "weeklytask", "weeklytaskscompleted"}
11+
// validHighscoreCategories stores all valid highscore categories
12+
validHighscoreCategories = func() *cimap.CaseInsensitiveMap[bool] {
13+
m := cimap.New[bool](38)
14+
for _, v := range []string{"achievements", "achievement", "axe", "axefighting", "charm", "charms", "charmpoints", "charmspoints", "club", "clubfighting", "distance", "distancefighting", "fishing", "fist", "fistfighting", "goshnar", "goshnars", "goshnarstaint", "loyalty", "loyaltypoints", "magic", "mlvl", "magiclevel", "shielding", "shield", "sword", "swordfighting", "drome", "dromescore", "experience", "boss", "bosses", "bosspoints", "bountypoints", "bountypoint", "bountypointsearned", "weeklytasks", "weeklytask", "weeklytaskscompleted"} {
15+
m.Add(v, true)
16+
}
17+
return m
18+
}()
1119
)
1220

1321
// IsHighscoreCategoryValid reports wheter the provided string represents a valid highscore category
1422
// Check if error == nil to see whether the highscore category is valid or not
1523
func IsHighscoreCategoryValid(hs string) error {
16-
for _, highscore := range validHighscoreCategories {
17-
if strings.EqualFold(hs, highscore) {
18-
return nil
19-
}
24+
if _, ok := validHighscoreCategories.Get(hs); ok {
25+
return nil
2026
}
2127

2228
return ErrorHighscoreCategoryDoesNotExist

src/validation/tibia.go

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
"unicode"
77
"unicode/utf8"
8+
9+
"github.com/projectbarks/cimap"
810
)
911

1012
const (
@@ -24,7 +26,13 @@ var (
2426
guildNameRegex = regexp.MustCompile(`[^\sa-zA-Z]`)
2527

2628
// validVocations stores all valid tibia vocations
27-
validVocations = []string{"none", "knight", "knights", "paladin", "paladins", "sorcerer", "sorcerers", "druid", "druids", "monk", "monks", "all"}
29+
validVocations = func() *cimap.CaseInsensitiveMap[bool] {
30+
m := cimap.New[bool](12)
31+
for _, v := range []string{"none", "knight", "knights", "paladin", "paladins", "sorcerer", "sorcerers", "druid", "druids", "monk", "monks", "all"} {
32+
m.Add(v, true)
33+
}
34+
return m
35+
}()
2836
)
2937

3038
// IsRestrictionMode reports whether the restriction mode is enabled
@@ -51,10 +59,8 @@ func IsNewsIDValid(ID int) error {
5159
// IsVocationValid reports wheter the provided string represents a valid vocation
5260
// Check if error == nil to see whether the vocation is valid or not
5361
func IsVocationValid(vocation string) error {
54-
for _, voc := range validVocations {
55-
if strings.EqualFold(vocation, voc) {
56-
return nil
57-
}
62+
if _, ok := validVocations.Get(vocation); ok {
63+
return nil
5864
}
5965

6066
return ErrorVocationDoesNotExist
@@ -196,25 +202,12 @@ func IsCreatureNameValid(name string) (string, error) {
196202
return "", ErrorCreatureNameInvalid
197203
}
198204

199-
var (
200-
found bool
201-
endpoint string
202-
)
203-
204205
// Check if creature exists
205-
for _, creature := range val.Creatures {
206-
if strings.EqualFold(name, creature.Endpoint) || strings.EqualFold(name, creature.Name) || strings.EqualFold(name, creature.PluralName) {
207-
found = true
208-
endpoint = creature.Endpoint
209-
break
210-
}
206+
if endpoint, ok := creatureLookup.Get(name); ok {
207+
return endpoint, nil
211208
}
212209

213-
if !found {
214-
return "", ErrorCreatureNotFound
215-
}
216-
217-
return endpoint, nil
210+
return "", ErrorCreatureNotFound
218211
}
219212

220213
// IsSpellNameOrFormulaValid reports wheter the provided string represents a valid spell name or formula
@@ -263,25 +256,12 @@ func IsSpellNameOrFormulaValid(name string) (string, error) {
263256
return "", ErrorSpellNameInvalid
264257
}
265258

266-
var (
267-
found bool
268-
endpoint string
269-
)
270-
271259
// Check if spell exists
272-
for _, spell := range val.Spells {
273-
if strings.EqualFold(name, spell.Endpoint) || strings.EqualFold(name, spell.Name) || strings.EqualFold(name, spell.Formula) {
274-
found = true
275-
endpoint = spell.Endpoint
276-
break
277-
}
260+
if endpoint, ok := spellLookup.Get(name); ok {
261+
return endpoint, nil
278262
}
279263

280-
if !found {
281-
return "", ErrorSpellNotFound
282-
}
283-
284-
return endpoint, nil
264+
return "", ErrorSpellNotFound
285265
}
286266

287267
// GetWorlds returns a list of all existing worlds
@@ -303,13 +283,8 @@ func WorldExists(world string) (bool, error) {
303283
}
304284

305285
// Try to find the world
306-
for _, w := range val.Worlds {
307-
if strings.EqualFold(w, world) {
308-
return true, nil
309-
}
310-
}
311-
312-
return false, nil
286+
_, found := worldLookup.Get(world)
287+
return found, nil
313288
}
314289

315290
// GetTowns returns a list of all existing towns
@@ -334,13 +309,8 @@ func TownExists(town string) (bool, error) {
334309
town = strings.ReplaceAll(town, "+", " ")
335310

336311
// Try to find the town
337-
for _, t := range val.Towns {
338-
if strings.EqualFold(t, town) {
339-
return true, nil
340-
}
341-
}
342-
343-
return false, nil
312+
_, found := townLookup.Get(town)
313+
return found, nil
344314
}
345315

346316
// GetHouses returns a slice of all houses

src/validation/validation.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"sync"
88
"unicode/utf8"
99

10+
"github.com/projectbarks/cimap"
1011
"github.com/tibiadata/tibiadata-api-go/src/tibiamapping"
1112
)
1213

@@ -45,6 +46,12 @@ var (
4546
sha256sum string // sha256sum stores the sha256sum of the data.min.json file
4647
sha512sum string // sha512sum stores the sha512sum of the data.min.json file
4748

49+
// Lookup maps for O(1) case-insensitive searches
50+
creatureLookup *cimap.CaseInsensitiveMap[string] // endpoint/name/plural_name -> endpoint
51+
spellLookup *cimap.CaseInsensitiveMap[string] // endpoint/name/formula -> endpoint
52+
worldLookup *cimap.CaseInsensitiveMap[bool] // world name -> true
53+
townLookup *cimap.CaseInsensitiveMap[bool] // town name -> true
54+
4855
smallestCreatureName, biggestCreatureName, smallestCreatureWord, biggestCreatureWord string // smallest and biggest creature names and words
4956
smallestCreatureNameRuneCount, biggestCreatureNameRuneCount, smallestCreatureWordRuneCount, biggestCreatureWordRuneCount int // smallest and biggest creature names and words rune count
5057
smallestSpellNameOrFormula, biggestSpellNameOrFormula, smallestSpellWord, biggestSpellWord string // smalles and biggest spell names or formulas and words
@@ -107,6 +114,39 @@ func Initiate(TibiaDataUserAgent string) error {
107114
func setVars() {
108115
setCreaturesVars()
109116
setSpellsVars()
117+
buildLookupMaps()
118+
}
119+
120+
// buildLookupMaps builds O(1) lookup maps from the slices.
121+
// Called once during Initiate().
122+
func buildLookupMaps() {
123+
creatureLookup = cimap.New[string](len(val.Creatures) * 3)
124+
for _, c := range val.Creatures {
125+
creatureLookup.Add(c.Endpoint, c.Endpoint)
126+
creatureLookup.Add(c.Name, c.Endpoint)
127+
if c.PluralName != "" {
128+
creatureLookup.Add(c.PluralName, c.Endpoint)
129+
}
130+
}
131+
132+
spellLookup = cimap.New[string](len(val.Spells) * 3)
133+
for _, s := range val.Spells {
134+
spellLookup.Add(s.Endpoint, s.Endpoint)
135+
spellLookup.Add(s.Name, s.Endpoint)
136+
if s.Formula != "" {
137+
spellLookup.Add(s.Formula, s.Endpoint)
138+
}
139+
}
140+
141+
worldLookup = cimap.New[bool](len(val.Worlds))
142+
for _, w := range val.Worlds {
143+
worldLookup.Add(w, true)
144+
}
145+
146+
townLookup = cimap.New[bool](len(val.Towns))
147+
for _, t := range val.Towns {
148+
townLookup.Add(t, true)
149+
}
110150
}
111151

112152
// setCreaturesVars sets creatures vars

0 commit comments

Comments
 (0)