From e6923490fde4a845c4855aaa783ac60323e4deee Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 03:32:48 +0400 Subject: [PATCH 1/2] the lexicon owns the weekdays and the oblique small numbers (V-581) Weekday names lived in four files outside internal/lexicon and each copy was short in a different direction. The habit map had the prepositional plural of Sunday and no dative of Wednesday. The plan refusal had the accusative of Wednesday and not the prepositional. cmd/mavend matched the stem. Weekdays hands out the seven nominatives whole, because every Russian case lemmatises to one of them and the case is morph's question. WeekdayEnglish is the half that has to be data: the vendored dictionary is Russian and leaves mondays as it found it. Cardinals gain the dative and instrumental of one to four. A spoken hour declines and five upward has one oblique form for the genitive, dative and prepositional, so pyati was already whole while dvum was missing and k dvum chasam is an hour he says. --- internal/lexicon/lexicon.go | 16 +++++++++++- internal/lexicon/lexicon_ru_v1.json | 22 ++++++++++++---- internal/lexicon/lexicon_test.go | 40 +++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/internal/lexicon/lexicon.go b/internal/lexicon/lexicon.go index 35ca1c7..227f3ea 100644 --- a/internal/lexicon/lexicon.go +++ b/internal/lexicon/lexicon.go @@ -62,7 +62,7 @@ func mustLoad() lexiconFile { } for _, name := range []string{ "interrogatives", "capture_verbs", "narrative_requests", "cardinals", "ordinals", - "day_offsets", "weekdays", "months_genitive", "hours_spoken", + "day_offsets", "weekdays", "weekdays_english", "months_genitive", "hours_spoken", "not_place_after_v", "parts_of_day", "reminder_verbs", "half_hour", "filler_particles", "task_done_words", "task_drop_words", "confirm_yes", "confirm_no", @@ -283,6 +283,20 @@ func DayOffsetIn(text string) (int, bool) { // Go's time.Weekday. An index off the end returns "". func Weekday(i int) string { return at("weekdays", i) } +// Weekdays returns the seven Russian names in one slice, Sunday first, for a +// caller matching a token against all of them rather than rendering one. Only +// the nominative is here: every other case lemmatises to it, so an oblique form +// is morph's question and not a second list (V-581). +func Weekdays() []string { return words("weekdays") } + +// WeekdayEnglish reports the Go time.Weekday index an English weekday names, +// singular or plural. English needs the list that Russian does not, because the +// vendored dictionary is Russian and leaves "mondays" as it found it. +func WeekdayEnglish(word string) (int, bool) { + n, ok := ru.Sets["weekdays_english"].Values[norm(word)] + return n, ok +} + // MonthGenitive returns the month name a date takes — "10 июля", not "июль". // The set is 1-indexed, so MonthGenitive(int(t.Month())) is the whole call. func MonthGenitive(m int) string { return at("months_genitive", m) } diff --git a/internal/lexicon/lexicon_ru_v1.json b/internal/lexicon/lexicon_ru_v1.json index 50cf1b0..608f0b2 100644 --- a/internal/lexicon/lexicon_ru_v1.json +++ b/internal/lexicon/lexicon_ru_v1.json @@ -56,13 +56,13 @@ } }, "cardinals": { - "note": "Number words as spoken, with the gender variants Russian requires (один/одна/одно and два/две agree with the noun that follows) and the oblique forms, because a spoken time declines: \"в семь\", \"к семи\", \"около семи\" are three forms of one hour (Vikunja #530). Values are the number itself. Twenties and up are compounds and are read as their parts, so only the round members are listed.", + "note": "Number words as spoken, with the gender variants Russian requires (один/одна/одно and два/две agree with the noun that follows) and the oblique forms, because a spoken time declines: \"в семь\", \"к семи\", \"около семи\" are three forms of one hour (Vikunja #530). Values are the number itself. Twenties and up are compounds and are read as their parts, so only the round members are listed. From five up one oblique form serves the genitive, dative and prepositional, so \"пяти\" is the whole set; one to four decline separately and carry the dative and instrumental of their own, because \"к двум часам\" and \"к трём\" are hours he says (V-581).", "values": { "ноль": 0, "нуль": 0, "zero": 0, - "один": 1, "одна": 1, "одно": 1, "одного": 1, "одной": 1, "одну": 1, "one": 1, - "два": 2, "две": 2, "двух": 2, "two": 2, - "три": 3, "трёх": 3, "трех": 3, "three": 3, - "четыре": 4, "четырёх": 4, "четырех": 4, "four": 4, + "один": 1, "одна": 1, "одно": 1, "одного": 1, "одной": 1, "одну": 1, "одному": 1, "одним": 1, "one": 1, + "два": 2, "две": 2, "двух": 2, "двум": 2, "двумя": 2, "two": 2, + "три": 3, "трёх": 3, "трех": 3, "трём": 3, "трем": 3, "тремя": 3, "three": 3, + "четыре": 4, "четырёх": 4, "четырех": 4, "четырём": 4, "четырем": 4, "четырьмя": 4, "four": 4, "пять": 5, "пяти": 5, "five": 5, "шесть": 6, "шести": 6, "six": 6, "семь": 7, "семи": 7, "seven": 7, @@ -111,6 +111,18 @@ "четверг", "пятница", "суббота" ] }, + "weekdays_english": { + "note": "The English weekday names with their Go time.Weekday index, plus the plural a habit is spoken in (\"on mondays\"). English is listed as words where Russian is not, because the vendored dictionary is Russian: it lemmatises \"пятницу\" to \"пятница\" on its own and leaves \"mondays\" alone (V-581). So the Russian side of a weekday match is grammar and the English side is data.", + "values": { + "sunday": 0, "sundays": 0, + "monday": 1, "mondays": 1, + "tuesday": 2, "tuesdays": 2, + "wednesday": 3, "wednesdays": 3, + "thursday": 4, "thursdays": 4, + "friday": 5, "fridays": 5, + "saturday": 6, "saturdays": 6 + } + }, "months_genitive": { "note": "The form a date takes: \"10 июля\", not \"июль\". 1-indexed, so slot 0 is empty and month numbers need no arithmetic.", "words": [ diff --git a/internal/lexicon/lexicon_test.go b/internal/lexicon/lexicon_test.go index 26c1a65..5f210e8 100644 --- a/internal/lexicon/lexicon_test.go +++ b/internal/lexicon/lexicon_test.go @@ -36,6 +36,46 @@ func TestClosedSetsAreComplete(t *testing.T) { if _, ok := Cardinal("бэкап"); ok { t.Error("Cardinal must not answer for a word that is not a number") } + + // A spoken hour declines, and one to four decline further than the rest: + // "к двум часам" and "к трём" are hours, and only the dative says so (V-581). + for _, tc := range []struct { + word string + want int + }{ + {"одному", 1}, {"двум", 2}, {"двумя", 2}, {"трём", 3}, {"трем", 3}, + {"четырём", 4}, {"четырем", 4}, {"пяти", 5}, {"семи", 7}, + } { + if got, ok := Cardinal(tc.word); !ok || got != tc.want { + t.Errorf("Cardinal(%q) = %d, %v; want %d, true", tc.word, got, ok, tc.want) + } + } +} + +// TestWeekdaysAreOneList — the second copy of a closed class is the bug (V-581). +// Weekdays lived in four files outside this one, so the list is handed out whole +// and the English forms, which the Russian dictionary cannot lemmatise, are here. +func TestWeekdaysAreOneList(t *testing.T) { + days := Weekdays() + if len(days) != 7 || days[0] != "воскресенье" || days[1] != "понедельник" { + t.Fatalf("Weekdays() = %v; want the seven, Sunday first", days) + } + for i, name := range days { + if Weekday(i) != name { + t.Errorf("Weekdays()[%d] = %q, but Weekday(%d) = %q", i, name, i, Weekday(i)) + } + } + for _, tc := range []struct { + word string + want int + }{{"sunday", 0}, {"monday", 1}, {"mondays", 1}, {"Friday", 5}, {"saturdays", 6}} { + if got, ok := WeekdayEnglish(tc.word); !ok || got != tc.want { + t.Errorf("WeekdayEnglish(%q) = %d, %v; want %d, true", tc.word, got, ok, tc.want) + } + } + if _, ok := WeekdayEnglish("понедельник"); ok { + t.Error("WeekdayEnglish answered for a Russian word; that side is morph's") + } } // TestDayOffsetHasNoOrderingTrap — the defect a lookup removes. The callers this From 9e1958e7b04a13b9dac920e0c30d7839433edd50 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 03:33:01 +0400 Subject: [PATCH 2/2] one weekday matcher, and a stem list stops answering for sredstvo (V-581) router.WeekdayIndex reads the lexicon and asks the dictionary about the case. Four private lists go away: the habit declension map, the weekday block of the day-plan refusal, the weekday and part-of-day entries of the weather guard, and the stem list in ruwords.go. The stem list was the real defect. mentionsUnknownDay matched sred, pyatnits and subbot with strings.Contains, so sredi, sredstvo and sredniy all read as Wednesday and a question carrying one was answered with onlyNearDaysReply instead of a date. It matches whole tokens now. The weather guard was a third copy of three closed sets that already exist. It kept the rooms of the house, which are its own, and asks the lexicon for the weekdays, the parts of the day and the words that follow v without naming a place. Questions phrased v srede, v utra and v obshchem reached the geocoder as cities before. Full suite green under -race. --- cmd/mavend/ruwords.go | 23 +++++++++------- cmd/mavend/ruwords_test.go | 35 ++++++++++++++++++++++++ cmd/mavend/weatherq.go | 48 ++++++++++++++++++++++++++------- cmd/mavend/weatherq_test.go | 7 +++++ internal/router/calendar.go | 15 ++++++----- internal/router/habit.go | 26 ++++-------------- internal/router/timementions.go | 35 ++++++++++++++++++------ internal/router/weekday_test.go | 44 ++++++++++++++++++++++++++++++ 8 files changed, 179 insertions(+), 54 deletions(-) create mode 100644 cmd/mavend/ruwords_test.go create mode 100644 internal/router/weekday_test.go diff --git a/cmd/mavend/ruwords.go b/cmd/mavend/ruwords.go index 0fe4a84..6ceff05 100644 --- a/cmd/mavend/ruwords.go +++ b/cmd/mavend/ruwords.go @@ -17,6 +17,7 @@ import ( "time" "github.com/kami/maven/internal/lexicon" + "github.com/kami/maven/internal/router" "github.com/kami/maven/internal/say" ) @@ -73,22 +74,26 @@ func mentionsUnknownPlace(u string) bool { // date for a day she did not understand. const onlyNearDaysReply = "я считаю только сегодня, завтра, послезавтра и вчера — про другие дни пока не скажу." -// dayWords — day references the calendar parser cannot resolve. A weekday name -// or a "через …" phrase means he asked about a specific other day. -var dayWords = []string{ - "понедельник", "вторник", "сред", "четверг", "пятниц", "суббот", "воскресен", - "через", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", -} - // mentionsUnknownDay reports whether the question names a day the calendar // parser could not resolve. Mirror of mentionsUnknownPlace: it exists only to // pick an honest reply over a confidently wrong one. // // Only called after ParseCalendarDate has already failed, so "завтра" and the // other words it does know never reach here. +// +// The weekday half was a list of STEMS matched with strings.Contains until +// V-581 — "сред", "пятниц", "суббот". That is the hand-written Russian pattern +// the sweep of 2026-08-04 took out, and it was wrong in the way such a pattern +// always is: "среди", "средство" and "средний" all contain "сред", so a question +// carrying any of them was answered with onlyNearDaysReply instead of the date. +// Whole tokens now, and the weekday itself is router.WeekdayIndex, which reads +// the lexicon and asks the dictionary about the case. func mentionsUnknownDay(u string) bool { - for _, w := range dayWords { - if strings.Contains(u, w) { + for _, tok := range quietTokens(u) { + if tok == "через" { + return true + } + if _, ok := router.WeekdayIndex(tok); ok { return true } } diff --git a/cmd/mavend/ruwords_test.go b/cmd/mavend/ruwords_test.go new file mode 100644 index 0000000..5b98fc5 --- /dev/null +++ b/cmd/mavend/ruwords_test.go @@ -0,0 +1,35 @@ +package main + +import "testing" + +// TestMentionsUnknownDayReadsWordsNotStems — the defect V-581 found. The +// weekday half of this guard was a list of stems matched with strings.Contains, +// so "среди", "средство" and "средний" all read as Wednesday and the question +// was answered with onlyNearDaysReply instead of a date. +// +// The other half of the fix is coverage: a stem list stops at the forms whoever +// wrote it thought of, and "воскресеньях" was not one of them. +func TestMentionsUnknownDayReadsWordsNotStems(t *testing.T) { + for _, u := range []string{ + "какое число в понедельник", + "какое число в среду", + "какое число в среде", + "что там по воскресеньям", + "what is the date on friday", + "какое число через неделю", + } { + if !mentionsUnknownDay(u) { + t.Errorf("mentionsUnknownDay(%q) = false, want true", u) + } + } + for _, u := range []string{ + "какое число в среднем", + "сколько это в среднем", + "какое сегодня средство", + "какое число", + } { + if mentionsUnknownDay(u) { + t.Errorf("mentionsUnknownDay(%q) = true; it names no day", u) + } + } +} diff --git a/cmd/mavend/weatherq.go b/cmd/mavend/weatherq.go index 04f67f9..b8254ca 100644 --- a/cmd/mavend/weatherq.go +++ b/cmd/mavend/weatherq.go @@ -7,6 +7,10 @@ package main import ( "regexp" "strings" + + "github.com/kami/maven/internal/lexicon" + "github.com/kami/maven/internal/morph" + "github.com/kami/maven/internal/router" ) // isWeatherQuery returns true if the utterance is about weather. @@ -27,16 +31,42 @@ func isWeatherQuery(u string) bool { // come through whole and "в 5 утра" does not. var weatherPlace = regexp.MustCompile(`(?i)(?:^|\s)(?:в|во|in)\s+([\p{L}-]+(?:\s+[\p{L}-]+)?)`) -// weatherNonPlaces — words that follow "в" in a weather question and are not -// cities. "какая погода в доме" is the smart-home sensor, not Open-Meteo, and -// "тепло в комнате" is the same question about the same room. -var weatherNonPlaces = map[string]bool{ +// weatherRooms — the rooms of the house, which are the only words in this +// guard that belong to it. "какая погода в доме" is the smart-home sensor, not +// Open-Meteo, and "тепло в комнате" is the same question about the same room. +// +// The rest of the guard used to be a third copy of three closed sets that +// already exist in the lexicon: the weekdays, the parts of the day, and the +// words that follow "в" without naming a place (V-581). Each copy was short in +// its own direction — "среду" but not "среде", "утром" but not "утра", "целом" +// but not "общем" — so the same question phrased one word differently reached +// the geocoder as a city. +var weatherRooms = map[string]bool{ "доме": true, "квартире": true, "комнате": true, "спальне": true, "гостиной": true, "кухне": true, "гараже": true, "офисе": true, - "выходные": true, "субботу": true, "воскресенье": true, "понедельник": true, - "вторник": true, "среду": true, "четверг": true, "пятницу": true, - "обед": true, "обеде": true, "утро": true, "утром": true, "вечер": true, - "вечером": true, "ночь": true, "ночью": true, "целом": true, "принципе": true, + "обед": true, "обеде": true, "выходные": true, "выходных": true, +} + +// isWeatherNonPlace reports whether the word after "в" names something other +// than a place he could ask the weather for. +func isWeatherNonPlace(word string) bool { + if weatherRooms[word] { + return true + } + if _, ok := router.WeekdayIndex(word); ok { + return true + } + for _, w := range lexicon.PartsOfDay() { + if word == w || morph.SameWord(word, w) { + return true + } + } + for _, w := range lexicon.NotPlaceAfterV() { + if word == w { + return true + } + } + return false } // extractWeatherLocation returns the place he named, or the configured default @@ -63,7 +93,7 @@ func extractWeatherLocation(u, defaultLoc string) string { } place := strings.TrimSpace(m[1]) first := strings.ToLower(strings.Fields(place)[0]) - if weatherNonPlaces[first] { + if isWeatherNonPlace(first) { return defaultLoc } return place diff --git a/cmd/mavend/weatherq_test.go b/cmd/mavend/weatherq_test.go index 8e24a2f..18c61f2 100644 --- a/cmd/mavend/weatherq_test.go +++ b/cmd/mavend/weatherq_test.go @@ -29,6 +29,13 @@ func TestExtractWeatherLocation(t *testing.T) { // the house sensors and the day words answer elsewhere. {"тепло в комнате?", "Berlin", "Berlin"}, {"какая погода в выходные", "Berlin", "Berlin"}, + // The cases the three private copies of the lexicon were short by + // (V-581): a weekday in a case the old map did not list, a part of the + // day in one it did not list, and "в общем". + {"какая погода в среде", "Berlin", "Berlin"}, + {"какая погода в воскресеньях", "Berlin", "Berlin"}, + {"какая погода в понедельникам", "Berlin", "Berlin"}, + {"какая погода в общем", "Berlin", "Berlin"}, } for _, c := range cases { if got := extractWeatherLocation(c.utterance, c.def); got != c.want { diff --git a/internal/router/calendar.go b/internal/router/calendar.go index de182d0..058208a 100644 --- a/internal/router/calendar.go +++ b/internal/router/calendar.go @@ -35,16 +35,14 @@ var dayPlanWords = []string{ // answer today and stamp it with today's date, which is a wrong answer where // falling through is only a terse one. // -// The weekday names are here as a refusal, not as a feature. "какие планы на -// понедельник?" carries no other-day token in the сегодня family and does carry -// "планы", so the plan used to claim it and recite today. +// A weekday is a refusal too, and it is not in this list: IsDayPlanQuery asks +// WeekdayIndex, so every case of every name refuses rather than the nine forms +// that used to be written out here (V-581). "какие планы на понедельник?" +// carries no other-day token in the сегодня family and does carry "планы", so +// the plan used to claim it and recite today. var otherDayWords = []string{ "завтра", "послезавтра", "вчера", "позавчера", "tomorrow", "yesterday", - "понедельник", "вторник", "среду", "среда", "четверг", "пятницу", "пятница", - "субботу", "суббота", "воскресенье", - "понедельника", "вторника", "четверга", "пятницы", "субботы", "воскресенья", - "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", "неделю", "неделя", "недели", "неделе", "выходные", "выходных", "выходным", "месяц", "месяца", "месяце", @@ -69,6 +67,9 @@ func IsDayPlanQuery(text string) bool { } toks := planTokens(text) for _, t := range toks { + if _, ok := WeekdayIndex(t); ok { + return false + } for _, w := range otherDayWords { if t == w { return false diff --git a/internal/router/habit.go b/internal/router/habit.go index f9e25d4..8f58ab6 100644 --- a/internal/router/habit.go +++ b/internal/router/habit.go @@ -27,26 +27,10 @@ var habitMarkers = []string{ "typically", "normally", } -// weekdayWords — every form of a weekday name maven needs to recognise, -// including the "по …ам" plural the question is usually phrased in. -var weekdayWords = map[string]time.Weekday{ - "понедельник": time.Monday, "понедельникам": time.Monday, - "вторник": time.Tuesday, "вторникам": time.Tuesday, - "среда": time.Wednesday, "среду": time.Wednesday, "средам": time.Wednesday, - "четверг": time.Thursday, "четвергам": time.Thursday, - "пятница": time.Friday, "пятницу": time.Friday, "пятницам": time.Friday, - "суббота": time.Saturday, "субботу": time.Saturday, "субботам": time.Saturday, - "воскресенье": time.Sunday, "воскресеньям": time.Sunday, - "воскресенья": time.Sunday, "воскресенью": time.Sunday, - "воскресеньем": time.Sunday, "воскресеньях": time.Sunday, - "monday": time.Monday, "mondays": time.Monday, - "tuesday": time.Tuesday, "tuesdays": time.Tuesday, - "wednesday": time.Wednesday, "wednesdays": time.Wednesday, - "thursday": time.Thursday, "thursdays": time.Thursday, - "friday": time.Friday, "fridays": time.Friday, - "saturday": time.Saturday, "saturdays": time.Saturday, - "sunday": time.Sunday, "sundays": time.Sunday, -} +// The weekday a habit question names comes from WeekdayIndex, not from a map +// here. This file used to keep its own declension table, which had "воскресеньях" +// and no "средах" — a list of forms is finished by whoever last thought of one, +// and a dictionary is not (V-581). // weekendWords — the weekend as one unit. "что я обычно делаю по выходным?" // has a habit marker and names days, but no weekday name is in it, so it used @@ -77,7 +61,7 @@ func ParseHabitQuery(text string) (HabitQuery, bool) { return HabitQuery{}, false } for _, t := range toks { - if wd, ok := weekdayWords[t]; ok { + if wd, ok := WeekdayIndex(t); ok { return HabitQuery{Weekday: wd, HasWeekday: true}, true } if weekendWords[t] { diff --git a/internal/router/timementions.go b/internal/router/timementions.go index 017c746..b5dd9f3 100644 --- a/internal/router/timementions.go +++ b/internal/router/timementions.go @@ -3,6 +3,7 @@ package router import ( "strconv" "strings" + "time" "github.com/kami/maven/internal/lexicon" "github.com/kami/maven/internal/morph" @@ -234,16 +235,34 @@ func isMonth(tok string) bool { return false } -// isWeekday reports whether the token is a day of the week in any case. The -// lexicon lists the nominative, and "в пятницу" is what a reminder says, so the -// match is by lemma — grammar is morph's job, not a second word list. -func isWeekday(tok string) bool { - for i := 0; i < 7; i++ { - if morph.SameWord(tok, lexicon.Weekday(i)) { - return true +// WeekdayIndex reports which day of the week a token names, in any case and in +// either language, or false when it names none. +// +// One matcher for the whole daemon (V-581). Four files used to keep a weekday +// list of their own and each one was short in a different direction: the habit +// map had "воскресеньях" but no "средах", the plan refusal had "среду" but not +// "среде", and cmd/mavend matched the STEM "сред" with strings.Contains, so +// "среди" and "средство" read as Wednesday. The lexicon lists the nominative, +// every Russian case lemmatises to it, and only English needs its forms written +// out — the vendored dictionary is Russian and leaves "mondays" alone. +func WeekdayIndex(tok string) (time.Weekday, bool) { + t := strings.ToLower(strings.TrimSpace(tok)) + if n, ok := lexicon.WeekdayEnglish(t); ok { + return time.Weekday(n), true + } + for i, name := range lexicon.Weekdays() { + if morph.SameWord(t, name) { + return time.Weekday(i), true } } - return false + return 0, false +} + +// isWeekday reports whether the token is a day of the week, when the caller +// does not need to know which one. +func isWeekday(tok string) bool { + _, ok := WeekdayIndex(tok) + return ok } // timeMarkers — the words that name a time on their own: the qualifiers that diff --git a/internal/router/weekday_test.go b/internal/router/weekday_test.go new file mode 100644 index 0000000..d881f1a --- /dev/null +++ b/internal/router/weekday_test.go @@ -0,0 +1,44 @@ +package router + +import ( + "testing" + "time" +) + +// TestWeekdayIndexReplacesFourLists — four files kept a weekday list of their +// own and each was short in a different direction (V-581). The forms below are +// the ones at least one of those lists missed, so they are the point of having +// one matcher: the lexicon names the day and the dictionary answers the case. +func TestWeekdayIndexReplacesFourLists(t *testing.T) { + for _, tc := range []struct { + word string + want time.Weekday + }{ + {"понедельник", time.Monday}, + {"понедельникам", time.Monday}, + {"понедельником", time.Monday}, + {"вторник", time.Tuesday}, + {"среда", time.Wednesday}, + {"среду", time.Wednesday}, + {"среде", time.Wednesday}, + {"средам", time.Wednesday}, + {"четверга", time.Thursday}, + {"пятницу", time.Friday}, + {"субботам", time.Saturday}, + {"воскресеньях", time.Sunday}, + {"Воскресенье", time.Sunday}, + {"monday", time.Monday}, + {"Fridays", time.Friday}, + } { + got, ok := WeekdayIndex(tc.word) + if !ok || got != tc.want { + t.Errorf("WeekdayIndex(%q) = %v, %v; want %v, true", tc.word, got, ok, tc.want) + } + } + // A stem match said yes to all of these. A word match says no. + for _, w := range []string{"среди", "средство", "средний", "среднем", "субботник", "", "через"} { + if _, ok := WeekdayIndex(w); ok { + t.Errorf("WeekdayIndex(%q) claimed a weekday", w) + } + } +}