Files
tiennm99bot/internal/modules/weather/format.go
T
tiennm99 053fd07e5a feat(weather)!: add /thuyvan flood alerts for Tân Thuận
/thuyvan shows the 5-day tide-peak forecast at Phú An and Nhà Bè from
the Đài KTTV Nam Bộ bulletin PDF, the Open-Meteo rain forecast, and live
VNDMS river gauges within 30 km. /thuyvan_subscribe opts a chat into a
10:30 ICT push, retried at 12:30, sent only when a forecast peak reaches
báo động I (1.40 m) or a day's rain reaches 50 mm. A missing or stale
bulletin fails the push instead of reading as no risk.

The thoitiet module is renamed to weather; the /thoitiet* commands keep
their names.

BREAKING CHANGE: the module key is now "weather". A MODULES list that
names thoitiet fails at startup and must name weather instead.
2026-10-02 13:06:07 +07:00

175 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package weather
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
// sourceLine credits the data source, as Open-Meteo's CC BY 4.0 licence asks.
const sourceLine = "Nguồn: Open-Meteo"
type condition struct {
label string
emoji string
}
// wmoConditions maps WMO weather interpretation codes, the "weather_code"
// field Open-Meteo returns, to a Vietnamese label.
var wmoConditions = map[int]condition{
0: {"Trời quang", "☀️"},
1: {"Ít mây", "🌤️"},
2: {"Mây rải rác", "⛅"},
3: {"Nhiều mây", "☁️"},
45: {"Sương mù", "🌫️"},
48: {"Sương mù đóng băng", "🌫️"},
51: {"Mưa phùn nhẹ", "🌦️"},
53: {"Mưa phùn", "🌦️"},
55: {"Mưa phùn dày", "🌧️"},
56: {"Mưa phùn băng nhẹ", "🌧️"},
57: {"Mưa phùn băng", "🌧️"},
61: {"Mưa nhẹ", "🌧️"},
63: {"Mưa vừa", "🌧️"},
65: {"Mưa to", "🌧️"},
66: {"Mưa băng nhẹ", "🌧️"},
67: {"Mưa băng", "🌧️"},
71: {"Tuyết nhẹ", "🌨️"},
73: {"Tuyết vừa", "🌨️"},
75: {"Tuyết dày", "❄️"},
77: {"Tuyết hạt", "🌨️"},
80: {"Mưa rào nhẹ", "🌦️"},
81: {"Mưa rào", "🌦️"},
82: {"Mưa rào rất to", "⛈️"},
85: {"Mưa tuyết nhẹ", "🌨️"},
86: {"Mưa tuyết", "🌨️"},
95: {"Dông", "⛈️"},
96: {"Dông kèm mưa đá nhẹ", "⛈️"},
99: {"Dông kèm mưa đá", "⛈️"},
}
func conditionFor(code int) condition {
if c, ok := wmoConditions[code]; ok {
return c
}
return condition{"Không rõ", "🌡️"}
}
// vnWeekdays are the short Vietnamese weekday names, indexed by time.Weekday.
var vnWeekdays = [...]string{"CN", "T2", "T3", "T4", "T5", "T6", "T7"}
// hourlyViewHours is how many upcoming hours /thoitiet lists.
const hourlyViewHours = 6
// formatHourly renders current conditions followed by one block per upcoming
// hour.
func formatHourly(p place, f forecast) string {
c := f.Current
cur := conditionFor(c.WeatherCode)
h := f.Hourly
idx := f.upcomingHours(hourlyViewHours)
var sb strings.Builder
fmt.Fprintf(&sb, "🕐 Thời tiết %d giờ tới — %s\n", len(idx), displayName(p))
fmt.Fprintf(&sb, "Hiện tại %s: %s (cảm giác %s), %s %s\n", clock(c.Time), temp(c.Temperature),
temp(c.ApparentTemperature), cur.label, cur.emoji)
for _, i := range idx {
hc := conditionFor(h.WeatherCode[i])
fmt.Fprintf(&sb, "\n%s %s %s, %s (cảm giác %s)\n", clock(h.Time[i]), hc.emoji, hc.label,
temp(h.Temperature[i]), temp(h.ApparentTemperature[i]))
fmt.Fprintf(&sb, "Mưa %d%% (%s mm), độ ẩm %d%%, gió %s km/h\n", round(h.PrecipitationProbability[i]),
decimal(h.Precipitation[i]), round(h.Humidity[i]), decimal(h.WindSpeed[i]))
}
sb.WriteString("\n" + sourceLine)
return sb.String()
}
// formatToday renders current conditions plus today's daily summary.
func formatToday(p place, f forecast) string {
c := f.Current
cur := conditionFor(c.WeatherCode)
var sb strings.Builder
fmt.Fprintf(&sb, "%s Thời tiết hôm nay %s — %s\n", conditionFor(f.Daily.WeatherCode[0]).emoji,
shortDate(f.Daily.Time[0]), displayName(p))
fmt.Fprintf(&sb, "Hiện tại: %s (cảm giác %s), %s %s\n", temp(c.Temperature), temp(c.ApparentTemperature),
cur.label, cur.emoji)
fmt.Fprintf(&sb, "Độ ẩm %d%%, gió %s km/h\n", round(c.Humidity), decimal(c.WindSpeed))
writeDay(&sb, "Cả ngày", f.Daily, 0)
sb.WriteString(sourceLine)
return sb.String()
}
// formatTomorrow renders tomorrow's daily summary.
func formatTomorrow(p place, f forecast) string {
d := f.Daily
var sb strings.Builder
fmt.Fprintf(&sb, "%s Thời tiết ngày mai %s %s — %s\n", conditionFor(d.WeatherCode[1]).emoji,
weekday(d.Time[1]), shortDate(d.Time[1]), displayName(p))
writeDay(&sb, "Dự báo", d, 1)
sb.WriteString(sourceLine)
return sb.String()
}
// formatWeek renders one line per day for every day in the forecast.
func formatWeek(p place, f forecast) string {
d := f.Daily
n := d.days()
var sb strings.Builder
fmt.Fprintf(&sb, "📅 Thời tiết %d ngày tới — %s\n", n, displayName(p))
for i := range n {
c := conditionFor(d.WeatherCode[i])
fmt.Fprintf(&sb, "%s %s: %s–%s %s %s, mưa %d%%\n", weekday(d.Time[i]), shortDate(d.Time[i]),
tempValue(d.TemperatureMin[i]), temp(d.TemperatureMax[i]), c.emoji, c.label,
round(d.PrecipitationProbability[i]))
}
sb.WriteString(sourceLine)
return sb.String()
}
// writeDay renders the daily lines shared by the today and tomorrow views.
func writeDay(sb *strings.Builder, label string, d dailyWeather, i int) {
c := conditionFor(d.WeatherCode[i])
fmt.Fprintf(sb, "%s: %s–%s, %s %s\n", label, tempValue(d.TemperatureMin[i]), temp(d.TemperatureMax[i]),
c.label, c.emoji)
fmt.Fprintf(sb, "Khả năng mưa %d%% (%s mm), UV %s\n", round(d.PrecipitationProbability[i]),
decimal(d.PrecipitationSum[i]), decimal(d.UVIndexMax[i]))
fmt.Fprintf(sb, "Mặt trời mọc %s, lặn %s\n", clock(d.Sunrise[i]), clock(d.Sunset[i]))
}
func round(v float64) int { return int(math.Round(v)) }
func tempValue(v float64) string { return strconv.Itoa(round(v)) }
func temp(v float64) string { return tempValue(v) + "°C" }
// decimal renders one decimal place with the Vietnamese comma separator.
func decimal(v float64) string {
return strings.Replace(strconv.FormatFloat(v, 'f', 1, 64), ".", ",", 1)
}
// shortDate turns Open-Meteo's "2026-10-01" into "01/10".
func shortDate(iso string) string {
t, err := time.Parse(time.DateOnly, iso)
if err != nil {
return iso
}
return t.Format("02/01")
}
func weekday(iso string) string {
t, err := time.Parse(time.DateOnly, iso)
if err != nil {
return ""
}
return vnWeekdays[t.Weekday()]
}
// clock turns Open-Meteo's local "2026-10-01T05:42" into "05:42".
func clock(iso string) string {
t, err := time.Parse("2006-01-02T15:04", iso)
if err != nil {
return iso
}
return t.Format("15:04")
}