380 lines
16 KiB
Go
380 lines
16 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)
|
||
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) {
|
||
// Main conversion phrases
|
||
conversionPhrases := []string{
|
||
// Universal/math
|
||
"➞", "→", "⇒", ">", "->", "=", "≈", "~", ":", "≡",
|
||
// English
|
||
"to", "in", "into", "as", "equals", "equal to", "equals to", "is", "becomes", "be", "makes", "converted to", "convert to", "convert into", "converted into",
|
||
"exchange for", "exchanged for", "value in", "as currency", "convert", "equivalent to", "same as", "is equal to", ">", "gives", "makes", "result is", "returns", "will be", "equals:", "is equivalent to", "≈", "~", ":",
|
||
// German (DE)
|
||
"auf", "in", "zu", "umrechnen in", "umrechnen zu", "als", "gleich", "ist", "ist gleich", "umwandeln in", "wird zu", "ergibt", "macht", "ist", "resultiert in", "gleichwertig mit",
|
||
// Spanish (ES)
|
||
"en", "a", "como", "igual a", "es", "es igual a", "es igual", "convertir a", "cambiar a", "valor en", "convierte en", "devuelve", "será", "equivale a", "es equivalente a",
|
||
// French (FR)
|
||
"vers", "en", "comme", "égal à", "est", "c'est", "convertir en", "changer en", "valeur en", "équivaut à", "sera", "fait", "rend", "est égal à", "équivalent à",
|
||
// Italian (IT)
|
||
"a", "in", "come", "uguale a", "è", "convertire in", "cambiare in", "valore in", "sarà", "fa", "equivale a", "è uguale a",
|
||
// Portuguese (PT/BR)
|
||
"para", "em", "como", "igual a", "é", "converter para", "trocar por", "valor em", "converte em", "vai ser", "faz", "equivale a", "é igual a", "é equivalente a",
|
||
// Dutch (NL)
|
||
"naar", "in", "als", "is gelijk aan", "is", "wordt", "omzetten naar", "waarde in", "gelijk aan", "is hetzelfde als",
|
||
// Czech (CS)
|
||
"na", "do", "jako", "rovná se", "je", "převést na", "výměna za", "hodnota v", "přepočet", "bude", "rovná", "je to", "je rovno", "je stejné jako",
|
||
// Slovak (SK)
|
||
"na", "do", "ako", "rovná sa", "je", "previesť na", "výměna za", "hodnota v", "prerátať", "bude", "rovná", "je to", "je rovné", "je rovnaké ako",
|
||
// Polish (PL)
|
||
"na", "w", "jako", "równa się", "jest", "przelicz na", "wymień na", "wartość w", "przelicza się na", "będzie", "to jest", "jest równy", "jest taki sam jak",
|
||
// Russian (RU)
|
||
"на", "в", "как", "равно", "есть", "конвертировать в", "обменять на", "значение в", "равняется", "будет", "это", "такое же как",
|
||
// Ukrainian (UA)
|
||
"на", "у", "як", "дорівнює", "є", "конвертувати у", "обміняти на", "значення в", "буде", "це", "таке саме як",
|
||
// Croatian / Serbian / Bosnian / Slovenian (HR/SR/BS/SL)
|
||
"na", "u", "za", "kao", "jednako", "je", "pretvori u", "zamijeniti za", "vrijednost u", "preračunaj u", "biti", "to je", "jednako kao", "je isto kao",
|
||
"v", "kot", "je enako", "pretvoriti v", "zamenjati za", "vrednost v", "je isto kao", "je enakovredno",
|
||
// Bulgarian (BG)
|
||
"на", "в", "като", "равно на", "е", "преобразувай в", "обмени на", "стойност в", "ще бъде", "това е", "равностойно на",
|
||
// Turkish (TR)
|
||
"için", "olarak", "eşittir", "bu", "dönüştür to", "değiştir to", "değer olarak", "olur", "eşit", "bu olur", "aynı olarak",
|
||
// Greek (EL)
|
||
"σε", "ως", "ίσον", "είναι", "μετατροπή σε", "ανταλλαγή με", "τιμή σε", "θα είναι", "αυτό είναι", "ισοδυναμεί με", "ίσο με",
|
||
// Chinese (Simplified and Traditional, ZH)
|
||
"到", "变为", "換成", "转换为", "等于", "等於", "是", "为", "結果是", "相等於", "等同於", "一樣",
|
||
// Japanese (JA)
|
||
"に", "として", "等しい", "は", "に変換", "に交換", "の値", "は", "結果は", "となる", "同じ", "等価", "等しく",
|
||
// Korean (KO)
|
||
"으로", "같이", "같다", "이다", "로 변환", "교환하다", "값", "이 된다", "와 같다", "같음", "동일하다",
|
||
// Arabic (AR)
|
||
"إلى", "الى", "في", "كـ", "يساوي", "هو", "تحويل إلى", "قيمة في", "يصبح", "يساوي نفس", "تعادل", "تساوي",
|
||
// Hebrew (HE)
|
||
"ל", "ב", "בתור", "שווה ל", "הוא", "המר ל", "ערך ב", "יהיה", "אותו הדבר כמו", "זהה ל",
|
||
// Romanian (RO)
|
||
"la", "în", "ca", "egal cu", "este", "converti la", "schimbă în", "valoare în", "va fi", "este egal cu",
|
||
// Hungarian (HU)
|
||
"ra", "re", "ba", "be", "mint", "egyenlő", "az", "átvált", "értéke", "lesz", "ugyanaz mint",
|
||
// Swedish (SE)
|
||
"till", "i", "som", "är", "är lika med", "omvandla till", "värde i", "blir", "är samma som",
|
||
// Danish (DK)
|
||
"til", "i", "som", "er", "er lig med", "konverter til", "værdi i", "bliver", "er det samme som",
|
||
// Norwegian (NO)
|
||
"til", "i", "som", "er", "er lik", "konverter til", "verdi i", "blir", "er det samme som",
|
||
// Finnish (FI)
|
||
"ksi", "in", "kuin", "on", "on yhtä kuin", "muunna", "arvo", "tulee olemaan", "sama kuin",
|
||
// Estonian (EE)
|
||
"ks", "sisse", "nagu", "on", "on võrdne", "teisendada", "väärtus", "saab olema", "sama mis",
|
||
// Latvian (LV)
|
||
"uz", "iekš", "kā", "ir", "ir vienāds ar", "konvertēt uz", "vērtība", "būs", "tāpat kā",
|
||
// Lithuanian (LT)
|
||
"į", "kaip", "yra", "yra lygus", "konvertuoti į", "vertė", "bus", "tas pats kaip",
|
||
// Persian (FA)
|
||
"به", "در", "مثل", "برابر با", "است", "تبدیل به", "ارزش در", "خواهد بود", "همانند",
|
||
// Hindi (HI)
|
||
"को", "में", "के रूप में", "बराबर", "है", "में बदलें", "मूल्य में", "होगा", "के समान",
|
||
// Thai (TH)
|
||
"ไปที่", "ใน", "เป็น", "เท่ากับ", "คือ", "แปลงเป็น", "ค่าใน", "จะเป็น", "เท่ากัน",
|
||
// Indonesian (ID)
|
||
"ke", "dalam", "sebagai", "sama dengan", "adalah", "konversi ke", "nilai dalam", "akan menjadi", "sama dengan",
|
||
// Vietnamese (VI)
|
||
"thành", "trong", "là", "bằng", "là", "chuyển đổi thành", "giá trị trong", "sẽ là", "tương đương với",
|
||
// Malay (MS)
|
||
"kepada", "dalam", "sebagai", "sama dengan", "ialah", "tukar ke", "nilai dalam", "akan jadi", "setara dengan",
|
||
// Filipino/Tagalog (TL)
|
||
"sa", "sa loob ng", "bilang", "katumbas ng", "ay", "i-convert sa", "halaga sa", "magiging", "pareho sa",
|
||
}
|
||
|
||
// Build the OR group for all currency conversion phrases to use in the regex pattern
|
||
var orGroup strings.Builder
|
||
for i, phrase := range conversionPhrases {
|
||
if i > 0 {
|
||
orGroup.WriteString("|")
|
||
}
|
||
// escape for regex with special symbols:
|
||
orGroup.WriteString(regexp.QuoteMeta(phrase))
|
||
}
|
||
regexPattern := fmt.Sprintf(`(?i)([\d,]+(?:\.\d+)?)\s*([^\d,]+?)\s+(?:%s)\s+([^\d,]+)`, orGroup.String())
|
||
re := regexp.MustCompile(regexPattern)
|
||
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{
|
||
// Major Global Currencies
|
||
"$": "USD", "usd": "USD", "dollar": "USD", "dollars": "USD", "buck": "USD", "bucks": "USD", "us dollar": "USD", "american dollar": "USD", "freedom units": "USD",
|
||
"€": "EUR", "eur": "EUR", "euro": "EUR", "euros": "EUR",
|
||
"£": "GBP", "gbp": "GBP", "pound": "GBP", "pounds": "GBP", "sterling": "GBP", "quid": "GBP", "pound sterling": "GBP",
|
||
"¥": "JPY", "jpy": "JPY", "yen": "JPY", "cn¥": "CNY", // Handle ¥ ambiguity with CN¥ for Chinese Yuan
|
||
"₩": "KRW", "krw": "KRW", "won": "KRW", "korean won": "KRW",
|
||
"₹": "INR", "inr": "INR", "rupee": "INR", "rupees": "INR", "indian rupee": "INR",
|
||
"₽": "RUB", "rub": "RUB", "ruble": "RUB", "rubles": "RUB", "russian ruble": "RUB",
|
||
|
||
// Americas
|
||
"c$": "CAD", "cad": "CAD", "canadian dollar": "CAD", "loonie": "CAD",
|
||
"a$": "AUD", "aud": "AUD", "australian dollar": "AUD", "aussie dollar": "AUD",
|
||
"nz$": "NZD", "nzd": "NZD", "new zealand dollar": "NZD", "kiwi": "NZD", "kiwi dollar": "NZD",
|
||
"r$": "BRL", "brl": "BRL", "real": "BRL", "reais": "BRL", "brazilian real": "BRL",
|
||
"mx$": "MXN", "mxn": "MXN", "mexican peso": "MXN", "mexican pesos": "MXN",
|
||
"col$": "COP", "cop": "COP", "colombian peso": "COP",
|
||
"s/": "PEN", "pen": "PEN", "sol": "PEN", "soles": "PEN", "peruvian sol": "PEN",
|
||
"clp$": "CLP", "clp": "CLP", "chilean peso": "CLP",
|
||
"arg$": "ARS", "ars": "ARS", "argentine peso": "ARS",
|
||
|
||
// Europe & CIS
|
||
"chf": "CHF", "fr": "CHF", "swiss franc": "CHF", "franc suisse": "CHF",
|
||
"sek": "SEK", "kr": "SEK", "swedish krona": "SEK", "swedish kronor": "SEK",
|
||
"nok": "NOK", "norwegian krone": "NOK", "norwegian kroner": "NOK",
|
||
"dkk": "DKK", "danish krone": "DKK", "danish kroner": "DKK",
|
||
"zł": "PLN", "pln": "PLN", "zloty": "PLN", "polish zloty": "PLN",
|
||
"tl": "TRY", "try": "TRY", "turkish lira": "TRY", "türk lirası": "TRY", "₺": "TRY",
|
||
"huf": "HUF", "ft": "HUF", "forint": "HUF", "hungarian forint": "HUF",
|
||
"czk": "CZK", "kč": "CZK", "czech koruna": "CZK",
|
||
"ron": "RON", "lei": "RON", "romanian leu": "RON",
|
||
"bgn": "BGN", "лв": "BGN", "bulgarian lev": "BGN",
|
||
"uah": "UAH", "₴": "UAH", "hryvnia": "UAH", "ukrainian hryvnia": "UAH",
|
||
"kzt": "KZT", "₸": "KZT", "tenge": "KZT", "kazakhstani tenge": "KZT",
|
||
|
||
// Asia/Pacific
|
||
"cny": "CNY", "rmb": "CNY", "yuan": "CNY", "renminbi": "CNY", "chinese yuan": "CNY",
|
||
"hk$": "HKD", "hkd": "HKD", "hong kong dollar": "HKD",
|
||
"s$": "SGD", "sgd": "SGD", "singapore dollar": "SGD",
|
||
"nt$": "TWD", "twd": "TWD", "taiwan dollar": "TWD", "new taiwan dollar": "TWD",
|
||
"฿": "THB", "thb": "THB", "baht": "THB", "thai baht": "THB",
|
||
"rp": "IDR", "idr": "IDR", "rupiah": "IDR", "indonesian rupiah": "IDR",
|
||
"₱": "PHP", "php": "PHP", "philippine peso": "PHP",
|
||
"rm": "MYR", "myr": "MYR", "ringgit": "MYR", "malaysian ringgit": "MYR",
|
||
"₫": "VND", "vnd": "VND", "dong": "VND", "vietnamese dong": "VND",
|
||
"₭": "LAK", "lak": "LAK", "kip": "LAK", "lao kip": "LAK",
|
||
"៛": "KHR", "khr": "KHR", "riel": "KHR", "cambodian riel": "KHR",
|
||
|
||
// Middle East & Africa
|
||
"₪": "ILS", "ils": "ILS", "shekel": "ILS", "new israeli shekel": "ILS",
|
||
"﷼": "SAR", "sr": "SAR", "sar": "SAR", "riyal": "SAR", "saudi riyal": "SAR",
|
||
"د.إ": "AED", "dh": "AED", "aed": "AED", "dirham": "AED", "uae dirham": "AED",
|
||
"egp": "EGP", "e£": "EGP", "egyptian pound": "EGP",
|
||
"zar": "ZAR", "r": "ZAR", "rand": "ZAR", "south african rand": "ZAR",
|
||
"₦": "NGN", "ngn": "NGN", "naira": "NGN", "nigerian naira": "NGN",
|
||
}
|
||
|
||
// Improved normalization function
|
||
normalizeCurrency := func(input string) string {
|
||
clean := strings.TrimSpace(strings.ToLower(input))
|
||
clean = strings.Join(strings.Fields(clean), " ")
|
||
// Direct map
|
||
if mapped, ok := currencyMap[clean]; ok {
|
||
return mapped
|
||
}
|
||
// Fuzzy match: for last word
|
||
words := strings.Fields(clean)
|
||
for i := 0; i < len(words); i++ {
|
||
sub := strings.Join(words[i:], " ")
|
||
if mapped, ok := currencyMap[sub]; ok {
|
||
return mapped
|
||
}
|
||
}
|
||
// Fuzzy match: try reducing phrase from the end
|
||
for i := len(words) - 1; i >= 0; i-- {
|
||
sub := strings.Join(words[:i], " ")
|
||
if mapped, ok := currencyMap[sub]; ok {
|
||
return mapped
|
||
}
|
||
}
|
||
// Handle currency symbols at the end (e.g. "100usd")
|
||
if len(clean) > 1 {
|
||
if symbol, ok := currencyMap[string(clean[len(clean)-1])]; ok {
|
||
return symbol
|
||
}
|
||
}
|
||
// Currency code fallback
|
||
if len(clean) == 3 {
|
||
upper := strings.ToUpper(clean)
|
||
exchangeCacheMutex.RLock()
|
||
defer exchangeCacheMutex.RUnlock()
|
||
if _, exists := exchangeRates[upper]; exists {
|
||
return upper
|
||
}
|
||
}
|
||
return strings.ToUpper(input)
|
||
}
|
||
|
||
fromCurr := normalizeCurrency(matches[2])
|
||
toCurr := normalizeCurrency(matches[3])
|
||
|
||
// Validate currencies exist in exchange rates
|
||
exchangeCacheMutex.RLock()
|
||
defer exchangeCacheMutex.RUnlock()
|
||
if _, fromExists := exchangeRates[fromCurr]; !fromExists {
|
||
return 0, "", "", false
|
||
}
|
||
if _, toExists := exchangeRates[toCurr]; !toExists {
|
||
return 0, "", "", false
|
||
}
|
||
|
||
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
|
||
}
|