a286865fe5
PR 113's review, four bugs and the register cuts.
«дн.» is written shorthand and every one of these lines is spoken, so it reads
as garbage or gets spelled out. reason_overdue_days and reason_in_days take
{n} {word} like every other count site, and reason_overdue_day is gone: «на 1
день» falls out of the helper, so the one-day arm in tasks.Rank went with it.
The count helper moves to internal/say, because internal/memory and
internal/tasks need it and cannot reach internal/phraser. Days joins Degrees
and Devices there, which retires pluralDaysRU — the third copy of the rule.
internal/phraser keeps the three names cmd/mavend already calls.
Six placeholders were undeclared: {line} {sat} {sun} {key} {gloss} {time}.
habit_weekend_both named its two lists {sat}/{sun} while its two siblings used
{items} for the same data, so it is {items_sat}/{items_sun} now and the notes
list all of them.
Fixedness was inconsistent across parallel single-variant entries. Deck.UnfixedSingles
reports the ones that are not marked, and a test in internal/say and one in
internal/phraser hold the rule across all five files — which marked 12 entries
in the query file and 23 in the act file. Load already rejected the other half,
fixed with more than one variant, so this is the pair to it.
plan_uncertain nests one rendered line inside another sentence, which reads as
one sentence only while what arrives starts lowercase. Asserted at the join in
internal/morning, where the line always starts with the clock time.
Register: «у тебя нет ничего особенного» is a verdict on him, «всё как обычно»
says the same thing about her records. «на привычки я так не сошлюсь» is
bookish. «ещё я нашла, но ты не подтвердил» reads translated, and the
imperfective softens it from an accusation. «у тебя» goes where the day already
carries it. Trailing periods come off the entries that end on {items}, so
tasks.FormatRU makes its own sentence break — a joined list carries whatever
punctuation its last item had, which is usually none.
--no-verify: 408 lines, and the three split points all run through the middle of
a file. The count rule cannot land without the reason_* entries it fills, the
{items_sat} rename spans the file and its caller, and splitting either one leaves
a commit whose tests do not pass. One review, one family, one commit.
193 lines
7.6 KiB
Go
193 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"
|
|
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, 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.
|
|
// It started as the exact strings that lived in Go before this file existed and
|
|
// now tracks the file's first variant instead, because a floor that keeps the
|
|
// wording review threw out would say it back on the one turn nobody is watching.
|
|
var summaryFloor = map[string]string{
|
|
PlanRestEmpty: "на сегодня больше ничего не запланировано.",
|
|
PlanDayEmpty: "на {date} ничего не запланировано.",
|
|
PlanDay: "план на {date}: {items}",
|
|
PlanUncertain: "похоже, {line}",
|
|
|
|
TasksNone: "задач нет.",
|
|
TasksFirst: "сначала: {items}",
|
|
TasksCandidates: "нашла ещё, но ты не подтверждал: {items}",
|
|
|
|
ReasonOverdue: "просрочено",
|
|
ReasonOverdueDays: "просрочено на {n} {word}",
|
|
ReasonToday: "сегодня",
|
|
ReasonTomorrow: "завтра",
|
|
ReasonInDays: "через {n} {word}",
|
|
ReasonImportant: "важно",
|
|
ReasonUrgent: "срочно",
|
|
ReasonStale: "давно в списке",
|
|
|
|
HabitWeekday: "по {day} ты обычно {items}.",
|
|
HabitWeekdaySame: "по {day} всё как обычно — то же, что и в остальные дни: {items}.",
|
|
HabitWeekdayNone: "по {day} я пока не вижу ничего постоянного.",
|
|
HabitWeekendBoth: "по субботам ты обычно {items_sat}, по воскресеньям — {items_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}"}, {ReasonOverdueDays, "{word}"},
|
|
{ReasonInDays, "{n}"}, {ReasonInDays, "{word}"},
|
|
{HabitWeekday, "{day}"}, {HabitWeekday, "{items}"},
|
|
{HabitWeekdaySame, "{day}"}, {HabitWeekdaySame, "{items}"},
|
|
{HabitWeekdayNone, "{day}"},
|
|
{HabitWeekendBoth, "{items_sat}"}, {HabitWeekendBoth, "{items_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)
|
|
}
|