numbers and reminder markers come from the lexicon (V-530)

ruNumerals was a second copy of the number words. It stopped at fifty, had no
oblique forms, and disagreed with lexicon_ru_v1.json about its own members, so
"к семи" was not the hour "в семь" was. The lexicon now carries the oblique
cardinals and numwords.go asks lexicon.Cardinal. "час" and "часу" stay local:
they are the hour noun as often as the number one, and nobody counts "час
яблок".

reminderbody.go built its markers from three inline word lists. Two of them
are new lexicon sets, reminder_verbs and parts_of_day, and the day offsets
were already there. The alternation helper sorts by length so a longer form
wins the regex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 21:18:52 +04:00
parent d9ef9ecef2
commit 997f92f5c4
4 changed files with 115 additions and 65 deletions
+28 -32
View File
@@ -1,38 +1,34 @@
package router
import "strings"
import (
"strconv"
"strings"
// ruNumerals — spoken numbers as digits, for the clock hours and the minutes
// that follow them. Every case ending he might say is listed rather than
// stemmed: "в семь", "к семи", "около семи" are three forms of one hour, and a
// prefix rule short enough to cover them also matches "семья".
"github.com/kami/maven/internal/lexicon"
)
// hourNouns — the two words that are the hour noun as often as they are the
// number one. "в час дня" means one o'clock, so rewriting it to "в 1 дня" is
// right either way.
//
// Stops at thirty, which is as far as a spoken time goes ("без двадцати
// восемь", "в половине шестого"). Anything larger is said in digits.
var ruNumerals = map[string]string{
"один": "1", "одного": "1", "одну": "1", "час": "1", "часу": "1",
"два": "2", "две": "2", "двух": "2",
"три": "3", "трёх": "3", "трех": "3",
"четыре": "4", "четырёх": "4", "четырех": "4",
"пять": "5", "пяти": "5",
"шесть": "6", "шести": "6",
"семь": "7", "семи": "7",
"восемь": "8", "восьми": "8",
"девять": "9", "девяти": "9",
"десять": "10", "десяти": "10",
"одиннадцать": "11", "одиннадцати": "11",
"двенадцать": "12", "двенадцати": "12",
"тринадцать": "13", "тринадцати": "13",
"четырнадцать": "14", "четырнадцати": "14",
"пятнадцать": "15", "пятнадцати": "15",
"шестнадцать": "16", "шестнадцати": "16",
"семнадцать": "17", "семнадцати": "17",
"восемнадцать": "18", "восемнадцати": "18",
"девятнадцать": "19", "девятнадцати": "19",
"двадцать": "20", "двадцати": "20",
"тридцать": "30", "тридцати": "30",
"сорок": "40", "сорока": "40",
"пятьдесят": "50", "пятидесяти": "50",
// They are not cardinals and do not belong in the lexicon's number set: nobody
// counts "час яблок". Everything else this file reads comes from
// lexicon.Cardinal, which is where the number words live complete, oblique forms
// included (Vikunja #530). The table here used to be a second copy that stopped
// at fifty and disagreed with the lexicon about its own members.
var hourNouns = map[string]string{"час": "1", "часу": "1"}
// numeralDigit reports the digits a spoken number is written as, for a clock
// hour or the minutes after it.
func numeralDigit(word string) (string, bool) {
if d, ok := hourNouns[word]; ok {
return d, true
}
n, ok := lexicon.Cardinal(word)
if !ok {
return "", false
}
return strconv.Itoa(n), true
}
// numeralContext — the words that make a numeral a time. A numeral is only
@@ -67,7 +63,7 @@ func SpellOutDigits(text string) string {
copy(out, toks)
for i, tok := range toks {
key := strings.ToLower(strings.Trim(tok, ".,!?;:«»\"'"))
digit, ok := ruNumerals[key]
digit, ok := numeralDigit(key)
if !ok {
continue
}