96 lines
2.2 KiB
Go
96 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
var baseCurrency = "EUR"
|
|
var ratesAPIURL = "https://open.er-api.com/v6/latest/EUR"
|
|
|
|
type RatesCache struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Rates map[string]float64 `json:"rates"`
|
|
}
|
|
|
|
// fetchRates downloads exchange rates from the API or loads them from a local cache
|
|
// if they are less than 24 hours old.
|
|
func fetchRates() error {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("could not find user home dir for cache: %w", err)
|
|
}
|
|
cacheFile := filepath.Join(home, ".goca_rates.json")
|
|
|
|
var data RatesCache
|
|
useCache := false
|
|
|
|
// Try loading from cache
|
|
file, err := os.Open(cacheFile)
|
|
if err == nil {
|
|
if err := json.NewDecoder(file).Decode(&data); err == nil {
|
|
if time.Since(data.Timestamp) < 24*time.Hour && len(data.Rates) > 0 {
|
|
useCache = true
|
|
}
|
|
}
|
|
file.Close()
|
|
}
|
|
|
|
if !useCache {
|
|
// Fetch from API
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Get(ratesAPIURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch rates from API: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var apiResp struct {
|
|
Rates map[string]float64 `json:"rates"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
|
return fmt.Errorf("failed to decode rates API response: %w", err)
|
|
}
|
|
|
|
data = RatesCache{
|
|
Timestamp: time.Now(),
|
|
Rates: apiResp.Rates,
|
|
}
|
|
|
|
// Save to cache asynchronously
|
|
go func() {
|
|
file, err := os.Create(cacheFile)
|
|
if err == nil {
|
|
json.NewEncoder(file).Encode(data)
|
|
file.Close()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Populate unit registry with rates
|
|
for code, rate := range data.Rates {
|
|
// Avoid division by zero
|
|
if rate > 0 {
|
|
factor := decimal.NewFromFloat(1.0).Div(decimal.NewFromFloat(rate))
|
|
unitRegistry[code] = UnitInfo{factor, DimCurrency, code}
|
|
}
|
|
}
|
|
unitRegistry["EUR"] = UnitInfo{decimal.NewFromFloat(1.0), DimCurrency, "EUR"}
|
|
|
|
return nil
|
|
}
|
|
|
|
// eurRatesLookup returns the conversion factor of the currency to EUR.
|
|
func eurRatesLookup(code string) decimal.Decimal {
|
|
if u, ok := unitRegistry[code]; ok && u.Dimension == DimCurrency {
|
|
return u.Factor
|
|
}
|
|
return decimal.NewFromFloat(1.0)
|
|
}
|