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 }