Add regional variant tagging (us/gb) for English words
- SCOWL loader now loads american-words and british-words alongside english-words, tagging each with variant "us", "gb", or NULL (common) - Words appearing in both American and British lists get NULL variant - Add variant column to words table with schema migration - API: /define returns variant field for English, /random and /words accept ?variant=us|gb filter - Add total word count to server status line - Fix rows.Close leak in PopulateCMUTails, add rows.Err() check - Add tests for PopulateCMUTails, cmuTail, TotalWordCount - Expand test seed data and assertions to cover pt-PT definitions, synonyms, and validity checks - Update README with variant docs, /difficulty, /words, /rhyme endpoints, and current database statistics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,35 +31,61 @@ var scowlFrequency = map[int]int{
|
||||
}
|
||||
|
||||
type scowlFile struct {
|
||||
path string
|
||||
size int
|
||||
path string
|
||||
size int
|
||||
variant string // "us", "gb", or "" for common English
|
||||
}
|
||||
|
||||
func (SCOWLLoader) Load(db *sql.DB, dataDir string) error {
|
||||
pattern := filepath.Join(dataDir, "scowl", "final", "english-words.*")
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scowl: glob: %w", err)
|
||||
type prefixVariant struct {
|
||||
prefix string
|
||||
variant string
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("scowl: no files matching %s", pattern)
|
||||
prefixes := []prefixVariant{
|
||||
{"english-words", ""},
|
||||
{"american-words", "us"},
|
||||
{"british-words", "gb"},
|
||||
}
|
||||
|
||||
// Filter to sizes <= 70 and track the tier
|
||||
var allFiles []string
|
||||
for _, pv := range prefixes {
|
||||
pattern := filepath.Join(dataDir, "scowl", "final", pv.prefix+".*")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scowl: glob: %w", err)
|
||||
}
|
||||
allFiles = append(allFiles, matches...)
|
||||
}
|
||||
if len(allFiles) == 0 {
|
||||
return fmt.Errorf("scowl: no word files found in %s", filepath.Join(dataDir, "scowl", "final"))
|
||||
}
|
||||
|
||||
// Build a map from prefix to variant for quick lookup
|
||||
prefixToVariant := make(map[string]string)
|
||||
for _, pv := range prefixes {
|
||||
prefixToVariant[pv.prefix] = pv.variant
|
||||
}
|
||||
|
||||
// Filter to sizes <= 70 and track the tier + variant
|
||||
var selected []scowlFile
|
||||
for _, f := range files {
|
||||
for _, f := range allFiles {
|
||||
base := filepath.Base(f)
|
||||
// Files are like english-words.10, english-words.20, etc.
|
||||
// Files are like english-words.10, american-words.35, etc.
|
||||
parts := strings.SplitN(base, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
prefix := parts[0]
|
||||
variant, ok := prefixToVariant[prefix]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var size int
|
||||
if _, err := fmt.Sscanf(parts[1], "%d", &size); err != nil {
|
||||
continue
|
||||
}
|
||||
if size <= 70 {
|
||||
selected = append(selected, scowlFile{path: f, size: size})
|
||||
selected = append(selected, scowlFile{path: f, size: size, variant: variant})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +95,15 @@ func (SCOWLLoader) Load(db *sql.DB, dataDir string) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.Prepare(`INSERT INTO words (word, lang, frequency) VALUES (?, 'en', ?)
|
||||
ON CONFLICT(word, lang) DO UPDATE SET frequency = MAX(frequency, excluded.frequency)`)
|
||||
stmt, err := tx.Prepare(`INSERT INTO words (word, lang, frequency, variant) VALUES (?, 'en', ?, ?)
|
||||
ON CONFLICT(word, lang) DO UPDATE SET
|
||||
frequency = MAX(frequency, excluded.frequency),
|
||||
variant = CASE
|
||||
WHEN words.variant IS NULL THEN excluded.variant
|
||||
WHEN excluded.variant IS NULL THEN words.variant
|
||||
WHEN words.variant = excluded.variant THEN words.variant
|
||||
ELSE NULL
|
||||
END`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scowl: prepare: %w", err)
|
||||
}
|
||||
@@ -79,7 +112,7 @@ func (SCOWLLoader) Load(db *sql.DB, dataDir string) error {
|
||||
var count int
|
||||
for _, sf := range selected {
|
||||
freq := scowlFrequency[sf.size]
|
||||
n, err := loadSCOWLFile(stmt, sf.path, freq)
|
||||
n, err := loadSCOWLFile(stmt, sf.path, freq, sf.variant)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scowl: load %s: %w", sf.path, err)
|
||||
}
|
||||
@@ -93,13 +126,19 @@ func (SCOWLLoader) Load(db *sql.DB, dataDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSCOWLFile(stmt *sql.Stmt, path string, freq int) (int, error) {
|
||||
func loadSCOWLFile(stmt *sql.Stmt, path string, freq int, variant string) (int, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Convert empty variant to SQL NULL
|
||||
var variantParam any
|
||||
if variant != "" {
|
||||
variantParam = variant
|
||||
}
|
||||
|
||||
var count int
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
@@ -111,7 +150,7 @@ func loadSCOWLFile(stmt *sql.Stmt, path string, freq int) (int, error) {
|
||||
if containsAny(word, " ", "'", "-") || containsDigit(word) {
|
||||
continue
|
||||
}
|
||||
if _, err := stmt.Exec(word, freq); err != nil {
|
||||
if _, err := stmt.Exec(word, freq, variantParam); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
count++
|
||||
|
||||
Reference in New Issue
Block a user