// Package ttsnorm rewrites machine-formatted dates/times/numbers into RU text // a TTS voice speaks naturally — so "10.07.2026" is not read as "number dot // number dot number". Pure, deterministic; runs on reply/nudge text before synth. package ttsnorm import ( "regexp" "strconv" "strings" ) var months = [...]string{"", "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"} var ( reDateY = regexp.MustCompile(`\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b`) reDate = regexp.MustCompile(`\b(\d{1,2})\.(\d{1,2})\b`) reTime = regexp.MustCompile(`\b(\d{1,2}):(\d{2})\b`) reDots = regexp.MustCompile(`\b\d+(?:\.\d+){2,}\b`) ) // Speakable rewrites d.m.y, d.m, h:mm, and residual dotted-number runs. // Order matters: dates with year first, then times, then multi-dot numbers // (3+ parts — never valid dates), then 2-part dates. func Speakable(s string) string { s = reDateY.ReplaceAllStringFunc(s, func(m string) string { p := reDateY.FindStringSubmatch(m) return spokenDate(p[1], p[2], p[3]) }) s = reTime.ReplaceAllStringFunc(s, func(m string) string { p := reTime.FindStringSubmatch(m) return p[1] + " часов " + p[2] + " минут" }) s = reDots.ReplaceAllStringFunc(s, func(m string) string { return strings.Join(strings.Split(m, "."), " точка ") }) s = reDate.ReplaceAllStringFunc(s, func(m string) string { p := reDate.FindStringSubmatch(m) return spokenDate(p[1], p[2], "") }) return s } func spokenDate(dd, mm, yyyy string) string { mi, _ := strconv.Atoi(mm) if mi < 1 || mi > 12 { return dd + " " + mm + gap(yyyy) } day := strconv.Itoa(mustInt(dd)) out := day + " " + months[mi] if yyyy != "" { out += " " + yyyy } return out } func mustInt(s string) int { n, _ := strconv.Atoi(s); return n } func gap(y string) string { if y == "" { return "" }; return " " + y }