Search/ia-currency.go
partisan e2f1707723
Some checks failed
Run Integration Tests / test (push) Failing after 38s
Improved IA currency detection and matching
2025-06-29 09:41:00 +02:00

380 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}