diff --git a/internal/persona/persona.go b/internal/persona/persona.go index d3928bb..b15e7f9 100644 --- a/internal/persona/persona.go +++ b/internal/persona/persona.go @@ -17,6 +17,8 @@ import ( "fmt" "strings" "time" + + "github.com/kami/maven/internal/lexicon" ) // Facts — the optional, deployment-specific half of the block. All fields may @@ -34,12 +36,10 @@ type Facts struct { Tools bool // at least one shell act is on the allowlist } -var ruWeekdays = [...]string{"воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"} - -var ruMonths = [...]string{ - "января", "февраля", "марта", "апреля", "мая", "июня", - "июля", "августа", "сентября", "октября", "ноября", "декабря", -} +// The weekday and month names are closed classes and live in internal/lexicon, +// which indexes weekdays from Sunday the way time.Weekday does and months from +// one. This file used to carry its own copies, making four copies of the twelve +// months in the tree after cmd/mavend/ruwords.go gave up its own (Vikunja #525). // Block renders the context block for one turn. Russian even in front of the // English prompts: the rules it states are Russian grammar (ты/тебя, feminine @@ -61,7 +61,7 @@ func (f Facts) Block(now time.Time) string { } b.WriteString(fmt.Sprintf("Сейчас: %s, %d %s %d, %02d:%02d (местное время).\n", - ruWeekdays[int(now.Weekday())], now.Day(), ruMonths[int(now.Month())-1], now.Year(), + lexicon.Weekday(int(now.Weekday())), now.Day(), lexicon.MonthGenitive(int(now.Month())), now.Year(), now.Hour(), now.Minute())) b.WriteString("Умеешь: " + strings.Join(f.can(), "; ") + diff --git a/internal/say/summary.go b/internal/say/summary.go index 0ae0cfa..bc2b575 100644 --- a/internal/say/summary.go +++ b/internal/say/summary.go @@ -141,7 +141,8 @@ func LoadSummaries(src rand.Source) (*Summaries, error) { {PlanUncertain, "{line}"}, {TasksFirst, "{items}"}, {TasksCandidates, "{items}"}, {StallOverdue, "{n}"}, {StallOverdue, "{word}"}, - {StallSitting, "{n}"}, {StallSitting, "{days}"}, + {StallSitting, "{n}"}, {StallSitting, "{word}"}, + {StallSitting, "{days}"}, {StallSitting, "{dayword}"}, {StallUnconfirmed, "{n}"}, {StallUnconfirmed, "{word}"}, {ReasonOverdueDays, "{n}"}, {ReasonOverdueDays, "{word}"}, {ReasonInDays, "{n}"}, {ReasonInDays, "{word}"}, diff --git a/internal/ttsnorm/ttsnorm.go b/internal/ttsnorm/ttsnorm.go index 5fb37a1..ab97c7e 100644 --- a/internal/ttsnorm/ttsnorm.go +++ b/internal/ttsnorm/ttsnorm.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/kami/maven/internal/lexicon" + "github.com/kami/maven/internal/say" ) // The month names are a closed class and live in internal/lexicon, 1-indexed, @@ -32,7 +33,7 @@ func Speakable(s string) string { }) s = reTime.ReplaceAllStringFunc(s, func(m string) string { p := reTime.FindStringSubmatch(m) - return p[1] + " часов " + p[2] + " минут" + return spokenTime(mustInt(p[1]), mustInt(p[2])) }) s = reDots.ReplaceAllStringFunc(s, func(m string) string { return strings.Join(strings.Split(m, "."), " точка ") @@ -46,6 +47,20 @@ func Speakable(s string) string { return Pronounce(s) } +// spokenTime reads a clock time the way it is said rather than the way it is +// written. Two things the written form gets wrong out loud. The noun after a +// numeral inflects, so 21:00 is "час" and 22:00 is "часа", where the old +// rewrite said "часов" for every hour and "минут" for every minute. And a +// leading zero is punctuation, not a word: 14:00 is "14 часов" and 9:05 is +// "9 часов 5 минут", never "00 минут" or "05 минут". +func spokenTime(h, m int) string { + out := strconv.Itoa(h) + " " + say.CountWord(h, "час", "часа", "часов") + if m == 0 { + return out + } + return out + " " + strconv.Itoa(m) + " " + say.CountWord(m, "минута", "минуты", "минут") +} + func spokenDate(dd, mm, yyyy string) string { mi, _ := strconv.Atoi(mm) if mi < 1 || mi > 12 { diff --git a/internal/ttsnorm/ttsnorm_test.go b/internal/ttsnorm/ttsnorm_test.go index 1d4206a..3d938ad 100644 --- a/internal/ttsnorm/ttsnorm_test.go +++ b/internal/ttsnorm/ttsnorm_test.go @@ -6,8 +6,11 @@ func TestSpeakable(t *testing.T) { cases := []struct{ in, want string }{ {"напомню 10.07.2026", "напомню 10 июля 2026"}, {"срок 01.01", "срок 1 января"}, - {"встреча в 14:00", "встреча в 14 часов 00 минут"}, - {"в 9:05 подъём", "в 9 часов 05 минут подъём"}, + {"встреча в 14:00", "встреча в 14 часов"}, + {"в 9:05 подъём", "в 9 часов 5 минут подъём"}, + {"в 21:00 отбой", "в 21 час отбой"}, + {"в 22:02 отбой", "в 22 часа 2 минуты отбой"}, + {"в 1:01 проснулся", "в 1 час 1 минута проснулся"}, {"это 3.2.1 версия", "это 3 точка 2 точка 1 версия"}, {"без чисел", "без чисел"}, }