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 day that is not today. The plan is built for the clock's // own day only, so an utterance naming another one belongs to the calendar // listing instead. Claiming it here would answer the wrong day, which is worse // than answering more tersely. var otherDayWords = []string{ "завтра", "послезавтра", "вчера", "позавчера", "tomorrow", "yesterday", } // 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 { 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")) } 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, "; ")) }