From e391a91c5ca167ebb49659e395467f83518849dd Mon Sep 17 00:00:00 2001 From: Kasper Date: Mon, 29 Jun 2026 11:23:54 +0200 Subject: [PATCH] Limit sig figs (precision) of exchange rates --- src/currency.rs | 17 +++++++++--- src/lib.rs | 4 +-- src/units.rs | 72 ++++++++++++++++++++++++++++++++----------------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/src/currency.rs b/src/currency.rs index 75038d6..5298c13 100644 --- a/src/currency.rs +++ b/src/currency.rs @@ -58,6 +58,17 @@ pub fn initialize_currency_cache() -> Result<(), String> { Ok(()) } +/// Limit the sig figs (precision) of the number +fn round_to_sig_figs(x: D128, sig_figs: i32) -> D128 { + if x.is_zero() { + return x; + } + let mag: i32 = x.log10().floor().try_into().unwrap(); + let shift = sig_figs - 1 - mag; + let factor = D128::TEN.powi(shift); + (x * factor).round(0) / factor +} + /// Get the exchange rate from one currency to another /// Both currencies must be currency units pub fn get_exchange_rate(from: Unit, to: Unit) -> Result { @@ -80,9 +91,9 @@ pub fn get_exchange_rate(from: Unit, to: Unit) -> Result { .get(&to) .ok_or_else(|| format!("No exchange rate found for {:?}", to))?; - // Convert from -> EUR -> to - // rate = (to / EUR) / (from / EUR) = to / from - Ok(*to_rate / *from_rate) + let rate = *to_rate / *from_rate; + let rounded_rate = round_to_sig_figs(rate, 6); + Ok(rounded_rate) } /// Fetch currency rates from the Frankfurter API (native version) diff --git a/src/lib.rs b/src/lib.rs index 831c855..dd88037 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,8 +84,8 @@ impl Number { pub fn primitive_unit(&self) -> Vec<(Unit, isize)> { primitive_unit(&self.unit) } - pub fn contains_primitive(&self, unit: UnitType) -> bool { - self.unit.iter().any(|(u, _)| u.category() == unit) + pub fn contains_category(&self, category: UnitType) -> bool { + self.unit.iter().any(|(u, _)| u.category() == category) } fn get_unit_string(&self, plural: bool) -> String { let mut s = String::new(); diff --git a/src/units.rs b/src/units.rs index 0317846..61760fd 100644 --- a/src/units.rs +++ b/src/units.rs @@ -582,6 +582,22 @@ fn integer_power(base: D128, exp: isize) -> D128 { } } +fn contains_category(unit: &[(Unit, isize)], category: UnitType) -> bool { + unit.iter().any(|(u, _)| u.category() == category) +} + +/// Get the non-currency weight of a unit vector +fn non_currency_weight(unit: &[(Unit, isize)]) -> D128 { + use UnitType::*; + unit.iter().fold(D128::from(1), |acc, (u, exp)| { + if u.category() == Currency { + acc + } else { + acc * integer_power(u.weight(), *exp) + } + }) +} + /// Convert a [`Number`] to a specified [`Unit`]. pub fn convert(number: Number, to_unit: Vec<(Unit, isize)>) -> Result { if number.primitive_unit() != primitive_unit(&to_unit) { @@ -621,29 +637,35 @@ pub fn convert(number: Number, to_unit: Vec<(Unit, isize)>) -> Result= d!(1) { - // For input amounts >= 1, round result to 6 decimal places - (value * d!(1000000)).round(0) / d!(1000000) - } else { - // For input amounts < 1, keep full precision to handle very small amounts - value - }; + let value = number.value * rate * source_non_currency / target_non_currency; - Ok(Number { - value: value, - unit: to_unit.to_vec(), - }) + Ok(Number { + value, + unit: to_unit.to_vec(), + }) + } else { + Err("Currency conversion requires both units to have currency".to_string()) + } } else { let source_weight = combined_weight(&number.unit); let target_weight = combined_weight(&to_unit); @@ -675,7 +697,7 @@ pub fn add(left: Number, right: Number) -> Result { if left.unit == right.unit { Ok(Number::with_unit(left.value + right.value, left.unit)) } else if left.primitive_unit() == right.primitive_unit() - && !left.contains_primitive(Temperature) + && !left.contains_category(Temperature) { let (left, right) = convert_to_lowest(left, right)?; Ok(Number::with_unit(left.value + right.value, left.unit)) @@ -689,7 +711,7 @@ pub fn subtract(left: Number, right: Number) -> Result { if left.unit == right.unit { Ok(Number::with_unit(left.value - right.value, left.unit)) } else if left.primitive_unit() == right.primitive_unit() - && !left.contains_primitive(Temperature) + && !left.contains_category(Temperature) { let (left, right) = convert_to_lowest(left, right)?; Ok(Number::with_unit(left.value - right.value, left.unit)) @@ -893,7 +915,7 @@ pub fn to_ideal_unit(number: Number) -> Number { /// /// Temperatures don't work pub fn multiply(left: Number, right: Number) -> Result { - if left.contains_primitive(Temperature) || right.contains_primitive(Temperature) { + if left.contains_category(Temperature) || right.contains_category(Temperature) { Err(format!("Cannot multiply {} and {}", left, right)) } else { multiply_any(left, right) @@ -919,7 +941,7 @@ pub(crate) fn multiply_any(left: Number, right: Number) -> Result Result { - if left.contains_primitive(Temperature) || right.contains_primitive(Temperature) { + if left.contains_category(Temperature) || right.contains_category(Temperature) { Err(format!("Cannot divide {} by {}", left, right)) } else { divide_any(left, right) @@ -945,7 +967,7 @@ pub fn divide_any(left: Number, right: Number) -> Result { /// /// Temperatures don't work. pub fn modulo(left: Number, right: Number) -> Result { - if left.contains_primitive(Temperature) || right.contains_primitive(Temperature) { + if left.contains_category(Temperature) || right.contains_category(Temperature) { Err(format!("Cannot modulo {} by {}", left, right)) } else if left.primitive_unit() == right.primitive_unit() { // 5 km % 3 m @@ -964,7 +986,7 @@ pub fn modulo(left: Number, right: Number) -> Result { /// - etc. pub fn pow(left: Number, right: Number) -> Result { // I tried converting `right` to use powi, but somehow that was slower - if left.contains_primitive(Temperature) || right.has_unit() { + if left.contains_category(Temperature) || right.has_unit() { Err(format!("Cannot raise {} to the power of {}", left, right)) } else if left.is_unitless() { let result = left.value.pow(right.value); -- 2.51.2