Tonight's work as one branch: all 46 reviewed PRs, verified green #47
@@ -57,17 +57,16 @@ func TestReplySystemUnknownDayIsHonest(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestReplySystemClockCity — the clock arm must not answer local time for a
|
||||
// question about another city (Vikunja #388). Known cities get their own zone;
|
||||
// unknown places get an honest "local time only".
|
||||
// question about another city (Vikunja #388). She keeps one clock, so every
|
||||
// named place gets the honest "local time only" answer.
|
||||
func TestReplySystemClockCity(t *testing.T) {
|
||||
// 12:00 UTC — Kyiv is +03 in July, Moscow +03, London +01.
|
||||
now := time.Date(2026, 7, 30, 12, 0, 0, 0, time.UTC)
|
||||
h := systemHandler(now)
|
||||
cases := []struct{ utterance, want string }{
|
||||
{"который час", "сейчас 12 часов ровно"},
|
||||
{"который час в киеве", "в Киеве сейчас 15 часов ровно"},
|
||||
{"сколько времени в москве", "в Москве сейчас 15 часов ровно"},
|
||||
{"который час в лондоне", "в Лондоне сейчас 13 часов ровно"},
|
||||
{"который час в киеве", onlyLocalTimeReply},
|
||||
{"сколько времени в москве", onlyLocalTimeReply},
|
||||
{"который час в лондоне", onlyLocalTimeReply},
|
||||
{"который час в бишкеке", onlyLocalTimeReply},
|
||||
}
|
||||
for _, c := range cases {
|
||||
|
||||
+9
-61
@@ -55,9 +55,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
// Embeds the tz database in the binary so time.LoadLocation works even in
|
||||
// a container image without /usr/share/zoneinfo. Stdlib, offline.
|
||||
_ "time/tzdata"
|
||||
|
||||
hexisclient "github.com/kami/hexis/pkg/client"
|
||||
"github.com/kami/maven/internal/audio"
|
||||
@@ -941,56 +938,15 @@ var ruMonths = []string{
|
||||
"июля", "августа", "сентября", "октября", "ноября", "декабря",
|
||||
}
|
||||
|
||||
// onlyLocalTimeReply — the honest answer when the user names a place whose
|
||||
// time zone we cannot resolve offline. Better than confidently naming the
|
||||
// wrong city's time.
|
||||
// onlyLocalTimeReply — the honest answer when the user asks the time somewhere
|
||||
// other than here. She only keeps one clock, and saying so is better than
|
||||
// naming the wrong city's time.
|
||||
//
|
||||
// There used to be a city→time-zone table here. It was removed on purpose: the
|
||||
// user only ever asks for local time, so the table was a second list of cities
|
||||
// to keep in step with the weather one for no gain.
|
||||
const onlyLocalTimeReply = "я знаю только местное время, про другие города пока не скажу."
|
||||
|
||||
// cityZone — a city we can answer the clock for: its IANA time zone (resolved
|
||||
// from the tzdata built into the binary, never over the network) and its
|
||||
// Russian name in the "в ..." case.
|
||||
type cityZone struct {
|
||||
zone string
|
||||
prepositional string
|
||||
}
|
||||
|
||||
// cityZones maps a lowercase city stem to its zone. Stems, not full words, so
|
||||
// "в москве" / "москва" both hit. Keep in sync-ish with the weather city list.
|
||||
var cityZones = map[string]cityZone{
|
||||
"москв": {"Europe/Moscow", "Москве"},
|
||||
"moscow": {"Europe/Moscow", "Москве"},
|
||||
"питер": {"Europe/Moscow", "Питере"},
|
||||
"петербур": {"Europe/Moscow", "Петербурге"},
|
||||
"киев": {"Europe/Kyiv", "Киеве"},
|
||||
"kyiv": {"Europe/Kyiv", "Киеве"},
|
||||
"kiev": {"Europe/Kyiv", "Киеве"},
|
||||
"минск": {"Europe/Minsk", "Минске"},
|
||||
"лондон": {"Europe/London", "Лондоне"},
|
||||
"london": {"Europe/London", "Лондоне"},
|
||||
"париж": {"Europe/Paris", "Париже"},
|
||||
"paris": {"Europe/Paris", "Париже"},
|
||||
"берлин": {"Europe/Berlin", "Берлине"},
|
||||
"berlin": {"Europe/Berlin", "Берлине"},
|
||||
"нью-йорк": {"America/New_York", "Нью-Йорке"},
|
||||
"new york": {"America/New_York", "Нью-Йорке"},
|
||||
"токио": {"Asia/Tokyo", "Токио"},
|
||||
"tokyo": {"Asia/Tokyo", "Токио"},
|
||||
"тбилиси": {"Asia/Tbilisi", "Тбилиси"},
|
||||
"екатеринбург": {"Asia/Yekaterinburg", "Екатеринбурге"},
|
||||
"новосибирск": {"Asia/Novosibirsk", "Новосибирске"},
|
||||
"владивосток": {"Asia/Vladivostok", "Владивостоке"},
|
||||
}
|
||||
|
||||
// lookupCityZone finds a known city named in the utterance.
|
||||
func lookupCityZone(u string) (cityZone, bool) {
|
||||
for stem, cz := range cityZones {
|
||||
if strings.Contains(u, stem) {
|
||||
return cz, true
|
||||
}
|
||||
}
|
||||
return cityZone{}, false
|
||||
}
|
||||
|
||||
// notPlaceAfterV — words that follow "в" without naming a place, so
|
||||
// mentionsUnknownPlace does not mistake them for a city.
|
||||
var notPlaceAfterV = map[string]bool{
|
||||
@@ -1155,16 +1111,8 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision)
|
||||
|
||||
switch {
|
||||
case strings.Contains(u, "час") || strings.Contains(u, "врем"):
|
||||
// "который час в киеве" — answer for the named city when we know its
|
||||
// time zone locally, never guess. Unknown place: say so plainly.
|
||||
if city, ok := lookupCityZone(u); ok {
|
||||
loc, err := time.LoadLocation(city.zone)
|
||||
if err != nil {
|
||||
log.Printf("voice: load zone %s: %v", city.zone, err)
|
||||
return onlyLocalTimeReply
|
||||
}
|
||||
return fmt.Sprintf("в %s сейчас %s", city.prepositional, ruClock(now.In(loc)))
|
||||
}
|
||||
// "который час в киеве" — she keeps one clock, so any named place gets
|
||||
// the honest answer. Never local time dressed up as the city's.
|
||||
if mentionsUnknownPlace(u) {
|
||||
return onlyLocalTimeReply
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user