Drop the city time-zone table

The user only ever asks the time in his own zone, so answering other
cities was code kept in step with the weather city list for no gain.
Any named place now gets the honest "local time only" answer that was
already there for unknown cities.

Removes the 22-entry table, the lookup and the embedded tz database.
Closes Vikunja #389 — there is only one city list again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
kami
2026-07-31 14:14:18 +04:00
parent 84ba217892
commit 3dbf67f8f9
2 changed files with 14 additions and 67 deletions
+5 -6
View File
@@ -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
View File
@@ -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
}