Files
Maven/internal/router/calendar.go
T
kami 89afe4ca99 Merge branch 'fix/g04' into fix/integrated
# Conflicts:
#	cmd/mavend/actions_query.go
#	cmd/mavend/dayplan_test.go
2026-08-01 14:19:15 +04:00

150 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "; "))
}