Skip to content

Commit 8f037f6

Browse files
committed
facts db
1 parent e4d46fd commit 8f037f6

4 files changed

Lines changed: 1267 additions & 0 deletions

File tree

internal/factdb/factdb.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Package factdb provides a "Did you know?" fact database of obscure,
2+
// genuinely interesting facts about countries, cities, and regions.
3+
//
4+
// Facts are stored at two levels:
5+
// - Country-level: unusual records, historical quirks, counterintuitive
6+
// geography, cultural inversions, little-known firsts.
7+
// - City/region-level: facts specific to a city or sub-national region.
8+
//
9+
// The seed database is embedded in the binary (seed_facts.json), so facts
10+
// are always available offline with zero network activity. City-level facts
11+
// are tried first; if none exist, a country-level fact is returned.
12+
package factdb
13+
14+
import (
15+
_ "embed"
16+
"encoding/json"
17+
"fmt"
18+
"math/rand"
19+
"sync"
20+
"time"
21+
)
22+
23+
//go:embed seed_facts.json
24+
var seedJSON []byte
25+
26+
// seedData is the raw JSON shape of seed_facts.json.
27+
type seedData struct {
28+
Countries map[string][]string `json:"countries"`
29+
Cities map[string][]string `json:"cities"`
30+
Regions map[string][]string `json:"regions"`
31+
}
32+
33+
// Fact is a single "Did you know?" entry.
34+
type Fact struct {
35+
// Text is the fact sentence.
36+
Text string `json:"text"`
37+
// Level is "country", "city", or "region".
38+
Level string `json:"level"`
39+
// Place is the name of the country/city/region this fact is about.
40+
Place string `json:"place"`
41+
}
42+
43+
// IsZero returns true when the Fact has no content.
44+
func (f Fact) IsZero() bool { return f.Text == "" }
45+
46+
// DB is a fact database that returns random obscure facts for countries,
47+
// cities, and regions. All data is loaded from the embedded seed JSON at
48+
// construction time and is safe for concurrent use.
49+
type DB struct {
50+
mu sync.RWMutex
51+
data seedData
52+
rng *rand.Rand
53+
}
54+
55+
// New creates a DB loaded from the embedded seed_facts.json.
56+
func New() (*DB, error) {
57+
var data seedData
58+
if err := json.Unmarshal(seedJSON, &data); err != nil {
59+
return nil, fmt.Errorf("factdb: failed to parse seed data: %w", err)
60+
}
61+
if data.Countries == nil {
62+
data.Countries = make(map[string][]string)
63+
}
64+
if data.Cities == nil {
65+
data.Cities = make(map[string][]string)
66+
}
67+
if data.Regions == nil {
68+
data.Regions = make(map[string][]string)
69+
}
70+
return &DB{
71+
data: data,
72+
rng: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint:gosec
73+
}, nil
74+
}
75+
76+
// GetFact returns a random interesting fact for the given country and,
77+
// optionally, city. The lookup order is: city → region → country.
78+
// Returns a zero Fact{} if no entry is found.
79+
func (db *DB) GetFact(country, city string) Fact {
80+
db.mu.RLock()
81+
defer db.mu.RUnlock()
82+
83+
if city != "" {
84+
if f, ok := db.pick(db.data.Cities, city); ok {
85+
return Fact{Text: f, Level: "city", Place: city}
86+
}
87+
}
88+
if country != "" {
89+
if f, ok := db.pick(db.data.Regions, country); ok {
90+
return Fact{Text: f, Level: "region", Place: country}
91+
}
92+
if f, ok := db.pick(db.data.Countries, country); ok {
93+
return Fact{Text: f, Level: "country", Place: country}
94+
}
95+
}
96+
return Fact{}
97+
}
98+
99+
// GetCountryFact is a convenience wrapper that ignores city.
100+
func (db *DB) GetCountryFact(country string) Fact {
101+
return db.GetFact(country, "")
102+
}
103+
104+
// CountryCount returns the number of countries with at least one fact.
105+
func (db *DB) CountryCount() int {
106+
db.mu.RLock()
107+
defer db.mu.RUnlock()
108+
return len(db.data.Countries)
109+
}
110+
111+
// pick selects a uniformly random entry from m[key], returning ("", false)
112+
// if the key does not exist or its slice is empty.
113+
func (db *DB) pick(m map[string][]string, key string) (string, bool) {
114+
facts, ok := m[key]
115+
if !ok || len(facts) == 0 {
116+
return "", false
117+
}
118+
return facts[db.rng.Intn(len(facts))], true
119+
}

0 commit comments

Comments
 (0)