[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.

Limit sig figs (precision) of exchange rates

Kasper (Jun 29, 2026, 11:23 AM +0200) e391a91c e7c8c397

+63 -30
+14 -3
src/currency.rs
··· 58 58 Ok(()) 59 59 } 60 60 61 + /// Limit the sig figs (precision) of the number 62 + fn round_to_sig_figs(x: D128, sig_figs: i32) -> D128 { 63 + if x.is_zero() { 64 + return x; 65 + } 66 + let mag: i32 = x.log10().floor().try_into().unwrap(); 67 + let shift = sig_figs - 1 - mag; 68 + let factor = D128::TEN.powi(shift); 69 + (x * factor).round(0) / factor 70 + } 71 + 61 72 /// Get the exchange rate from one currency to another 62 73 /// Both currencies must be currency units 63 74 pub fn get_exchange_rate(from: Unit, to: Unit) -> Result<D128, String> { ··· 80 91 .get(&to) 81 92 .ok_or_else(|| format!("No exchange rate found for {:?}", to))?; 82 93 83 - // Convert from -> EUR -> to 84 - // rate = (to / EUR) / (from / EUR) = to / from 85 - Ok(*to_rate / *from_rate) 94 + let rate = *to_rate / *from_rate; 95 + let rounded_rate = round_to_sig_figs(rate, 6); 96 + Ok(rounded_rate) 86 97 } 87 98 88 99 /// Fetch currency rates from the Frankfurter API (native version)
+2 -2
src/lib.rs
··· 84 84 pub fn primitive_unit(&self) -> Vec<(Unit, isize)> { 85 85 primitive_unit(&self.unit) 86 86 } 87 - pub fn contains_primitive(&self, unit: UnitType) -> bool { 88 - self.unit.iter().any(|(u, _)| u.category() == unit) 87 + pub fn contains_category(&self, category: UnitType) -> bool { 88 + self.unit.iter().any(|(u, _)| u.category() == category) 89 89 } 90 90 fn get_unit_string(&self, plural: bool) -> String { 91 91 let mut s = String::new();
+47 -25
src/units.rs
··· 582 582 } 583 583 } 584 584 585 + fn contains_category(unit: &[(Unit, isize)], category: UnitType) -> bool { 586 + unit.iter().any(|(u, _)| u.category() == category) 587 + } 588 + 589 + /// Get the non-currency weight of a unit vector 590 + fn non_currency_weight(unit: &[(Unit, isize)]) -> D128 { 591 + use UnitType::*; 592 + unit.iter().fold(D128::from(1), |acc, (u, exp)| { 593 + if u.category() == Currency { 594 + acc 595 + } else { 596 + acc * integer_power(u.weight(), *exp) 597 + } 598 + }) 599 + } 600 + 585 601 /// Convert a [`Number`] to a specified [`Unit`]. 586 602 pub fn convert(number: Number, to_unit: Vec<(Unit, isize)>) -> Result<Number, String> { 587 603 if number.primitive_unit() != primitive_unit(&to_unit) { ··· 621 637 Number::with_unit(d!(0), to_unit).plural() 622 638 )), 623 639 } 624 - } else if number.primitive_unit() == Currency.primitive() { 625 - let from_currency = number.unit[0].0; 626 - let to_currency = to_unit[0].0; 640 + } else if number.contains_category(Currency) && contains_category(&to_unit, Currency) { 641 + // Handle compound units with currency, like "EUR/liter" 642 + // Find the currency in both units 643 + let from_currency = number 644 + .unit 645 + .iter() 646 + .find(|(u, _)| u.category() == Currency) 647 + .map(|(u, _)| *u); 648 + let to_currency = to_unit 649 + .iter() 650 + .find(|(u, _)| u.category() == Currency) 651 + .map(|(u, _)| *u); 627 652 628 - // Get the exchange rate from the currency module 629 - let rate = currency::get_exchange_rate(from_currency, to_currency)?; 653 + if let (Some(from_curr), Some(to_curr)) = (from_currency, to_currency) { 654 + let rate = currency::get_exchange_rate(from_curr, to_curr)?; 630 655 631 - let value = number.value * rate; 656 + // Calculate the ratio of non-currency parts 657 + let source_non_currency = non_currency_weight(&number.unit); 658 + let target_non_currency = non_currency_weight(&to_unit); 632 659 633 - // For currency conversions, limit precision to match source data 634 - // This preserves small amounts while avoiding excessive decimals for normal amounts 635 - let value = if number.value.abs() >= d!(1) { 636 - // For input amounts >= 1, round result to 6 decimal places 637 - (value * d!(1000000)).round(0) / d!(1000000) 638 - } else { 639 - // For input amounts < 1, keep full precision to handle very small amounts 640 - value 641 - }; 660 + let value = number.value * rate * source_non_currency / target_non_currency; 642 661 643 - Ok(Number { 644 - value: value, 645 - unit: to_unit.to_vec(), 646 - }) 662 + Ok(Number { 663 + value, 664 + unit: to_unit.to_vec(), 665 + }) 666 + } else { 667 + Err("Currency conversion requires both units to have currency".to_string()) 668 + } 647 669 } else { 648 670 let source_weight = combined_weight(&number.unit); 649 671 let target_weight = combined_weight(&to_unit); ··· 675 697 if left.unit == right.unit { 676 698 Ok(Number::with_unit(left.value + right.value, left.unit)) 677 699 } else if left.primitive_unit() == right.primitive_unit() 678 - && !left.contains_primitive(Temperature) 700 + && !left.contains_category(Temperature) 679 701 { 680 702 let (left, right) = convert_to_lowest(left, right)?; 681 703 Ok(Number::with_unit(left.value + right.value, left.unit)) ··· 689 711 if left.unit == right.unit { 690 712 Ok(Number::with_unit(left.value - right.value, left.unit)) 691 713 } else if left.primitive_unit() == right.primitive_unit() 692 - && !left.contains_primitive(Temperature) 714 + && !left.contains_category(Temperature) 693 715 { 694 716 let (left, right) = convert_to_lowest(left, right)?; 695 717 Ok(Number::with_unit(left.value - right.value, left.unit)) ··· 893 915 /// 894 916 /// Temperatures don't work 895 917 pub fn multiply(left: Number, right: Number) -> Result<Number, String> { 896 - if left.contains_primitive(Temperature) || right.contains_primitive(Temperature) { 918 + if left.contains_category(Temperature) || right.contains_category(Temperature) { 897 919 Err(format!("Cannot multiply {} and {}", left, right)) 898 920 } else { 899 921 multiply_any(left, right) ··· 919 941 /// 920 942 /// Temperatures don't work. 921 943 pub fn divide(left: Number, right: Number) -> Result<Number, String> { 922 - if left.contains_primitive(Temperature) || right.contains_primitive(Temperature) { 944 + if left.contains_category(Temperature) || right.contains_category(Temperature) { 923 945 Err(format!("Cannot divide {} by {}", left, right)) 924 946 } else { 925 947 divide_any(left, right) ··· 945 967 /// 946 968 /// Temperatures don't work. 947 969 pub fn modulo(left: Number, right: Number) -> Result<Number, String> { 948 - if left.contains_primitive(Temperature) || right.contains_primitive(Temperature) { 970 + if left.contains_category(Temperature) || right.contains_category(Temperature) { 949 971 Err(format!("Cannot modulo {} by {}", left, right)) 950 972 } else if left.primitive_unit() == right.primitive_unit() { 951 973 // 5 km % 3 m ··· 964 986 /// - etc. 965 987 pub fn pow(left: Number, right: Number) -> Result<Number, String> { 966 988 // I tried converting `right` to use powi, but somehow that was slower 967 - if left.contains_primitive(Temperature) || right.has_unit() { 989 + if left.contains_category(Temperature) || right.has_unit() { 968 990 Err(format!("Cannot raise {} to the power of {}", left, right)) 969 991 } else if left.is_unitless() { 970 992 let result = left.value.pow(right.value);