207 lines
5.1 KiB
Go
207 lines
5.1 KiB
Go
|
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
|
||
|
}
|