|
| 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