diff --git a/cmd/mavend/netscan.go b/cmd/mavend/netscan.go index c5de502..88d28f1 100644 --- a/cmd/mavend/netscan.go +++ b/cmd/mavend/netscan.go @@ -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 diff --git a/cmd/mavend/netscan_test.go b/cmd/mavend/netscan_test.go index df87124..eb10526 100644 --- a/cmd/mavend/netscan_test.go +++ b/cmd/mavend/netscan_test.go @@ -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{ "какие устройства в сети?", diff --git a/cmd/mavend/smarthome.go b/cmd/mavend/smarthome.go index 03946b2..2e14f3a 100644 --- a/cmd/mavend/smarthome.go +++ b/cmd/mavend/smarthome.go @@ -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 } diff --git a/internal/phraser/plural.go b/internal/phraser/plural.go new file mode 100644 index 0000000..23ad704 --- /dev/null +++ b/internal/phraser/plural.go @@ -0,0 +1,47 @@ +package phraser + +// The counted noun, in the form the number in front of it demands. +// +// Russian inflects a noun after a numeral, and the form depends on the last two +// digits: 1 градус, 2 градуса, 5 градусов, 11 градусов, 21 градус, 22 градуса. +// A line file cannot spell that out, so a count in a template splits into two +// placeholders — the number, and {word} filled from here. +// +// The rule lived once as hostWord in cmd/mavend/netscan.go, which meant the +// weather line said "градусов" for every temperature and was wrong for 1-4 and +// for every number ending in 1-4. One helper, every count site (Vikunja #521). + +import "math" + +// CountWord picks between the three forms n needs: one for 1, few for 2-4, many +// for 0, 5-20 and anything ending in those. A negative count reads its own +// magnitude, since minus does not change the noun: -2 градуса. +func CountWord(n int, one, few, many string) string { + if n < 0 { + n = -n + } + if n%100 >= 11 && n%100 <= 14 { + return many + } + switch n % 10 { + case 1: + return one + case 2, 3, 4: + return few + default: + return many + } +} + +// Degrees — the noun for a temperature. Takes the reading as it arrives from a +// weather provider and counts by the whole degrees she is about to say, so the +// noun agrees with the number in the same sentence rather than with the reading +// behind it. +func Degrees(temp float64) string { + return CountWord(int(math.Round(temp)), "градус", "градуса", "градусов") +} + +// Devices — the noun for a count of hosts on the LAN or of smart-home devices. +func Devices(n int) string { + return CountWord(n, "устройство", "устройства", "устройств") +} diff --git a/internal/phraser/plural_test.go b/internal/phraser/plural_test.go new file mode 100644 index 0000000..cfd4fd2 --- /dev/null +++ b/internal/phraser/plural_test.go @@ -0,0 +1,32 @@ +package phraser + +import "testing" + +// The bug the helper exists for: the weather line said "градусов" for every +// reading, which is wrong for 1-4 and for every number ending in 1-4. +func TestDegreesAgreeWithTheReading(t *testing.T) { + for temp, want := range map[float64]string{ + 1: "градус", 1.4: "градус", 2: "градуса", 4: "градуса", 5: "градусов", + 0: "градусов", 11: "градусов", 14: "градусов", 21: "градус", + 22: "градуса", 25: "градусов", 101: "градус", + // Minus does not change the noun, and a reading rounds to the number + // she is about to say: -2.4° is "-2 градуса", not "-2 градусов". + -1: "градус", -2.4: "градуса", -5: "градусов", -11: "градусов", + } { + if got := Degrees(temp); got != want { + t.Errorf("Degrees(%v) = %q, want %q", temp, got, want) + } + } +} + +func TestDevicesAgreeWithTheCount(t *testing.T) { + for n, want := range map[int]string{ + 1: "устройство", 2: "устройства", 4: "устройства", 5: "устройств", + 11: "устройств", 12: "устройств", 21: "устройство", 22: "устройства", + 25: "устройств", 111: "устройств", 101: "устройство", 0: "устройств", + } { + if got := Devices(n); got != want { + t.Errorf("Devices(%d) = %q, want %q", n, got, want) + } + } +}