89afe4ca99
# Conflicts: # cmd/mavend/actions_query.go # cmd/mavend/dayplan_test.go
150 lines
6.0 KiB
Go
150 lines
6.0 KiB
Go
package router
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
)
|
||
|
||
// CalendarEventFormatter formats calendar events into a Russian reply string.
|
||
type CalendarEventFormatter struct{}
|
||
|
||
// CalendarEntry — one event to recite. Uncertain marks an event maven did not
|
||
// read off a calendar server: the work calendar arrives as relayed phone
|
||
// notifications (Vikunja #126), stored below full confidence, and she says so
|
||
// rather than reciting a guess as fact.
|
||
type CalendarEntry struct {
|
||
Text string
|
||
Uncertain bool
|
||
}
|
||
|
||
// dayPlanWords — the tokens that ask for the day as a whole rather than for a
|
||
// calendar listing. Whole words, not substrings: "планёрка" is a MEETING, and a
|
||
// notification about one must not be mistaken for a request for the plan.
|
||
var dayPlanWords = []string{
|
||
"план", "плана", "плану", "плане", "планом",
|
||
"планы", "планов", "планам", "планах",
|
||
"расписание", "расписании", "распорядок", "распорядке",
|
||
"plan", "plans", "schedule", "agenda",
|
||
}
|
||
|
||
// otherDayWords — a span that is not the clock's own day. The plan can only be
|
||
// built for today, so an utterance naming another day, a weekday, a week or a
|
||
// weekend belongs to the calendar listing instead. Claiming it here would
|
||
// answer today and stamp it with today's date, which is a wrong answer where
|
||
// falling through is only a terse one.
|
||
//
|
||
// The weekday names are here as a refusal, not as a feature. "какие планы на
|
||
// понедельник?" carries no other-day token in the сегодня family and does carry
|
||
// "планы", so the plan used to claim it and recite today.
|
||
var otherDayWords = []string{
|
||
"завтра", "послезавтра", "вчера", "позавчера",
|
||
"tomorrow", "yesterday",
|
||
"понедельник", "вторник", "среду", "среда", "четверг", "пятницу", "пятница",
|
||
"субботу", "суббота", "воскресенье",
|
||
"понедельника", "вторника", "четверга", "пятницы", "субботы", "воскресенья",
|
||
"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
|
||
"неделю", "неделя", "недели", "неделе",
|
||
"выходные", "выходных", "выходным",
|
||
"месяц", "месяца", "месяце",
|
||
"week", "weekend", "month",
|
||
}
|
||
|
||
// IsDayPlanQuery reports whether an utterance asks for today's plan (Vikunja
|
||
// #128) — "какие планы на сегодня?", "что у меня по плану?", "что дальше?".
|
||
//
|
||
// Deliberately narrow. The calendar listing already answers "что у меня
|
||
// сегодня?" and a plan that hijacks every date-bearing question would bury the
|
||
// events under checklist lines. Only a plan-shaped ask, and only about today.
|
||
func IsDayPlanQuery(text string) bool {
|
||
// A habit question is never a day plan, whatever words it shares with one.
|
||
// "какие у меня обычно планы по вторникам?" carries "планы", so the plan
|
||
// source claimed it and answered today's calendar stamped with today's
|
||
// date, and the habit source never ran. Deciding it here rather than by
|
||
// reordering the source table keeps one matcher from depending on the
|
||
// other's position in a slice.
|
||
if _, ok := ParseHabitQuery(text); ok {
|
||
return false
|
||
}
|
||
toks := planTokens(text)
|
||
for _, t := range toks {
|
||
for _, w := range otherDayWords {
|
||
if t == w {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
for _, t := range toks {
|
||
for _, w := range dayPlanWords {
|
||
if t == w {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
// "что дальше?" / "what's next?" — the rest of the day, with no plan word
|
||
// in it. Both tokens rather than adjacency, because "what's" splits into
|
||
// "what" and "s" and because "и что потом дальше" is the same question.
|
||
return (hasTok(toks, "что") && hasTok(toks, "дальше")) ||
|
||
(hasTok(toks, "what") && hasTok(toks, "next"))
|
||
}
|
||
|
||
// IsRestOfDayQuery reports whether the utterance asks for what is left of the
|
||
// day rather than for the whole of it — "что дальше?" and its English form.
|
||
//
|
||
// Tokenized for the same reason IsDayPlanQuery is: the substring form matched
|
||
// "дальше" inside longer words and "next" inside "nextcloud", and the two
|
||
// predicates deciding the same utterance differently is worse than either
|
||
// being wrong on its own.
|
||
func IsRestOfDayQuery(text string) bool {
|
||
toks := planTokens(text)
|
||
return hasTok(toks, "дальше") || hasTok(toks, "next")
|
||
}
|
||
|
||
func hasTok(toks []string, w string) bool {
|
||
for _, t := range toks {
|
||
if t == w {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// planTokens lowercases and splits on everything that is not a letter or a
|
||
// digit, so "планы?" and "что-дальше" tokenize like the plain words do.
|
||
func planTokens(text string) []string {
|
||
return strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
|
||
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
||
})
|
||
}
|
||
|
||
// Format returns a Russian reply for the given calendar events on the given
|
||
// date. Every event is treated as certain — use FormatEntries when provenance
|
||
// differs between them.
|
||
func (f CalendarEventFormatter) Format(events []string, date time.Time) string {
|
||
entries := make([]CalendarEntry, len(events))
|
||
for i, e := range events {
|
||
entries[i] = CalendarEntry{Text: e}
|
||
}
|
||
return f.FormatEntries(entries, date)
|
||
}
|
||
|
||
// FormatEntries returns a Russian reply, hedging the entries maven is not sure
|
||
// about. "похоже" and not "возможно": the notification did arrive, what is
|
||
// uncertain is whether it describes the meeting correctly.
|
||
func (CalendarEventFormatter) FormatEntries(entries []CalendarEntry, date time.Time) string {
|
||
dateStr := date.Format("02.01.2006")
|
||
if len(entries) == 0 {
|
||
return fmt.Sprintf("на %s ничего нет.", dateStr)
|
||
}
|
||
parts := make([]string, len(entries))
|
||
for i, e := range entries {
|
||
if e.Uncertain {
|
||
parts[i] = "похоже, " + e.Text
|
||
continue
|
||
}
|
||
parts[i] = e.Text
|
||
}
|
||
return fmt.Sprintf("на %s: %s", dateStr, strings.Join(parts, "; "))
|
||
}
|