997f92f5c4
ruNumerals was a second copy of the number words. It stopped at fifty, had no oblique forms, and disagreed with lexicon_ru_v1.json about its own members, so "к семи" was not the hour "в семь" was. The lexicon now carries the oblique cardinals and numwords.go asks lexicon.Cardinal. "час" and "часу" stay local: they are the hour noun as often as the number one, and nobody counts "час яблок". reminderbody.go built its markers from three inline word lists. Two of them are new lexicon sets, reminder_verbs and parts_of_day, and the day offsets were already there. The alternation helper sorts by length so a longer form wins the regex. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.6 KiB
Go
86 lines
3.6 KiB
Go
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
|
|
}
|