Files
tiennm99bot/internal/modules/thoitiet/format.go
T

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 thoitiet
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")
}