Search/ia-currency.go

211 lines
5.2 KiB
Go
Raw Normal View History

2025-06-25 23:23:33 +02:00
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)
2025-06-29 08:11:02 +02:00
nextUpdateTime time.Time
2025-06-25 23:23:33 +02:00
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 {
2025-06-29 08:11:02 +02:00
Result string `json:"result"`
Rates map[string]float64 `json:"rates"`
TimeNextUpdateUnix int64 `json:"time_next_update_unix"`
2025-06-25 23:23:33 +02:00
}
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
2025-06-29 08:11:02 +02:00
nextUpdateTime = time.Unix(apiResponse.TimeNextUpdateUnix, 0)
2025-06-25 23:23:33 +02:00
// Update list of all currencies
allCurrencies = make([]string, 0, len(exchangeRates))
for currency := range exchangeRates {
allCurrencies = append(allCurrencies, currency)
}
2025-06-29 08:11:02 +02:00
printDebug("Updated currency rates: %d currencies cached", len(allCurrencies))
2025-06-25 23:23:33 +02:00
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
}
2025-06-29 08:11:02 +02:00
printDebug("Precaching all currency pairs (%d total)", len(exchangeRates))
2025-06-25 23:23:33 +02:00
2025-06-29 08:11:02 +02:00
for from := range exchangeRates {
for to := range exchangeRates {
2025-06-25 23:23:33 +02:00
if from == to {
continue
}
2025-06-29 08:11:02 +02:00
// Cache the cross-rate
2025-06-25 23:23:33 +02:00
GetExchangeRate(from, to)
}
}
2025-06-29 08:11:02 +02:00
printDebug("All currency pairs precached")
2025-06-25 23:23:33 +02:00
}
// GetExchangeRate gets the current exchange rate with caching
func GetExchangeRate(from, to string) (float64, bool) {
// Auto-update cache if expired
2025-06-29 08:11:02 +02:00
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)
}
2025-06-25 23:23:33 +02:00
}
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
}