package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"math/rand"
"os"
"sort"
)
// Titles are BUILT, not drawn from a word list. Two syllable tables and a
// handful of lines give more distinct titles than you will ever need, ship no
// data file, and — because the tables are small — produce titles that SHARE
// LONG PREFIXES. That last property is not decoration: it is what makes prefix
// search interesting later, and what stops a "search for a title" benchmark
// from being trivially easy.
var (
onsets = []string{"b", "d", "g", "k", "l", "m", "n", "p", "r", "s", "t", "v", "ž", "č", "š"}
nuclei = []string{"a", "e", "i", "o", "u", "ė", "ū", "ą"}
)
// syllables gives 15*8 = 120 syllables, so a 4-syllable word is one of
// 120^4 = 207,360,000 possibilities.
func word(rng *rand.Rand, syl int) string {
b := make([]byte, 0, syl*3)
for i := 0; i < syl; i++ {
b = append(b, onsets[rng.Intn(len(onsets))]...)
b = append(b, nuclei[rng.Intn(len(nuclei))]...)
}
// Capitalise the first rune (all onsets are single letters, so byte 0..1).
r := []rune(string(b))
up := []rune{'B', 'D', 'G', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'Ž', 'Č', 'Š'}
lo := []rune{'b', 'd', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'ž', 'č', 'š'}
for i := range lo {
if r[0] == lo[i] {
r[0] = up[i]
break
}
}
return string(r)
}
func genTitle(rng *rand.Rand) string {
t := word(rng, 3+rng.Intn(2))
if rng.Intn(3) == 0 {
t += " " + word(rng, 2)
}
return t
}
func cmdGen(args []string) error {
fs := flag.NewFlagSet("gen", flag.ContinueOnError)
n := fs.Int("n", 1000, "how many items to generate")
seed := fs.Int64("seed", 1, "random seed — same seed, same library")
out := fs.String("o", "library.jsonl", "output file")
sorted := fs.Bool("sorted", false, "emit sorted by Title (ascending)")
reverse := fs.Bool("reverse", false, "emit sorted by Title (descending)")
shuffled := fs.Bool("shuffled", false, "emit in random order (the default)")
dupRate := fs.Float64("dup-rate", 0, "fraction of items reusing an earlier Title (0..1)")
if err := fs.Parse(args); err != nil {
return err
}
if *n < 0 {
return fmt.Errorf("-n must be >= 0, got %d", *n)
}
if *sorted && *reverse {
return fmt.Errorf("-sorted and -reverse are mutually exclusive")
}
if *dupRate < 0 || *dupRate > 1 {
return fmt.Errorf("-dup-rate must be between 0 and 1, got %v", *dupRate)
}
_ = *shuffled // shuffled is the default; the flag exists to be explicit
rng := rand.New(rand.NewSource(*seed))
items := make([]Item, *n)
// With -dup-rate 0 the titles are GUARANTEED distinct: a collision is
// redrawn. Otherwise "no duplicates" would still leak accidental ones from
// the syllable space, and a later lesson keyed on Title would be debugging
// collisions it never asked for.
seen := make(map[string]bool, *n)
for i := range items {
title := genTitle(rng)
for seen[title] {
title = genTitle(rng)
}
// A duplicate reuses an EARLIER title, so duplicates are real collisions
// rather than a second random draw that happens to match.
if i > 0 && *dupRate > 0 && rng.Float64() < *dupRate {
title = items[rng.Intn(i)].Title
} else {
seen[title] = true
}
items[i] = Item{
ID: i + 1,
Title: title,
Artist: word(rng, 2+rng.Intn(2)),
Year: MinYear + rng.Intn(MaxYear-MinYear+1),
Rating: MinRating + rng.Intn(MaxRating-MinRating+1),
}
}
switch {
case *sorted:
sort.Slice(items, func(i, j int) bool { return items[i].Title < items[j].Title })
case *reverse:
sort.Slice(items, func(i, j int) bool { return items[i].Title > items[j].Title })
}
f, err := os.Create(*out)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
enc := json.NewEncoder(w)
for _, it := range items {
if err := enc.Encode(it); err != nil {
return err
}
}
if err := w.Flush(); err != nil {
return err
}
order := "shuffled"
if *sorted {
order = "sorted"
} else if *reverse {
order = "reverse"
}
fmt.Printf("wrote %d items to %s (seed=%d, order=%s, dup-rate=%.2f)\n",
*n, *out, *seed, order, *dupRate)
return nil
}
// load reads a library written by cmdGen.
func load(path string) ([]Item, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var items []Item
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for sc.Scan() {
var it Item
if err := json.Unmarshal(sc.Bytes(), &it); err != nil {
return nil, err
}
items = append(items, it)
}
return items, sc.Err()
}