diff --git a/internal/lexicon/lexicon.go b/internal/lexicon/lexicon.go index 227f3ea..0feb4b7 100644 --- a/internal/lexicon/lexicon.go +++ b/internal/lexicon/lexicon.go @@ -65,7 +65,7 @@ func mustLoad() lexiconFile { "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", + "confirm_yes", "confirm_no", "hour_units", "minute_units", } { s, ok := f.Sets[name] if !ok || (len(s.Words) == 0 && len(s.Values) == 0) { @@ -139,7 +139,40 @@ func TaskDropWords() []string { return words("task_drop_words") } // making the utterance a request of its own. A caller strips these (along with // the numbers and the other closed time sets) to see whether an utterance // carries any content beside the value it was asked for. See the set's note. -func SlotValueFrame() []string { return words("slot_value_frame") } +// The hour and the minute nouns are part of the frame and are kept in their own +// sets, so there is one copy of each closed class rather than a copy per caller. +func SlotValueFrame() []string { + out := words("slot_value_frame") + out = append(out, HourUnits()...) + out = append(out, MinuteUnits()...) + return out +} + +// HourUnits returns every form of the hour noun, and MinuteUnits every form of +// the minute noun. One home for each, because four router sets used to list the +// hour and all four stopped at "часу" (V-609). A caller folding time words into +// one set reads these; a caller asking about a single word reads IsHourUnit or +// IsMinuteUnit. +func HourUnits() []string { return words("hour_units") } + +// MinuteUnits — see HourUnits. +func MinuteUnits() []string { return words("minute_units") } + +// IsHourUnit reports whether a word is the hour noun in any form. +func IsHourUnit(word string) bool { return inSet("hour_units", word) } + +// IsMinuteUnit reports whether a word is the minute noun in any form. +func IsMinuteUnit(word string) bool { return inSet("minute_units", word) } + +func inSet(set, word string) bool { + w := norm(word) + for _, s := range ru.Sets[set].Words { + if w == s { + return true + } + } + return false +} // DialogueCancel returns the ways he calls off the request Maven is assembling. // Distinct from TaskDropWords, which abandons an item that already exists. diff --git a/internal/lexicon/lexicon_ru_v1.json b/internal/lexicon/lexicon_ru_v1.json index 608f0b2..8629c96 100644 --- a/internal/lexicon/lexicon_ru_v1.json +++ b/internal/lexicon/lexicon_ru_v1.json @@ -210,6 +210,20 @@ "передумал", "передумала", "неактуально" ] }, + "hour_units": { + "note": "Every form of the hour noun, Russian and English (V-609). One home for a closed class that four router sets used to list separately, and all four stopped at \"часу\": \"напомни к двум часам\" lost its hour and the reminder was left asking \"Когда?\". Russian declines, so the dative plural is as ordinary a way to say an hour as the accusative singular. A caller that folds time words into one set reads HourUnits; a caller asking about one word reads IsHourUnit.", + "words": [ + "час", "часа", "часов", "часу", "часам", "часами", "часах", + "hour", "hours" + ] + }, + "minute_units": { + "note": "Every form of the minute noun, Russian and English (V-609). Same class as hour_units one noun over, and it had the same gap: the dative plural \"минутам\" was missing everywhere \"минут\" and \"минуты\" were present.", + "words": [ + "минута", "минуты", "минуту", "минут", "минуте", "минутам", "минутами", "минутах", + "minute", "minutes" + ] + }, "slot_value_frame": { "note": "The words that can stand around a bare slot value without making the utterance a request of its own (Vikunja #560). Prepositions, hedges and the nouns a spoken time is built from: strip these, the numbers, the interrogatives, the filler particles and the other time sets, and whatever is left is the utterance's OWN content. \"а что если в 11:00\" leaves nothing and is an answer; \"какая сейчас погода в Риме\" leaves \"погода\" and \"Риме\" and is not. Closed because each part of it is closed — Russian has a fixed list of prepositions, and a clock is built from a fixed list of nouns. It is not a stopword list: a word goes in only if it can never be the thing he is asking about.", "words": [ @@ -217,10 +231,10 @@ "at", "on", "in", "by", "to", "till", "until", "after", "before", "about", "for", "нет", "не", "да", "ага", "угу", "ой", "ох", "тогда", "лучше", "может", "можно", "наверное", "наверно", "пожалуй", "точнее", "скорее", "если", "пусть", "прости", "извини", "слушай", "значит", "как-то", "типа", "вообще-то", "no", "yes", "yeah", "ok", "okay", "sorry", "maybe", "actually", "rather", "then", "well", - "час", "часа", "часов", "часу", "часам", "минут", "минута", "минуты", "минуту", "минутах", "полдень", "полночь", "полдня", + "полдень", "полночь", "полдня", "утра", "утро", "утру", "дня", "день", "днями", "вечера", "вечер", "вечеру", "ночи", "ночь", "ночью", "сейчас", "теперь", "сегодняшний", "ближайший", "ближайшее", - "hour", "hours", "minute", "minutes", "noon", "midnight", "am", "pm", "oclock", "now" + "noon", "midnight", "am", "pm", "oclock", "now" ] }, "dialogue_cancel": { diff --git a/internal/router/dateparser.go b/internal/router/dateparser.go index 15cd364..f15d44a 100644 --- a/internal/router/dateparser.go +++ b/internal/router/dateparser.go @@ -45,10 +45,10 @@ try: now = datetime.fromisoformat(sys.argv[2]) # Pre-process: replace Russian time qualifiers with AM/PM. # Handles "9 утра", "10 часов утра", "3 часа дня" etc. - text = re.sub(r'(\d+)\s+(?:час(?:а|ов)?\s+)?утра\b', r'\1 am', text, flags=re.IGNORECASE) - text = re.sub(r'(\d+)\s+(?:час(?:а|ов)?\s+)?вечера\b', r'\1 pm', text, flags=re.IGNORECASE) - text = re.sub(r'(\d+)\s+(?:час(?:а|ов)?\s+)?дня\b', r'\1 pm', text, flags=re.IGNORECASE) - text = re.sub(r'(\d+)\s+(?:час(?:а|ов)?\s+)?ночи\b', r'\1 am', text, flags=re.IGNORECASE) + text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?утра\b', r'\1 am', text, flags=re.IGNORECASE) + text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?вечера\b', r'\1 pm', text, flags=re.IGNORECASE) + text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?дня\b', r'\1 pm', text, flags=re.IGNORECASE) + text = re.sub(r'(\d+)\s+(?:час(?:а|ов|у|ам)?\s+)?ночи\b', r'\1 am', text, flags=re.IGNORECASE) # A bare hour after a preposition is dropped on the floor by dateparser: # "завтра в 7" resolves to tomorrow at the CURRENT clock, and "завтра в 7 # часов" is read as seven hours from now. Only a qualifier (already an @@ -56,7 +56,9 @@ try: # English "at 7" fails identically, so both prepositions are rewritten. # "на 9" is the same hour said with the other preposition, and it was not # read at all until V-579: "в 9" set the reminder and "на 9" did not. - text = re.sub(r'(?