package main import ( "regexp" "sort" "strings" "github.com/kami/maven/internal/lexicon" ) // reminderMarker — the words that open a reminder. Stripped because they are // the instruction, not the thing to say at the hour. // // The verbs come from the lexicon (Vikunja #530). They are a closed set of the // commands she answers to, exactly like capture_verbs, and the literal that // stood here knew four of them. var reminderMarker = regexp.MustCompile(`(?i)^\s*(?:` + alternation(lexicon.ReminderVerbs()) + `)\s*(?:мне|me)?[\s,:—-]*`) // reminderTimeWords — the time expressions a reminder carries, removed from // the body because the fire time is already a column. Ordered longest-first // where two could match the same words, so "через полтора часа" does not leave // "полтора" behind. // // Go's \b is ASCII-only and never fires next to a Cyrillic letter, so the word // boundaries here are written out as whitespace or an end of string — the same // trap the agenda grammars hit. // // The Russian word lists are gone (Vikunja #530). The day words are // lexicon.DayOffsetWords, which is why "вчера" and "позавчера" are stripped now // and were not before, and the times of day are lexicon.PartsOfDay. What is // still written out here is the shape of a clock reading — a preposition, digits, // a colon — which is structured input rather than a claim about Russian. var reminderTimeWords = []*regexp.Regexp{ regexp.MustCompile(`(?i)(^|\s)через\s+\S+(\s+(часа?|часов|минут[уы]?|секунд[уы]?|дня|дней|недел[юи]))?(\s|$)`), regexp.MustCompile(`(?i)(^|\s)(в|во)\s+\d{1,2}(:\d{2})?(\s*(часа?|часов))?(\s*(утра|вечера|дня|ночи))?(\s|$)`), regexp.MustCompile(`(?i)(^|\s)(` + alternation(lexicon.DayOffsetWords()) + `)(\s|$)`), regexp.MustCompile(`(?i)(^|\s)(` + alternation(lexicon.PartsOfDay()) + `)(\s|$)`), regexp.MustCompile(`(?i)(^|\s)(at|in)\s+\d{1,2}(:\d{2})?\s*(am|pm)?(\s|$)`), } // alternation folds a lexicon set into one regexp branch, longest member first // so "послезавтра" is not matched as "завтра" with a tail left behind. Sorted // rather than taken as given, because two members of equal length must still // produce the same pattern on every build. func alternation(set []string) string { out := make([]string, 0, len(set)) for _, w := range set { out = append(out, regexp.QuoteMeta(w)) } sort.Slice(out, func(i, j int) bool { if len(out[i]) != len(out[j]) { return len(out[i]) > len(out[j]) } return out[i] < out[j] }) return strings.Join(out, "|") } // reminderBody is what she says at the hour. // // The whole utterance used to be stored, so /reminders read "напомни завтра в // 9 утра выпить таблетки" where it should read "выпить таблетки", and the // agenda recited the marker back at him (Vikunja #469). The fire time is // already a column, and the marker is an instruction that was carried out. // // Falls back to the fuller text whenever stripping would leave nothing: an // empty body is a reminder that fires and says nothing, which is worse than a // wordy one. func reminderBody(utterance, text string) string { body := strings.TrimSpace(text) if body == "" { body = strings.TrimSpace(utterance) } stripped := reminderMarker.ReplaceAllString(body, "") for _, re := range reminderTimeWords { stripped = re.ReplaceAllString(stripped, " ") } stripped = strings.TrimSpace(strings.Join(strings.Fields(stripped), " ")) stripped = strings.Trim(stripped, " ,;:—-") if stripped == "" { return body } return stripped }