say, persona, ttsnorm: the clock is spoken and the months have one copy (V-581)
A clock time read aloud now inflects its nouns and drops its leading zeros.
The old rewrite said "часов" for every hour and "минут" for every minute, so
21:00 came out as "21 часов" and 14:00 as "14 часов 00 минут". Russian
inflects a noun after a numeral and internal/say already owns that rule, so
spokenTime calls say.CountWord for both halves and omits the minutes when
there are none. 21:00 is "21 час", 22:02 is "22 часа 2 минуты", 14:00 is
"14 часов".
internal/persona held its own copies of the twelve months and the seven
weekdays. Both are closed classes and both already live in internal/lexicon,
which is where cmd/mavend/ruwords.go sent its copy. The block now reads
lexicon.Weekday and lexicon.MonthGenitive and carries no word list of its own.
LoadSummaries asserted that stall_sitting keeps its two counts and not the two
count words beside them. A variant dropping {word} or {dayword} would have
loaded and spoken a bare number. Both are required now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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(), "; ") +
|
||||
|
||||
@@ -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}"},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 версия"},
|
||||
{"без чисел", "без чисел"},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user