first commit

This commit is contained in:
Matthias Hinrichs
2025-07-05 03:10:41 +02:00
commit 9b7bdcbc53
39 changed files with 5109 additions and 0 deletions
+230
View File
@@ -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
}