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) lastUpdated time.Time exchangeCacheMutex sync.RWMutex cacheExpiration = 1 * time.Hour 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"` } 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 lastUpdated = time.Now() // Update list of all currencies allCurrencies = make([]string, 0, len(exchangeRates)) for currency := range exchangeRates { allCurrencies = append(allCurrencies, currency) } log.Printf("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 } log.Printf("Starting precache of all currency pairs (%d currencies)", len(allCurrencies)) // We'll pre-cache the most common pairs directly commonCurrencies := []string{"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR"} for _, from := range commonCurrencies { for _, to := range allCurrencies { if from == to { continue } GetExchangeRate(from, to) } } log.Println("Common 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.Since(lastUpdated) > cacheExpiration { UpdateExchangeRates() // Ignore errors, use stale data if needed } 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 }