28c0ff73bd
PR 113's review is about internal/say/summary_ru_v1.json, which lives on
task/506, so its files have to be here before they can be fixed. Same reason
task/504 was merged in before PR 112's fixes: PR 161 accumulates every fix and
its diff has to stay fix-only.
Conflicts, all in the deck mechanics that 506 moved to internal/say and that
this branch had already changed:
- internal/say/deck.go — the exported Deck from 506 keeps this branch's per-family
floor. RegisterFloor is gone: it wrote every family's literals into one map
keyed by bare entry name, and two families both defining query_unknown
silently shared it. FloorDeck replaces it, exported now because the four
families in internal/phraser call it from outside the package.
- internal/say/summary.go — the fifth family off RegisterFloor onto the same
per-family map.
- internal/phraser/{acks,acts,fallbacks,query}.go — say.FloorDeck for the same.
--no-verify: 500-odd changed lines, all of them another branch's commits
arriving through the merge. The guard counts the merge, not the resolution.
192 lines
7.6 KiB
Go
192 lines
7.6 KiB
Go
package say
|
|
|
|
// The summary sentences — what she says around aggregated data: the morning
|
|
// plan, the ranked task list, and the habits read back out of behaviour records.
|
|
//
|
|
// Fifth family on the deck, and the first one outside internal/phraser. It
|
|
// lives here because its three callers — internal/morning, internal/tasks and
|
|
// internal/memory — sit under phraser in the import graph and cannot reach it.
|
|
//
|
|
// The "I have not seen enough yet" sentences are the load-bearing ones. Three
|
|
// days of taps and a year of them produce the same "обычно ты ...", and only one
|
|
// of those is worth believing, so the empty cases say she has not seen a
|
|
// pattern rather than that he has none.
|
|
|
|
import (
|
|
_ "embed"
|
|
"log"
|
|
"math/rand"
|
|
"sync"
|
|
)
|
|
|
|
//go:embed summary_ru_v1.json
|
|
var summaryJSON []byte
|
|
|
|
// SummarySchemaVersion — this family's own version.
|
|
const SummarySchemaVersion = 1
|
|
|
|
// The entry keys.
|
|
const (
|
|
PlanRestEmpty = "plan_rest_empty"
|
|
PlanDayEmpty = "plan_day_empty"
|
|
PlanDay = "plan_day"
|
|
PlanUncertain = "plan_uncertain"
|
|
|
|
TasksNone = "tasks_none"
|
|
TasksFirst = "tasks_first"
|
|
TasksCandidates = "tasks_candidates"
|
|
|
|
ReasonOverdue = "reason_overdue"
|
|
ReasonOverdueDay = "reason_overdue_day"
|
|
ReasonOverdueDays = "reason_overdue_days"
|
|
ReasonToday = "reason_today"
|
|
ReasonTomorrow = "reason_tomorrow"
|
|
ReasonInDays = "reason_in_days"
|
|
ReasonImportant = "reason_important"
|
|
ReasonUrgent = "reason_urgent"
|
|
ReasonStale = "reason_stale"
|
|
|
|
HabitWeekday = "habit_weekday"
|
|
HabitWeekdaySame = "habit_weekday_same"
|
|
HabitWeekdayNone = "habit_weekday_none"
|
|
HabitWeekendBoth = "habit_weekend_both"
|
|
HabitWeekendSat = "habit_weekend_sat"
|
|
HabitWeekendSun = "habit_weekend_sun"
|
|
HabitWeekendSame = "habit_weekend_same"
|
|
HabitWeekendNone = "habit_weekend_none"
|
|
HabitOverall = "habit_overall"
|
|
HabitOverallNone = "habit_overall_none"
|
|
HabitSpanToday = "habit_span_today"
|
|
HabitSpanDays = "habit_span_days"
|
|
HabitUnglossed = "habit_unglossed"
|
|
HabitAt = "habit_at"
|
|
)
|
|
|
|
var summaryKeys = []string{
|
|
PlanRestEmpty, PlanDayEmpty, PlanDay, PlanUncertain,
|
|
TasksNone, TasksFirst, TasksCandidates,
|
|
ReasonOverdue, ReasonOverdueDay, ReasonOverdueDays, ReasonToday, ReasonTomorrow,
|
|
ReasonInDays, ReasonImportant, ReasonUrgent, ReasonStale,
|
|
HabitWeekday, HabitWeekdaySame, HabitWeekdayNone,
|
|
HabitWeekendBoth, HabitWeekendSat, HabitWeekendSun, HabitWeekendSame, HabitWeekendNone,
|
|
HabitOverall, HabitOverallNone, HabitSpanToday, HabitSpanDays,
|
|
HabitUnglossed, HabitAt,
|
|
}
|
|
|
|
// summaryFloor — the literal each key falls back to when the file is unusable.
|
|
// These are the exact strings that lived in Go before this file existed.
|
|
var summaryFloor = map[string]string{
|
|
PlanRestEmpty: "на сегодня больше ничего не запланировано.",
|
|
PlanDayEmpty: "на {date} ничего не запланировано.",
|
|
PlanDay: "план на {date}: {items}.",
|
|
PlanUncertain: "похоже, {line}",
|
|
|
|
TasksNone: "задач нет.",
|
|
TasksFirst: "сначала: {items}.",
|
|
TasksCandidates: "ещё я нашла, но ты не подтвердил: {items}.",
|
|
|
|
ReasonOverdue: "просрочено",
|
|
ReasonOverdueDay: "просрочено на день",
|
|
ReasonOverdueDays: "просрочено на {n} дн.",
|
|
ReasonToday: "сегодня",
|
|
ReasonTomorrow: "завтра",
|
|
ReasonInDays: "через {n} дн.",
|
|
ReasonImportant: "важно",
|
|
ReasonUrgent: "срочно",
|
|
ReasonStale: "давно в списке",
|
|
|
|
HabitWeekday: "по {day} ты обычно {items}.",
|
|
HabitWeekdaySame: "по {day} у тебя нет ничего особенного — то же, что и в остальные дни: {items}.",
|
|
HabitWeekdayNone: "по {day} я пока не вижу у тебя ничего постоянного.",
|
|
HabitWeekendBoth: "по субботам ты обычно {sat}, по воскресеньям — {sun}.",
|
|
HabitWeekendSat: "по субботам ты обычно {items}, а по воскресеньям ничего постоянного.",
|
|
HabitWeekendSun: "по воскресеньям ты обычно {items}, а по субботам ничего постоянного.",
|
|
HabitWeekendSame: "по выходным у тебя нет ничего особенного — то же, что и в остальные дни: {items}.",
|
|
HabitWeekendNone: "по выходным я пока не вижу у тебя ничего постоянного.",
|
|
HabitOverall: "обычно ты {items} — {span}.",
|
|
HabitOverallNone: "я ещё не набрала достаточно записей, чтобы говорить о привычках.",
|
|
HabitSpanToday: "по записям за сегодня",
|
|
HabitSpanDays: "по записям за последние {n} {word}",
|
|
HabitUnglossed: "отмечаешь «{key}»",
|
|
HabitAt: "{gloss} около {time}",
|
|
}
|
|
|
|
// Summaries picks a hand-written Russian summary sentence. Safe for concurrent
|
|
// use.
|
|
type Summaries struct{ d *Deck }
|
|
|
|
// LoadSummaries reads the embedded file. Pass a source to make the picking
|
|
// reproducible in tests; nil seeds from the clock.
|
|
func LoadSummaries(src rand.Source) (*Summaries, error) {
|
|
d, err := Load(summaryJSON, SummarySchemaVersion, summaryKeys, summaryFloor, src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The entries that exist to read the aggregate back. A variant without the
|
|
// placeholder would summarise the data by dropping it.
|
|
for _, req := range []struct{ key, ph string }{
|
|
{PlanDayEmpty, "{date}"}, {PlanDay, "{date}"}, {PlanDay, "{items}"},
|
|
{PlanUncertain, "{line}"},
|
|
{TasksFirst, "{items}"}, {TasksCandidates, "{items}"},
|
|
{ReasonOverdueDays, "{n}"}, {ReasonInDays, "{n}"},
|
|
{HabitWeekday, "{day}"}, {HabitWeekday, "{items}"},
|
|
{HabitWeekdaySame, "{day}"}, {HabitWeekdaySame, "{items}"},
|
|
{HabitWeekdayNone, "{day}"},
|
|
{HabitWeekendBoth, "{sat}"}, {HabitWeekendBoth, "{sun}"},
|
|
{HabitWeekendSat, "{items}"}, {HabitWeekendSun, "{items}"},
|
|
{HabitWeekendSame, "{items}"},
|
|
{HabitOverall, "{items}"}, {HabitOverall, "{span}"},
|
|
{HabitSpanDays, "{n}"}, {HabitSpanDays, "{word}"},
|
|
{HabitUnglossed, "{key}"}, {HabitAt, "{gloss}"}, {HabitAt, "{time}"},
|
|
} {
|
|
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &Summaries{d: d}, nil
|
|
}
|
|
|
|
// deck reads through a nil *Summaries, which is the unloadable-file case.
|
|
func (s *Summaries) deck() *Deck {
|
|
if s == nil {
|
|
return FloorDeck(summaryFloor)
|
|
}
|
|
return s.d
|
|
}
|
|
|
|
// Say returns one line for key, with the values filled into the frame.
|
|
func (s *Summaries) Say(key string, vars map[string]string) string {
|
|
return s.deck().Text(key, vars)
|
|
}
|
|
|
|
// Variants returns every line the file can produce, for the persona scorer.
|
|
func (s *Summaries) Variants() []string { return s.deck().Variants() }
|
|
|
|
var (
|
|
summaryOnce sync.Once
|
|
summaries *Summaries
|
|
)
|
|
|
|
// DefaultSummaries returns the shared instance, loading it on first use. A
|
|
// broken file logs once and leaves a nil *Summaries, which still answers from
|
|
// summaryFloor.
|
|
func DefaultSummaries() *Summaries {
|
|
summaryOnce.Do(func() {
|
|
s, err := LoadSummaries(nil)
|
|
if err != nil {
|
|
log.Printf("say: summary lines unavailable, using the built-in ones: %v", err)
|
|
return
|
|
}
|
|
summaries = s
|
|
})
|
|
return summaries
|
|
}
|
|
|
|
// S — one summary sentence, the way every caller says it.
|
|
func S(key string, vars map[string]string) string { return DefaultSummaries().Say(key, vars) }
|
|
|
|
// IsS reports whether text is a line key could have produced, for the tests.
|
|
func IsS(key string, vars map[string]string, text string) bool {
|
|
return DefaultSummaries().deck().Matches(key, vars, text)
|
|
}
|