package main import ( "encoding/json" "fmt" "io" "log" "net/http" "regexp" "strconv" "strings" "sync" "time" ) // ExchangeRateCache holds currency rates with automatic refresh var ( exchangeRates = make(map[string]float64) nextUpdateTime time.Time exchangeCacheMutex sync.RWMutex allCurrencies []string ) // CurrencyAPIResponse structure for exchange rate API type CurrencyAPIResponse struct { Rates map[string]float64 `json:"rates"` } // UpdateExchangeRates fetches and caches currency rates func UpdateExchangeRates() error { exchangeCacheMutex.Lock() defer exchangeCacheMutex.Unlock() // Use a reliable free API with good rate limits resp, err := http.Get("https://open.er-api.com/v6/latest/USD") if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return err } var apiResponse struct { Result string `json:"result"` Rates map[string]float64 `json:"rates"` TimeNextUpdateUnix int64 `json:"time_next_update_unix"` } if err := json.Unmarshal(body, &apiResponse); err != nil { return err } if apiResponse.Result != "success" { return fmt.Errorf("API error: %s", apiResponse.Result) } // Cache the rates exchangeRates = apiResponse.Rates nextUpdateTime = time.Unix(apiResponse.TimeNextUpdateUnix, 0) // Update list of all currencies allCurrencies = make([]string, 0, len(exchangeRates)) for currency := range exchangeRates { allCurrencies = append(allCurrencies, currency) } printDebug("Updated currency rates: %d currencies cached", len(allCurrencies)) return nil } // PrecacheAllCurrencyPairs pre-caches conversion rates for all currency pairs func PrecacheAllCurrencyPairs() { exchangeCacheMutex.RLock() defer exchangeCacheMutex.RUnlock() if len(exchangeRates) == 0 { log.Println("Skipping precache - no rates available") return } printDebug("Precaching all currency pairs (%d total)", len(exchangeRates)) for from := range exchangeRates { for to := range exchangeRates { if from == to { continue } // Cache the cross-rate GetExchangeRate(from, to) } } printDebug("All currency pairs precached") } // GetExchangeRate gets the current exchange rate with caching func GetExchangeRate(from, to string) (float64, bool) { // Auto-update cache if expired if time.Now().After(nextUpdateTime) { err := UpdateExchangeRates() if err != nil { printWarn("Currency update failed: %v", err) // Postpone next attempt to avoid hammering API nextUpdateTime = time.Now().Add(5 * time.Minute) } } exchangeCacheMutex.RLock() defer exchangeCacheMutex.RUnlock() // Handle same currency if from == to { return 1, true } // Convert via USD if direct rate not available fromRate, fromExists := exchangeRates[from] toRate, toExists := exchangeRates[to] if !fromExists || !toExists { return 0, false } // Calculate cross rate: (1 USD / fromRate) * toRate return toRate / fromRate, true } // ParseCurrencyConversion detects and processes currency conversion queries func ParseCurrencyConversion(query string) (float64, string, string, bool) { // Match patterns like: "100 USD to EUR", "50 eur in gbp", "¥1000 to USD" re := regexp.MustCompile(`(?i)([\d,]+(?:\.\d+)?)\s*([$€£¥₩₹₽A-Za-z]{1,6})\s+(?:to|in|➞|→)\s+([$€£¥₩₹₽A-Za-z]{1,6})`) matches := re.FindStringSubmatch(query) if len(matches) < 4 { return 0, "", "", false } // Clean and parse amount amountStr := strings.ReplaceAll(matches[1], ",", "") amount, err := strconv.ParseFloat(amountStr, 64) if err != nil { return 0, "", "", false } // Normalize currency symbols currencyMap := map[string]string{ "$": "USD", "€": "EUR", "£": "GBP", "¥": "JPY", "₩": "KRW", "₹": "INR", "₽": "RUB", "usd": "USD", "eur": "EUR", "gbp": "GBP", "jpy": "JPY", "krw": "KRW", "inr": "INR", "rub": "RUB", "dollar": "USD", "euro": "EUR", "pound": "GBP", "yen": "JPY", "won": "KRW", "rupee": "INR", "ruble": "RUB", } fromCurr := strings.ToUpper(matches[2]) if mapped, ok := currencyMap[fromCurr]; ok { fromCurr = mapped } else if len(fromCurr) > 3 { // Try to match longer names for k, v := range currencyMap { if strings.EqualFold(k, fromCurr) { fromCurr = v break } } } toCurr := strings.ToUpper(matches[3]) if mapped, ok := currencyMap[toCurr]; ok { toCurr = mapped } else if len(toCurr) > 3 { // Try to match longer names for k, v := range currencyMap { if strings.EqualFold(k, toCurr) { toCurr = v break } } } return amount, fromCurr, toCurr, true } // ConvertCurrency handles the actual conversion func ConvertCurrency(amount float64, from, to string) (float64, bool) { if from == to { return amount, true } rate, ok := GetExchangeRate(from, to) if !ok { // Try to find similar currencies from = strings.ToUpper(from) to = strings.ToUpper(to) // Check if we have the currency in our list exchangeCacheMutex.RLock() defer exchangeCacheMutex.RUnlock() _, fromExists := exchangeRates[from] _, toExists := exchangeRates[to] if !fromExists || !toExists { return 0, false } // Shouldn't happen due to the check above, but just in case return 0, false } return amount * rate, true }