package router import ( "strconv" "strings" "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. // // 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 // 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 { // A half hour and a quarter-to hour are phrases rather than numerals, and // the hour they name is one less than the word in them (V-538). They are // rewritten whole, before the token pass, and come out as digits it leaves // alone. text = rewriteHalfPast(text) 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 := numeralDigit(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 }