phraser: one count helper, so the weather says "1 градус" (V-521)

The weather line spelled "градусов" out in the template, which is the wrong
form for 1-4 and for every number ending in 1-4. Russian inflects the noun
after a numeral, so the count splits into the number and {word}.

hostWord in cmd/mavend/netscan.go already knew the rule for устройство and was
the only place that did. It moves to internal/phraser as CountWord, with
Degrees and Devices over it, and the three call sites that counted devices now
read the same helper the weather line does. Degrees rounds before it counts, so
the noun agrees with the number she is about to say rather than the reading
behind it, and a negative reading counts by its magnitude.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
This commit is contained in:
2026-08-04 15:37:54 +04:00
parent 16d94894b7
commit d79b30a1a6
5 changed files with 84 additions and 32 deletions
+2 -18
View File
@@ -119,7 +119,7 @@ func (w *netWiring) scanSummary(ctx context.Context) (string, bool) {
if len(res.Hosts) == 0 {
return phraser.Q(phraser.QueryNetEmpty, map[string]string{"tail": tail}), true
}
out := fmt.Sprintf("нашла %d %s", len(res.Hosts), hostWord(len(res.Hosts)))
out := fmt.Sprintf("нашла %d %s", len(res.Hosts), phraser.Devices(len(res.Hosts)))
if shape := scanShape(res.Hosts); shape != "" {
out += ", " + shape
}
@@ -180,7 +180,7 @@ func (w *netWiring) writeScanRecord(ctx context.Context, res netscan.Result) {
if w.api == nil {
return
}
head := fmt.Sprintf("сканирование сети: %d %s", len(res.Hosts), hostWord(len(res.Hosts)))
head := fmt.Sprintf("сканирование сети: %d %s", len(res.Hosts), phraser.Devices(len(res.Hosts)))
if res.Truncated {
head += " (не вся сеть)"
}
@@ -211,22 +211,6 @@ func (w *netWiring) writeScanRecord(ctx context.Context, res netscan.Result) {
}
}
// hostWord — Russian counts inflect the noun: 1 устройство, 2-4 устройства,
// 5+ устройств, and the teens are all the last form.
func hostWord(n int) string {
if n%100 >= 11 && n%100 <= 14 {
return "устройств"
}
switch n % 10 {
case 1:
return "устройство"
case 2, 3, 4:
return "устройства"
default:
return "устройств"
}
}
// isNetworkQuery recognises a question about the LAN, narrowly. It needs a
// network word AND an ask: "интернет не работает" is a complaint, not a request
// to scan, and a scan she runs unasked is exactly the noisy behaviour the
-12
View File
@@ -74,18 +74,6 @@ func TestScanSummaryOnAnEmptyRange(t *testing.T) {
}
}
func TestHostWordAgreesWithTheCount(t *testing.T) {
for n, want := range map[int]string{
1: "устройство", 2: "устройства", 4: "устройства", 5: "устройств",
11: "устройств", 12: "устройств", 21: "устройство", 22: "устройства",
25: "устройств", 111: "устройств", 101: "устройство", 0: "устройств",
} {
if got := hostWord(n); got != want {
t.Errorf("hostWord(%d) = %q, want %q", n, got, want)
}
}
}
func TestIsNetworkQuery(t *testing.T) {
yes := []string{
"какие устройства в сети?",
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"time"
"github.com/kami/maven/internal/config"
"github.com/kami/maven/internal/phraser"
"github.com/kami/maven/internal/smarthome"
"github.com/kami/maven/internal/store"
)
@@ -185,7 +186,7 @@ func (w *homeWiring) homeSummary(ctx context.Context) (string, bool) {
case dark > 0 && len(sensors) == 0:
// Nothing is on and everything she can see is unreachable. "всё
// выключено" would be a claim about the house she cannot make.
return fmt.Sprintf("дом молчит: %d %s не отвечают.", dark, hostWord(dark)), true
return fmt.Sprintf("дом молчит: %d %s не отвечают.", dark, phraser.Devices(dark)), true
default:
parts = append(parts, "всё выключено")
}
@@ -193,7 +194,7 @@ func (w *homeWiring) homeSummary(ctx context.Context) (string, bool) {
parts = append(parts, strings.Join(sensors, ", "))
}
if dark > 0 {
parts = append(parts, fmt.Sprintf("%d %s не отвечают", dark, hostWord(dark)))
parts = append(parts, fmt.Sprintf("%d %s не отвечают", dark, phraser.Devices(dark)))
}
return strings.Join(parts, "; ") + ".", true
}