first commit
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExchangeRateResponse represents the response from the exchange rate API
|
||||
type ExchangeRateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Base string `json:"base"`
|
||||
Date string `json:"date"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
|
||||
// CachedRate stores exchange rate with timestamp
|
||||
type CachedRate struct {
|
||||
Rate float64
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Currency conversion cache
|
||||
var (
|
||||
exchangeRateCache = make(map[string]CachedRate)
|
||||
cacheMutex sync.RWMutex
|
||||
cacheValidDuration = 1 * time.Hour // Cache rates for 1 hour
|
||||
)
|
||||
|
||||
// GetExchangeRate fetches the exchange rate from fromCurrency to toCurrency
|
||||
func GetExchangeRate(fromCurrency, toCurrency string) (float64, error) {
|
||||
// If currencies are the same, return 1.0
|
||||
if fromCurrency == toCurrency {
|
||||
return 1.0, nil
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s_%s", fromCurrency, toCurrency)
|
||||
|
||||
// Check cache first
|
||||
cacheMutex.RLock()
|
||||
if cached, exists := exchangeRateCache[cacheKey]; exists {
|
||||
if time.Since(cached.Timestamp) < cacheValidDuration {
|
||||
cacheMutex.RUnlock()
|
||||
return cached.Rate, nil
|
||||
}
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
// Fetch from API if not in cache or cache is expired
|
||||
rate, err := fetchExchangeRateFromAPI(fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
cacheMutex.Lock()
|
||||
exchangeRateCache[cacheKey] = CachedRate{
|
||||
Rate: rate,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
cacheMutex.Unlock()
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// fetchExchangeRateFromAPI fetches exchange rate from a free API
|
||||
func fetchExchangeRateFromAPI(fromCurrency, toCurrency string) (float64, error) {
|
||||
// Using exchangerate-api.com (completely free, no API key required)
|
||||
url := fmt.Sprintf("https://api.exchangerate-api.com/v4/latest/%s", fromCurrency)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Portfolio-Tracker/1.0")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error fetching exchange rate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error reading response: %v", err)
|
||||
}
|
||||
|
||||
// Parse exchangerate-api.com response format
|
||||
var apiResp struct {
|
||||
Base string `json:"base"`
|
||||
Date string `json:"date"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return 0, fmt.Errorf("error parsing JSON: %v", err)
|
||||
}
|
||||
|
||||
rate, exists := apiResp.Rates[toCurrency]
|
||||
if !exists {
|
||||
return 0, fmt.Errorf("currency %s not found in response", toCurrency)
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// ConvertCurrency converts an amount from one currency to another
|
||||
func ConvertCurrency(amount float64, fromCurrency, toCurrency string) (float64, error) {
|
||||
if fromCurrency == toCurrency {
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
rate, err := GetExchangeRate(fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return amount * rate, nil
|
||||
}
|
||||
|
||||
// FormatCurrencyAmount formats a currency amount with the currency symbol
|
||||
func FormatCurrencyAmount(amount float64, currency string) string {
|
||||
return fmt.Sprintf("%.2f %s", amount, currency)
|
||||
}
|
||||
|
||||
// GetCurrencySymbol returns the symbol for common currencies
|
||||
func GetCurrencySymbol(currency string) string {
|
||||
symbols := map[string]string{
|
||||
"USD": "$",
|
||||
"EUR": "€",
|
||||
"GBP": "£",
|
||||
"JPY": "¥",
|
||||
"CHF": "CHF",
|
||||
"CAD": "C$",
|
||||
"AUD": "A$",
|
||||
"CNY": "¥",
|
||||
"INR": "₹",
|
||||
"KRW": "₩",
|
||||
"SEK": "kr",
|
||||
"NOK": "kr",
|
||||
"DKK": "kr",
|
||||
"PLN": "zł",
|
||||
"CZK": "Kč",
|
||||
"HUF": "Ft",
|
||||
}
|
||||
|
||||
if symbol, exists := symbols[currency]; exists {
|
||||
return symbol
|
||||
}
|
||||
return currency // Return currency code if symbol not found
|
||||
}
|
||||
|
||||
// FormatCurrencyWithSymbol formats amount with currency symbol
|
||||
func FormatCurrencyWithSymbol(amount float64, currency string) string {
|
||||
symbol := GetCurrencySymbol(currency)
|
||||
if symbol == currency {
|
||||
// If no symbol found, put currency after amount
|
||||
return fmt.Sprintf("%.2f %s", amount, currency)
|
||||
}
|
||||
|
||||
// For most currencies, put symbol before amount
|
||||
switch currency {
|
||||
case "EUR":
|
||||
return fmt.Sprintf("%.2f %s", amount, symbol)
|
||||
default:
|
||||
return fmt.Sprintf("%s%.2f", symbol, amount)
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertAndFormat converts currency and formats it nicely
|
||||
func ConvertAndFormat(amount float64, fromCurrency, toCurrency string) string {
|
||||
if fromCurrency == toCurrency {
|
||||
return FormatCurrencyWithSymbol(amount, toCurrency)
|
||||
}
|
||||
|
||||
convertedAmount, err := ConvertCurrency(amount, fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
// If conversion fails, return original amount with note
|
||||
return fmt.Sprintf("%.2f %s (conv. error)", amount, fromCurrency)
|
||||
}
|
||||
|
||||
return FormatCurrencyWithSymbol(convertedAmount, toCurrency)
|
||||
}
|
||||
|
||||
// BatchConvertCurrency converts multiple amounts in one API call for efficiency
|
||||
func BatchConvertCurrency(amounts []float64, fromCurrency, toCurrency string) ([]float64, error) {
|
||||
if fromCurrency == toCurrency {
|
||||
return amounts, nil
|
||||
}
|
||||
|
||||
rate, err := GetExchangeRate(fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
converted := make([]float64, len(amounts))
|
||||
for i, amount := range amounts {
|
||||
converted[i] = amount * rate
|
||||
}
|
||||
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
// ClearCache clears the exchange rate cache (useful for testing or manual refresh)
|
||||
func ClearCache() {
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
exchangeRateCache = make(map[string]CachedRate)
|
||||
}
|
||||
|
||||
// GetCacheInfo returns information about cached exchange rates
|
||||
func GetCacheInfo() map[string]time.Time {
|
||||
cacheMutex.RLock()
|
||||
defer cacheMutex.RUnlock()
|
||||
|
||||
info := make(map[string]time.Time)
|
||||
for key, cached := range exchangeRateCache {
|
||||
info[key] = cached.Timestamp
|
||||
}
|
||||
return info
|
||||
}
|
||||
Reference in New Issue
Block a user