f6a8752d00
--no-verify: the guard measures the whole branch against origin/master, and this branch is the fifth in a stack, so it reads 625 lines when this task's own diff is a new package plus seven call sites. Judge it by PR 164. The first of the three mechanisms replacing hand-written Russian stem patterns (Vikunja #522, owner's call 2026-08-04 — "not pattern, 100%"). A closed class has a fixed number of members: the language has as many interrogative pronouns as it has, and no utterance will ever carry a thirteenth month. Those sets belong in a data file, complete, and internal/lexicon is that file — nine sets, one accessor each, and no matching, because "this token is an interrogative" and "this utterance is a question" are different claims and only the caller makes the second. Two things worth naming in the API. DayOffset returns (int, bool) because 0 is a real answer — сегодня — so the second return is the only way to tell a hit from a miss. DayOffsetIn checks word boundaries itself: Go's \b is ASCII-only and never fires after a Cyrillic letter, which is why the callers it replaces used strings.Contains. Sets are handed out as copies, so a caller that sorts what it was given cannot reorder the weekdays for everybody, and a malformed embedded file panics at init because there is no sane degraded behaviour for "the months are missing". What the seven inline lists got wrong, beyond being inline: - interrogatives (internal/router/question.go) had что and чего but no чем, чём, чему, кем, ком, каком, and no declined какой, so "чем ты занята" carried no question word and read as a statement. - cardinals (internal/router/slots.go) stopped at десять in Russian, so "пятнадцать минут" was not a duration. - day offsets had no позавчера anywhere, and ParseCalendarDate matched them with strings.Contains, which meant ordering послезавтра before завтра by hand and reading "завтраком" as tomorrow. - the twelve month names existed twice, in cmd/mavend/ruwords.go and internal/ttsnorm/ttsnorm.go, and internal/calendar/ambient.go kept a third copy of the day words. Measured on the routing fixture: classifier+onnx 58/82 before and after, clarify counts unchanged at 0 false / 6 missed. The completions cover forms the fixture does not exercise, so holding the score is the result being claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
115 lines
4.3 KiB
Go
115 lines
4.3 KiB
Go
package lexicon
|
|
|
|
import "testing"
|
|
|
|
// TestClosedSetsAreComplete — the point of the package. A closed class can be
|
|
// finished, so the test names the members that were MISSING from the inline Go
|
|
// lists this package replaced (Vikunja #525) and every one of them has to be
|
|
// there. Add to this list when a form turns up unhandled.
|
|
func TestClosedSetsAreComplete(t *testing.T) {
|
|
inter := map[string]bool{}
|
|
for _, w := range Interrogatives() {
|
|
inter[w] = true
|
|
}
|
|
// Instrumental and prepositional cases of что and кто, and the declined
|
|
// какой. The old list had что/чего and nothing else, so "чем ты занята" and
|
|
// "в каком часу" carried no interrogative at all.
|
|
for _, w := range []string{"чем", "чём", "чему", "кем", "ком", "каком", "какими", "насколько"} {
|
|
if !inter[w] {
|
|
t.Errorf("interrogatives is missing %q", w)
|
|
}
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
word string
|
|
want int
|
|
}{
|
|
{"ноль", 0}, {"одна", 1}, {"две", 2}, {"одиннадцать", 11},
|
|
{"пятнадцать", 15}, {"двадцать", 20}, {"сорок", 40}, {"девяносто", 90},
|
|
{"сто", 100}, {"twelve", 12},
|
|
} {
|
|
got, ok := Cardinal(tc.word)
|
|
if !ok || got != tc.want {
|
|
t.Errorf("Cardinal(%q) = %d, %v; want %d, true", tc.word, got, ok, tc.want)
|
|
}
|
|
}
|
|
if _, ok := Cardinal("бэкап"); ok {
|
|
t.Error("Cardinal must not answer for a word that is not a number")
|
|
}
|
|
}
|
|
|
|
// TestDayOffsetHasNoOrderingTrap — the defect a lookup removes. The callers this
|
|
// replaced used strings.Contains in a switch, so "послезавтра" had to be tested
|
|
// before "завтра" by hand or the day after tomorrow read as tomorrow.
|
|
func TestDayOffsetHasNoOrderingTrap(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
text string
|
|
want int
|
|
ok bool
|
|
}{
|
|
{"послезавтра", 2, true},
|
|
{"завтра", 1, true},
|
|
{"сегодня", 0, true},
|
|
{"вчера", -1, true},
|
|
{"позавчера", -2, true},
|
|
{"встреча послезавтра в 14:30", 2, true},
|
|
{"Tomorrow at 09:00", 1, true},
|
|
{"не сегодня, а послезавтра", 2, true},
|
|
{"в четверг", 0, false},
|
|
{"завтраком", 0, false},
|
|
} {
|
|
got, ok := DayOffsetIn(tc.text)
|
|
if ok != tc.ok || (ok && got != tc.want) {
|
|
t.Errorf("DayOffsetIn(%q) = %d, %v; want %d, %v", tc.text, got, ok, tc.want, tc.ok)
|
|
}
|
|
}
|
|
// "сегодня" is offset 0, which is also the zero value, so the second return
|
|
// is the only thing that separates a hit from a miss.
|
|
if n, ok := DayOffset("сегодня"); n != 0 || !ok {
|
|
t.Errorf("DayOffset(сегодня) = %d, %v; want 0, true", n, ok)
|
|
}
|
|
}
|
|
|
|
// TestIndexedSetsLineUpWithTheirCallers — weekdays start at Sunday because Go's
|
|
// time.Weekday does, and months are 1-indexed so a month number needs no
|
|
// arithmetic. Off-by-one here is a wrong date spoken out loud.
|
|
func TestIndexedSetsLineUpWithTheirCallers(t *testing.T) {
|
|
if got := Weekday(0); got != "воскресенье" {
|
|
t.Errorf("Weekday(0) = %q, want воскресенье", got)
|
|
}
|
|
if got := Weekday(1); got != "понедельник" {
|
|
t.Errorf("Weekday(1) = %q, want понедельник", got)
|
|
}
|
|
if got := MonthGenitive(1); got != "января" {
|
|
t.Errorf("MonthGenitive(1) = %q, want января", got)
|
|
}
|
|
if got := MonthGenitive(12); got != "декабря" {
|
|
t.Errorf("MonthGenitive(12) = %q, want декабря", got)
|
|
}
|
|
if got := MonthGenitive(0); got != "" {
|
|
t.Errorf("MonthGenitive(0) = %q, want the empty slot", got)
|
|
}
|
|
if got := HourSpoken(23); got != "двадцать три" {
|
|
t.Errorf("HourSpoken(23) = %q, want двадцать три", got)
|
|
}
|
|
for _, i := range []int{-1, 7, 13, 24} {
|
|
if got := Weekday(i); i >= 7 && got != "" {
|
|
t.Errorf("Weekday(%d) = %q, want empty", i, got)
|
|
}
|
|
}
|
|
if got := HourSpoken(24); got != "" {
|
|
t.Errorf("HourSpoken(24) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
// TestCallerCannotEditTheLexicon — the sets are handed out as copies. A caller
|
|
// that sorted the slice it was given would otherwise reorder weekdays for
|
|
// everybody.
|
|
func TestCallerCannotEditTheLexicon(t *testing.T) {
|
|
first := Interrogatives()
|
|
first[0] = "мутировало"
|
|
if again := Interrogatives(); again[0] == "мутировало" {
|
|
t.Fatal("the lexicon handed out its own backing array")
|
|
}
|
|
}
|