442d3ec08e
"напомни мне позвонить маме в семь вечера" answered "не получилось разобрать время напоминания", while "в 19:00" set the reminder. Reminders arrive through speech, and speech says the hour in words, so this was the ordinary case failing and the typed one working. SpellOutDigits rewrites a spoken number as digits, but only when a time word stands beside it — "в три часа" becomes "в 3 часа" and "купить три яблока" is left alone. Both parsers see it: dateparser already rewrites "7 вечера" to "7 pm" and never saw a digit to rewrite, and the stub floor now reads the qualifier itself.
98 lines
4.1 KiB
Go
98 lines
4.1 KiB
Go
package router
|
|
|
|
import "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 "семья".
|
|
//
|
|
// 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",
|
|
}
|
|
|
|
// numeralContext — the words that make a numeral a time. A numeral is only
|
|
// rewritten when one of these sits next to it, so "три яблока" in a note is
|
|
// left alone and "в три часа" is not.
|
|
var numeralContext = map[string]bool{
|
|
"в": true, "во": true, "к": true, "около": true, "на": true,
|
|
"часа": true, "часов": true, "час": true, "часу": true,
|
|
"утра": true, "вечера": true, "дня": true, "ночи": true,
|
|
"минут": true, "минуты": true, "минуту": true,
|
|
"at": true, "by": true,
|
|
}
|
|
|
|
// SpellOutDigits rewrites spoken numbers as digits so the date parsers see the
|
|
// shape they know.
|
|
//
|
|
// "напомни мне позвонить маме в семь вечера" parsed to nothing, while "в 19:00"
|
|
// parsed fine (Vikunja #469). Speech is where reminders come from, and speech
|
|
// says the hour in words, so this is not a long-tail case — it is the ordinary
|
|
// one. dateparser reads "в 7 вечера" through the qualifier rewrite the python
|
|
// script already does; it does not read "в семь вечера".
|
|
//
|
|
// Conservative by construction: a numeral is only rewritten when a time word
|
|
// stands beside it. "три часа" becomes "3 часа"; "три яблока" stays as it is,
|
|
// and a note or a fact carrying a spoken number is untouched.
|
|
func SpellOutDigits(text string) string {
|
|
toks := strings.Fields(text)
|
|
if len(toks) == 0 {
|
|
return text
|
|
}
|
|
out := make([]string, len(toks))
|
|
copy(out, toks)
|
|
for i, tok := range toks {
|
|
key := strings.ToLower(strings.Trim(tok, ".,!?;:«»\"'"))
|
|
digit, ok := ruNumerals[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
// "час" and "часу" are the hour noun as often as they are the number
|
|
// one, and rewriting "в час дня" to "в 1 дня" is right either way. What
|
|
// must not happen is rewriting the noun that gives another numeral its
|
|
// context: "в семь часов" must keep "часов".
|
|
if !hasTimeNeighbour(toks, i) {
|
|
continue
|
|
}
|
|
out[i] = digit
|
|
}
|
|
return strings.Join(out, " ")
|
|
}
|
|
|
|
// hasTimeNeighbour reports whether the token before or after i is a time word.
|
|
func hasTimeNeighbour(toks []string, i int) bool {
|
|
for _, j := range []int{i - 1, i + 1} {
|
|
if j < 0 || j >= len(toks) {
|
|
continue
|
|
}
|
|
if numeralContext[strings.ToLower(strings.Trim(toks[j], ".,!?;:«»\"'"))] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|