[READ-ONLY] Mirror of https://github.com/probablykasper/cpc. Text calculator with support for units and conversion cpc.kasper.space
calculator cli conversion converter library math package units units-converter wasm website
0

Configure Feed

Select the types of activity you want to include in your feed.

Improve currency code

Kasper (Jun 29, 2026, 10:03 AM +0200) 823a50e5 db7aeea7

+63 -79
+14
Cargo.lock
··· 40 40 version = "0.12.1" 41 41 source = "registry+https://github.com/rust-lang/crates.io-index" 42 42 checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10" 43 + dependencies = [ 44 + "serde", 45 + "serde-big-array", 46 + ] 43 47 44 48 [[package]] 45 49 name = "bumpalo" ··· 166 170 "bnum", 167 171 "num-integer", 168 172 "num-traits", 173 + "serde", 169 174 ] 170 175 171 176 [[package]] ··· 923 928 dependencies = [ 924 929 "serde_core", 925 930 "serde_derive", 931 + ] 932 + 933 + [[package]] 934 + name = "serde-big-array" 935 + version = "0.5.1" 936 + source = "registry+https://github.com/rust-lang/crates.io-index" 937 + checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" 938 + dependencies = [ 939 + "serde", 926 940 ] 927 941 928 942 [[package]]
+1 -1
Cargo.toml
··· 22 22 crate-type = ["cdylib", "rlib"] 23 23 24 24 [dependencies] 25 - fastnum = "0.7" 25 + fastnum = { version = "0.7", features = ["serde"] } 26 26 unicode-segmentation = "1.13" 27 27 web-time = "1.1.0" 28 28 reqwest = { version = "0.12", features = ["blocking", "json"] }
+41 -73
src/currency.rs
··· 2 2 3 3 use crate::units::Unit; 4 4 use fastnum::D128; 5 + use fastnum::decimal::Context; 6 + use serde::Deserialize; 5 7 use std::collections::HashMap; 6 8 use std::sync::RwLock; 7 9 ··· 15 17 /// The base currency for all exchange rates (EUR) 16 18 pub const BASE_CURRENCY: Unit = Unit::EUR; 17 19 18 - /// Initialize the currency cache by fetching rates from the API 19 - pub fn initialize_currency_cache() -> Result<(), String> { 20 - if CURRENCY_CACHE.read().unwrap().is_some() { 21 - return Ok(()); // Already initialized 22 - } 20 + #[derive(Deserialize, Debug)] 21 + struct CurrencyRate { 22 + #[allow(dead_code)] 23 + date: String, 24 + base: String, 25 + quote: String, 26 + rate: serde_json::Number, 27 + } 23 28 24 - let rates = fetch_currency_rates()?; 25 - let mut cache = HashMap::new(); 29 + fn set_currency_cache(rates: Vec<CurrencyRate>) -> Result<(), String> { 30 + let mut cache = HashMap::with_capacity(rates.len() + 1); 26 31 27 - // Store rates relative to EUR (base currency) 28 - // EUR to EUR is 1 32 + // Add EUR as base 29 33 cache.insert(BASE_CURRENCY, D128::from(1)); 30 34 31 - for (quote_currency, rate) in rates { 32 - cache.insert(quote_currency, rate); 35 + for entry in rates { 36 + if entry.base != "EUR" { 37 + return Err("Exchange rate base currency must be EUR".to_string()); 38 + } 39 + if let Ok(quote_unit) = currency_code_to_unit(&entry.quote) { 40 + let rate_str = entry.rate.to_string(); 41 + let rate = D128::parse_str(&rate_str, Context::default()); 42 + cache.insert(quote_unit, rate); 43 + } 33 44 } 34 45 35 46 *CURRENCY_CACHE.write().unwrap() = Some(cache); 36 47 Ok(()) 37 48 } 38 49 50 + #[cfg(not(target_arch = "wasm32"))] 51 + pub fn initialize_currency_cache() -> Result<(), String> { 52 + if CURRENCY_CACHE.read().unwrap().is_some() { 53 + return Ok(()); // Already initialized 54 + } 55 + 56 + let rates = fetch_currency_rates()?; 57 + set_currency_cache(rates)?; 58 + Ok(()) 59 + } 60 + 39 61 /// Get the exchange rate from one currency to another 40 62 /// Both currencies must be currency units 41 63 pub fn get_exchange_rate(from: Unit, to: Unit) -> Result<D128, String> { ··· 44 66 } 45 67 46 68 // Ensure cache is initialized 69 + #[cfg(not(target_arch = "wasm32"))] 47 70 initialize_currency_cache()?; 48 71 49 72 let cache = CURRENCY_CACHE.read().unwrap(); ··· 64 87 65 88 /// Fetch currency rates from the Frankfurter API (native version) 66 89 #[cfg(not(target_arch = "wasm32"))] 67 - fn fetch_currency_rates() -> Result<Vec<(Unit, D128)>, String> { 90 + fn fetch_currency_rates() -> Result<Vec<CurrencyRate>, String> { 68 91 use reqwest::blocking::get; 69 - use serde::Deserialize; 70 - 71 - #[derive(Deserialize)] 72 - struct RateEntry { 73 - #[allow(dead_code)] 74 - date: String, 75 - base: String, 76 - quote: String, 77 - rate: f64, 78 - } 79 92 80 93 let url = "https://api.frankfurter.dev/v2/rates?base=EUR"; 81 94 let response = get(url).map_err(|e| format!("Failed to fetch currency rates: {}", e))?; 82 95 83 - let rates: Vec<RateEntry> = response 96 + let rates: Vec<CurrencyRate> = response 84 97 .json() 85 - .map_err(|e| format!("Failed to parse currency rates: {}", e))?; 98 + .map_err(|e| format!("Failed to parse currency rates: {:?}", e))?; 86 99 87 - let mut result = Vec::new(); 88 - for entry in rates { 89 - if entry.base == "EUR" { 90 - // Only process currencies that we support 91 - if let Ok(quote_unit) = currency_code_to_unit(&entry.quote) { 92 - let rate = D128::from_f64(entry.rate); 93 - result.push((quote_unit, rate)); 94 - } 95 - } 96 - } 97 - 98 - Ok(result) 99 - } 100 - 101 - /// Fetch currency rates for WASM target 102 - /// Returns empty - the web app should fetch real rates 103 - /// and call init_currency_cache_with_json 104 - #[cfg(target_arch = "wasm32")] 105 - fn fetch_currency_rates() -> Result<Vec<(Unit, D128)>, String> { 106 - // Return empty for WASM - the web app must call init_currency_cache_with_json 107 - // with rates fetched via JavaScript 108 - Ok(Vec::new()) 100 + Ok(rates) 109 101 } 110 102 111 103 /// Convert currency code to Unit enum ··· 131 123 #[cfg(target_arch = "wasm32")] 132 124 #[wasm_bindgen] 133 125 pub fn init_currency_cache_with_json(rates_json: &str) -> Result<(), JsValue> { 134 - use serde::Deserialize; 126 + let rates: Vec<CurrencyRate> = serde_json::from_str(rates_json) 127 + .map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?; 135 128 136 - #[derive(Deserialize)] 137 - struct RateEntry { 138 - #[allow(dead_code)] 139 - date: Option<String>, 140 - base: String, 141 - quote: String, 142 - rate: f64, 143 - } 144 - 145 - // Parse the JSON array of rate entries 146 - let rates: Vec<RateEntry> = serde_json::from_str(rates_json) 129 + set_currency_cache(rates) 147 130 .map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e)))?; 148 131 149 - let mut cache = HashMap::new(); 150 - 151 - // Add EUR as base 152 - cache.insert(BASE_CURRENCY, D128::from(1)); 153 - 154 - // Parse individual rates - only accept entries where base is EUR 155 - for entry in rates { 156 - if entry.base == "EUR" { 157 - if let Ok(unit) = currency_code_to_unit(&entry.quote) { 158 - cache.insert(unit, D128::from_f64(entry.rate)); 159 - } 160 - } 161 - } 162 - 163 - *CURRENCY_CACHE.write().unwrap() = Some(cache); 164 132 Ok(()) 165 133 }
+7 -5
web/src/routes/+page.svelte
··· 17 17 try { 18 18 const response = await fetch("https://api.frankfurter.dev/v2/rates?base=EUR"); 19 19 const ratesJson = await response.text(); 20 - if (mod.init_currency_cache_with_json) { 21 - mod.init_currency_cache_with_json(ratesJson); 20 + if (typeof mod.init_currency_cache_with_json !== 'function') { 21 + output = 'Error: Missing currency init function' 22 22 } 23 + mod.init_currency_cache_with_json(ratesJson); 23 24 } catch (e) { 24 25 console.error("Failed to fetch currency rates:", e); 26 + output = "Error: Failed to fetch currency rates:", e 25 27 } 26 28 }); 27 29 28 - function wasm_eval(input: string) { 29 - if (!cpc || input.trim().length === 0) { 30 + function wasm_eval() { 31 + if (input === '' || !cpc || input.trim().length === 0) { 30 32 return ""; 31 33 } 32 34 try { ··· 37 39 } 38 40 39 41 let input = $state(""); 40 - let output = $derived(wasm_eval(input)); 42 + let output = $derived(wasm_eval()); 41 43 let calc_history = new PersistedState< 42 44 { id: number; in: string; out: string }[] 43 45 >("calc_history", []);