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¤t=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, " ", "+"), "%", "") }