say: one count rule everywhere, and a page she can explain (V-521)
The PR 113 review found four defects in one line file. Swept the other four families and the Go side for the same four. The JSON was clean: no undeclared placeholder, no abbreviation spoken, no single-variant entry left unfixed. One register leak — page_blocked read "robots.txt" out loud, which is a filename, not a reason he can act on. The count rule was not clean. Four more copies of the three-way agreement existed and two of them were wrong: ruPlural produced «1 минут назад» and «5 часа назад» because formatTime spelled the noun out. pluralTasksRU was a fifth copy. All of them now call say.CountWord. The pending-notification summary picks the whole phrase, because the adjective declines with the noun.
This commit is contained in:
@@ -165,16 +165,17 @@ func TestQueryWebRefusesNonHTML(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// robots.txt is honoured on the answer path too, and she says so instead of
|
||||
// reporting a generic failure.
|
||||
// robots.txt is honoured on the answer path too, and she says the page is
|
||||
// closed instead of reporting a generic failure.
|
||||
func TestQueryWebObeysRobots(t *testing.T) {
|
||||
h := buildWebHandler(crawl.New(&robotsDenyFetcher{}, crawl.Config{}))
|
||||
reply, ok := askWeb(h, "посмотри https://example.org/private")
|
||||
if !ok {
|
||||
t.Fatal("the web source did not claim a question with a URL")
|
||||
}
|
||||
if !strings.Contains(reply, "robots.txt") {
|
||||
t.Errorf("reply = %q, want the robots answer", reply)
|
||||
// She names the cause without reading a filename out loud.
|
||||
if !strings.Contains(reply, "закрыта для чтения") || strings.Contains(reply, "robots") {
|
||||
t.Errorf("reply = %q, want the closed-page answer with no filename", reply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-20
@@ -4,6 +4,10 @@
|
||||
// plural agreement, clock/date rendering, and the "do I actually know this
|
||||
// place/day" guards that pick an honest reply over a confidently wrong one.
|
||||
// Extend this file rather than voice.go for anything in that shape.
|
||||
//
|
||||
// Count agreement is not here. It is say.CountWord, because there were four
|
||||
// copies of the same three-way rule and two of the sites that needed it were
|
||||
// spelling one form out (Vikunja #521).
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -11,6 +15,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
var ruWeekdays = []string{
|
||||
@@ -94,11 +100,11 @@ func mentionsUnknownDay(u string) bool {
|
||||
// ruClock renders the clock part of the time reply: "15 часов 4 минуты".
|
||||
func ruClock(t time.Time) string {
|
||||
h, m := t.Hour(), t.Minute()
|
||||
hourWord := ruPlural(h, "час", "часа", "часов")
|
||||
hourWord := say.CountWord(h, "час", "часа", "часов")
|
||||
if m == 0 {
|
||||
return fmt.Sprintf("%d %s ровно", h, hourWord)
|
||||
}
|
||||
return fmt.Sprintf("%d %s %d %s", h, hourWord, m, ruPlural(m, "минута", "минуты", "минут"))
|
||||
return fmt.Sprintf("%d %s %d %s", h, hourWord, m, say.CountWord(m, "минута", "минуты", "минут"))
|
||||
}
|
||||
|
||||
// dayPrefix names the day relative to now ("завтра", "вчера", …) so the date
|
||||
@@ -118,22 +124,6 @@ func dayPrefix(now, day time.Time) string {
|
||||
return "это"
|
||||
}
|
||||
|
||||
func ruPlural(n int, one, two, many string) string {
|
||||
n = n % 100
|
||||
if n > 10 && n < 20 {
|
||||
return many
|
||||
}
|
||||
n = n % 10
|
||||
switch n {
|
||||
case 1:
|
||||
return one
|
||||
case 2, 3, 4:
|
||||
return two
|
||||
default:
|
||||
return many
|
||||
}
|
||||
}
|
||||
|
||||
// hasDurationWords checks whether u is asking about elapsed/remaining time
|
||||
// rather than the current clock — guards replySystem from replying "сейчас
|
||||
// X часов" to "сколько времени прошло". Mirrors the stage0.go build filter.
|
||||
@@ -169,11 +159,13 @@ func formatTime(t time.Time) string {
|
||||
case diff < 10*time.Minute:
|
||||
return "несколько минут назад"
|
||||
case diff < 60*time.Minute:
|
||||
return fmt.Sprintf("%d минут назад", int(diff.Minutes()))
|
||||
n := int(diff.Minutes())
|
||||
return fmt.Sprintf("%d %s назад", n, say.CountWord(n, "минуту", "минуты", "минут"))
|
||||
case diff < 2*time.Hour:
|
||||
return "час назад"
|
||||
case diff < 24*time.Hour:
|
||||
return fmt.Sprintf("%d часа назад", int(diff.Hours()))
|
||||
n := int(diff.Hours())
|
||||
return fmt.Sprintf("%d %s назад", n, say.CountWord(n, "час", "часа", "часов"))
|
||||
default:
|
||||
return t.Format("2 января 15:04")
|
||||
}
|
||||
|
||||
+6
-1
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/kami/maven/internal/pattern"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/routine"
|
||||
"github.com/kami/maven/internal/say"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
@@ -627,7 +628,11 @@ func (t *tickLoop) maybeDrainDigest(ctx context.Context, state loop.State, now t
|
||||
fmt.Fprintf(&b, " · и ещё %d", extra)
|
||||
}
|
||||
body := b.String()
|
||||
summary := fmt.Sprintf("%d отложенных уведомлений", len(entries))
|
||||
// The adjective declines with the noun, so the count picks the whole
|
||||
// phrase: 1 отложенное уведомление, 2 отложенных уведомления, 5
|
||||
// отложенных уведомлений.
|
||||
summary := fmt.Sprintf("%d %s", len(entries), say.CountWord(len(entries),
|
||||
"отложенное уведомление", "отложенных уведомления", "отложенных уведомлений"))
|
||||
|
||||
cand := loop.Candidate{
|
||||
Rule: loop.Rule{Name: "digest", Severity: loop.Severity(maxSev)},
|
||||
|
||||
@@ -79,7 +79,7 @@ var queryFloor = map[string]string{
|
||||
QueryFactValue: "у меня записано: {key} — {value}",
|
||||
QueryFound: "вот что я нашла: {text}",
|
||||
QueryPageText: "вот что на странице: {text}",
|
||||
QueryPageBlocked: "эта страница закрыта для чтения — robots.txt не разрешает.",
|
||||
QueryPageBlocked: "эта страница закрыта для чтения — сам сайт это запрещает.",
|
||||
QueryPageEmpty: "страница открылась, но читать там нечего.",
|
||||
QueryFeedsOff: "ленты не настроены.",
|
||||
QueryFeedsNew: "вот что нового: {items}",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"page_blocked": {
|
||||
"fixed": true,
|
||||
"variants": ["эта страница закрыта для чтения — robots.txt не разрешает."]
|
||||
"variants": ["эта страница закрыта для чтения — сам сайт это запрещает."]
|
||||
},
|
||||
"page_empty": {
|
||||
"fixed": true,
|
||||
|
||||
+1
-15
@@ -263,21 +263,7 @@ func joinRU(rs []Ranked, limit int, withReasons bool) string {
|
||||
s := strings.Join(parts, "; ")
|
||||
if rest > 0 {
|
||||
// With the noun. Spoken, a bare number trails off mid-sentence.
|
||||
s += fmt.Sprintf("; и ещё %d %s", rest, pluralTasksRU(rest))
|
||||
s += fmt.Sprintf("; и ещё %d %s", rest, say.CountWord(rest, "задача", "задачи", "задач"))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// pluralTasksRU — the right form of "задача" for a count. Russian needs three.
|
||||
func pluralTasksRU(n int) string {
|
||||
if n%100 >= 11 && n%100 <= 14 {
|
||||
return "задач"
|
||||
}
|
||||
switch n % 10 {
|
||||
case 1:
|
||||
return "задача"
|
||||
case 2, 3, 4:
|
||||
return "задачи"
|
||||
}
|
||||
return "задач"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
func at(y int, m time.Month, d int) *time.Time {
|
||||
@@ -240,8 +242,8 @@ func TestFormatRUTailCarriesTheNoun(t *testing.T) {
|
||||
t.Errorf("reply = %q, want the count with its noun", got)
|
||||
}
|
||||
for n, want := range map[int]string{1: "задача", 2: "задачи", 5: "задач", 11: "задач", 21: "задача"} {
|
||||
if got := pluralTasksRU(n); got != want {
|
||||
t.Errorf("pluralTasksRU(%d) = %q, want %q", n, got, want)
|
||||
if got := say.CountWord(n, "задача", "задачи", "задач"); got != want {
|
||||
t.Errorf("CountWord(%d) = %q, want %q", n, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user