// Package main — ruwords.go holds Russian language + calendar/time formatting // helpers used by the voice reply paths (replySystem, the reminder/routine // phrasing, etc). Pure functions, no receivers: weekday/month name tables, // 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. package main import ( "fmt" "strconv" "strings" "time" ) var ruWeekdays = []string{ "воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота", } var ruMonths = []string{ "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря", } // 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 = "я знаю только местное время, про другие города пока не скажу." // notPlaceAfterV — words that follow "в" without naming a place, so // mentionsUnknownPlace does not mistake them for a city. var notPlaceAfterV = map[string]bool{ "данный": true, "данную": true, "этот": true, "эту": true, "котором": true, "какое": true, "какой": true, "который": true, "общем": true, "точности": true, "курсе": true, "сутках": true, "часах": true, "минутах": true, "секундах": true, "неделе": true, } // mentionsUnknownPlace reports whether the question has a "в <слово>" phrase // that looks like a place we do not know ("который час в киеве"). Used only to // pick the honest "local time only" reply instead of answering local time as // if it were the city's. func mentionsUnknownPlace(u string) bool { toks := strings.Fields(u) for i := 0; i+1 < len(toks); i++ { if toks[i] != "в" && toks[i] != "во" { continue } next := strings.Trim(toks[i+1], ".,?!") if next == "" || notPlaceAfterV[next] { continue } // A number after "в" is a clock ("в 5 часов"), not a place. if _, err := strconv.Atoi(strings.SplitN(next, ":", 2)[0]); err == nil { continue } return true } return false } // onlyNearDaysReply — she can work out today, tomorrow, the day after and // yesterday, and nothing further. Said out loud instead of answering today's // date for a day she did not understand. const onlyNearDaysReply = "я считаю только сегодня, завтра, послезавтра и вчера — про другие дни пока не скажу." // dayWords — day references the calendar parser cannot resolve. A weekday name // or a "через …" phrase means he asked about a specific other day. var dayWords = []string{ "понедельник", "вторник", "сред", "четверг", "пятниц", "суббот", "воскресен", "через", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", } // mentionsUnknownDay reports whether the question names a day the calendar // parser could not resolve. Mirror of mentionsUnknownPlace: it exists only to // pick an honest reply over a confidently wrong one. // // Only called after ParseCalendarDate has already failed, so "завтра" and the // other words it does know never reach here. func mentionsUnknownDay(u string) bool { for _, w := range dayWords { if strings.Contains(u, w) { return true } } return false } // 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, "час", "часа", "часов") if m == 0 { return fmt.Sprintf("%d %s ровно", h, hourWord) } return fmt.Sprintf("%d %s %d %s", h, hourWord, m, ruPlural(m, "минута", "минуты", "минут")) } // dayPrefix names the day relative to now ("завтра", "вчера", …) so the date // reply opens the way a person would say it. func dayPrefix(now, day time.Time) string { base := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) switch int(day.Sub(base).Hours() / 24) { case -1: return "вчера" case 0: return "сегодня" case 1: return "завтра" case 2: return "послезавтра" } 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. func hasDurationWords(u string) bool { s := strings.ToLower(strings.TrimSpace(u)) // First-word duration markers (same keywords as timeQueryBuild in stage0). first := strings.Fields(s) if len(first) > 0 { switch first[0] { case "прошло", "осталось", "пройдет", "минуло", "проходит": return true } } // Broader duration keywords appearing anywhere in the utterance. if strings.Contains(s, "прошло") || strings.Contains(s, "осталось") { return true } if strings.Contains(s, " до ") { return true } return false } // formatTime returns a human-readable Russian time string for a fact timestamp. // Used by the query handler when answering "когда я это сделал?"-style questions. func formatTime(t time.Time) string { now := time.Now() if t.After(now.Add(-2*time.Minute)) && t.Before(now.Add(2*time.Minute)) { return "только что" } diff := now.Sub(t) switch { case diff < 10*time.Minute: return "несколько минут назад" case diff < 60*time.Minute: return fmt.Sprintf("%d минут назад", int(diff.Minutes())) case diff < 2*time.Hour: return "час назад" case diff < 24*time.Hour: return fmt.Sprintf("%d часа назад", int(diff.Hours())) default: return t.Format("2 января 15:04") } }