Search/ia-weather.go

368 lines
11 KiB
Go
Raw Normal View History

2025-06-30 08:07:57 +02:00
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"unicode"
"golang.org/x/text/unicode/norm"
)
type WeatherCity struct {
Name string
Country string
Lat float64
Lon float64
}
type WeatherCurrent struct {
Temperature float64
Wind float64
Humidity int
Condition string
}
type WeatherDay struct {
Date string
MinTemp float64
MaxTemp float64
Condition string
}
type WeatherForecast struct {
Current WeatherCurrent
Forecast []WeatherDay
}
func getWeatherForQuery(query string) (city WeatherCity, forecast WeatherForecast, ok bool) {
// Expanded multi-language weather keywords (40+ languages/dialects)
weatherWords := []string{
// English
"weather", "forecast", "temperature", "conditions", "meteorology", "outlook",
// Czech/Slovak
"počasí", "předpověď", "teplota", "vlhkost", "srážky", "vítr", "meteo",
// German
"wetter", "vorhersage", "temperatur", "wettervorhersage", "wetterbericht",
// French
"météo", "prévisions", "température", "conditions météo", "prévision météo",
// Spanish
"tiempo", "clima", "pronóstico", "temperatura", "meteorología", "previsión",
// Italian
"tempo", "meteo", "previsioni", "temperatura", "condizioni atmosferiche",
// Portuguese
"tempo", "clima", "previsão", "temperatura", "meteorologia",
// Polish
"pogoda", "prognoza", "temperatura", "warunki atmosferyczne",
// Russian
"погода", "прогноз", "температура", "метео", "метеопрогноз",
// Ukrainian
"погода", "прогноз", "температура", "метео",
// Dutch
"weer", "voorspelling", "temperatuur", "weersverwachting",
// Scandinavian
"väder", "prognos", "temperatur", // Swedish
"vær", "prognose", "temperatur", // Norwegian/Danish
"veður", "spá", "hitastig", // Icelandic
// East Asian
"天気", "予報", "気温", // Japanese (tenki, yohō, kion)
"날씨", "예보", "기온", // Korean (nalssi, yebo, gion)
"天气", "预报", "气温", // Chinese (tiānqì, yùbào, qìwēn)
// South Asian
"मौसम", "पूर्वानुमान", "तापमान", // Hindi (mausam, purvanumaan, taapmaan)
"আবহাওয়া", "পূর্বাভাস", "তাপমাত্রা", // Bengali (ābhawāẏā, pūrbābhāsa, tāpamātrā)
// Middle Eastern
"طقس", "توقعات", "درجة الحرارة", // Arabic (ṭaqs, tawaqquʿāt, darajat al-ḥarāra)
"آب و ہوا", "پیش گوئی", "درجہ حرارت", // Urdu (āb-o-hawā, peshgoī, daraja ḥarārat)
// Turkish
"hava", "tahmin", "sıcaklık", "hava durumu",
// Greek
"καιρός", "πρόβλεψη", "θερμοκρασία",
// Hebrew
"מזג אוויר", "תחזית", "טמפרטורה",
// Other European
"időkép", "előrejelzés", "hőmérséklet", // Hungarian
"vreme", "prognoză", "temperatură", // Romanian
"vrijeme", "prognoza", "temperatura", // Croatian/Serbian
// Global/Internet slang
"temp", "wx", "meteo", "wea", "forec",
}
// Enhanced multi-language prepositions
prepositions := []string{
// English
"in", "at", "for", "around", "near",
// Czech/Slovak
"v", "ve", "na", "do", "u", "při", "blízko", "okolí",
// German
"in", "bei", "an", "für", "um", "nahe",
// Romance
"en", "a", "au", "aux", "dans", // French
"en", "a", "de", // Spanish
"a", "in", "da", // Italian
"em", "no", "na", // Portuguese
// Slavic
"w", "we", "na", "dla", "pod", // Polish
"в", "на", "у", "к", "под", // Russian/Ukrainian
// Nordic
"i", "på", "hos", // Swedish/Danish/Norwegian
// Others
"في", "عند", "قرب", // Arabic (fī, ʿind, qurb)
"में", "पर", "के पास", // Hindi (mẽ, par, ke pās)
"で", "に", "の近く", // Japanese (de, ni, no chikaku)
"에서", "에", "근처", // Korean (eseo, e, geuncheo)
"在", "于", "附近", // Chinese (zài, yú, fùjìn)
}
// Always normalize query (lowercase + remove diacritics)
normalized := removeDiacritics(strings.ToLower(query))
hasWeather := false
for _, word := range weatherWords {
if strings.Contains(normalized, removeDiacritics(word)) {
hasWeather = true
break
}
}
if !hasWeather {
return city, forecast, false
}
// Improved location extraction with diacritic handling
loc := extractWeatherLocation(normalized, weatherWords, prepositions)
if loc == "" {
return city, forecast, false
}
// Geocode and get weather
return geocodeAndGetWeather(loc)
}
func extractWeatherLocation(query string, weatherWords, prepositions []string) string {
// Create normalized versions for matching
normWeatherWords := make([]string, len(weatherWords))
for i, w := range weatherWords {
normWeatherWords[i] = removeDiacritics(w)
}
normPrepositions := make([]string, len(prepositions))
for i, p := range prepositions {
normPrepositions[i] = removeDiacritics(p)
}
// Pattern 1: [weather_word] [preposition]? [location]
pattern1 := `(?:` + strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)`
re1 := regexp.MustCompile(pattern1)
if matches := re1.FindStringSubmatch(query); len(matches) > 1 {
loc := cleanLocation(matches[1], normPrepositions)
if loc != "" {
return loc
}
}
// Pattern 2: [location] [weather_word]
pattern2 := `(.+?)\s+(?:` + strings.Join(normWeatherWords, "|") + `)`
re2 := regexp.MustCompile(pattern2)
if matches := re2.FindStringSubmatch(query); len(matches) > 1 {
loc := cleanLocation(matches[1], normPrepositions)
if loc != "" {
return loc
}
}
// Pattern 3: Question format
questionPattern := `(?:how is|what is|what's|jak[ée]\s+je|wie ist|quel est|qu[eé]\s+tal|com'[èe])\s+(?:the )?(?:` +
strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)`
re3 := regexp.MustCompile(questionPattern)
if matches := re3.FindStringSubmatch(query); len(matches) > 1 {
loc := cleanLocation(matches[1], normPrepositions)
if loc != "" {
return loc
}
}
// Fallback with smarter exclusion
return extractByExclusion(query, normWeatherWords, normPrepositions)
}
func cleanLocation(loc string, prepositions []string) string {
// Create preposition set
prepSet := make(map[string]bool)
for _, p := range prepositions {
prepSet[p] = true
}
words := strings.Fields(loc)
// Remove leading prepositions
for len(words) > 0 && prepSet[words[0]] {
words = words[1:]
}
// Remove trailing prepositions
for len(words) > 0 && prepSet[words[len(words)-1]] {
words = words[:len(words)-1]
}
// Rejoin and clean
cleaned := strings.Join(words, " ")
return strings.Trim(cleaned, ",.?!:;()[]{}'\"")
}
// Remove diacritics implementation
func removeDiacritics(s string) string {
var result []rune
for _, r := range norm.NFD.String(s) {
if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks
continue
}
result = append(result, r)
}
return string(result)
}
// Extract location by removing weather-related words
func extractByExclusion(query string, weatherWords, prepositions []string) string {
// Create removal set
removeSet := make(map[string]bool)
for _, w := range weatherWords {
removeSet[w] = true
}
for _, p := range prepositions {
removeSet[p] = true
}
// Process query words
words := strings.Fields(query)
var locWords []string
for _, word := range words {
if !removeSet[word] {
locWords = append(locWords, word)
}
}
loc := strings.Join(locWords, " ")
return cleanLocation(loc, prepositions)
}
// // Improved location cleaning
// func cleanLocation(loc string) string {
// loc = strings.Trim(loc, ",.?!:;()[]{}'\"")
// // Remove trailing verbs
// verbs := []string{"is", "at", "for", "in", "v", "ve", "na", "do", "w", "en", "a"}
// for _, v := range verbs {
// loc = strings.TrimSuffix(loc, " "+v)
// }
// return loc
// }
// // Remove diacritics implementation
// func removeDiacritics(s string) string {
// var result []rune
// for _, r := range norm.NFD.String(s) {
// if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks
// continue
// }
// result = append(result, r)
// }
// return string(result)
// }
func geocodeAndGetWeather(loc string) (WeatherCity, WeatherForecast, bool) {
var city WeatherCity
var forecast WeatherForecast
// 1. Geocode
geoURL := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1", urlQueryEscape(loc))
resp, err := http.Get(geoURL)
if err != nil {
return city, forecast, false
}
defer resp.Body.Close()
var geo struct {
Results []struct {
Name string `json:"name"`
Country string `json:"country"`
Lat float64 `json:"latitude"`
Lon float64 `json:"longitude"`
} `json:"results"`
}
if err := json.NewDecoder(resp.Body).Decode(&geo); err != nil || len(geo.Results) == 0 {
return city, forecast, false
}
g := geo.Results[0]
city = WeatherCity{
Name: g.Name,
Country: g.Country,
Lat: g.Lat,
Lon: g.Lon,
}
// 2. Weather (current + forecast)
weatherURL := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&current=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m&daily=temperature_2m_min,temperature_2m_max,weather_code&forecast_days=3&timezone=auto", g.Lat, g.Lon)
resp2, err := http.Get(weatherURL)
if err != nil {
return city, forecast, false
}
defer resp2.Body.Close()
var data struct {
Current struct {
Temp float64 `json:"temperature_2m"`
Wind float64 `json:"wind_speed_10m"`
Hum int `json:"relative_humidity_2m"`
Code int `json:"weather_code"`
} `json:"current"`
Daily struct {
Dates []string `json:"time"`
MinTemp []float64 `json:"temperature_2m_min"`
MaxTemp []float64 `json:"temperature_2m_max"`
Weather []int `json:"weather_code"`
} `json:"daily"`
}
body, _ := io.ReadAll(resp2.Body)
if err := json.Unmarshal(body, &data); err != nil {
return city, forecast, false
}
forecast.Current = WeatherCurrent{
Temperature: data.Current.Temp,
Wind: data.Current.Wind,
Humidity: data.Current.Hum,
Condition: weatherDescription(data.Current.Code),
}
for i := range data.Daily.Dates {
forecast.Forecast = append(forecast.Forecast, WeatherDay{
Date: data.Daily.Dates[i],
MinTemp: data.Daily.MinTemp[i],
MaxTemp: data.Daily.MaxTemp[i],
Condition: weatherDescription(data.Daily.Weather[i]),
})
}
return city, forecast, true
}
func weatherDescription(code int) string {
// Minimal mapping, can be expanded
switch code {
case 0:
return "Clear"
case 1, 2, 3:
return "Partly cloudy"
case 45, 48:
return "Fog"
case 51, 53, 55, 56, 57:
return "Drizzle"
case 61, 63, 65, 66, 67, 80, 81, 82:
return "Rain"
case 71, 73, 75, 77, 85, 86:
return "Snow"
case 95, 96, 99:
return "Thunderstorm"
default:
return "Unknown"
}
}
// Helper for safe query escaping
func urlQueryEscape(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, " ", "+"), "%", "")
}