use crate::units::{Metric, MetricQuantity}; pub fn format(quantity: MetricQuantity) -> String { let SiUnit(multiplier, unit) = si_unit(quantity); let amount = quantity.amount / multiplier; let amount = format_number(amount); format!("{amount} {unit}") } fn format_number(number: f64) -> String { let sign = if number < 0.0 { "-" } else { "" }; let number = number.abs(); // Lower the number of decimal digits as the number grows, to try // to maintain four significant figures let precision = if number < 1.0 { 4 } else if number < 10.0 { 3 } else if number < 100.0 { 2 } else if number < 1000.0 { 1 } else { 0 }; // Split number into integer and decimal parts for further processing let formatted = format!("{number:.precision$}"); let mut formatted = formatted.split('.'); let integer = formatted.next().expect("f64 formatted with .precision$ must have a part before '.'"); let decimal = if precision > 0 { formatted.next().expect("f64 formatted with .precision$ must have a part after '.' if precision > 0") } else { "" }; // Group the integer part into groups of three, e.g. 1000 → 10 000 let mut grouped = String::new(); let mut group_length = 0; for c in integer.chars().rev() { if group_length == 3 { grouped.push(' '); group_length = 0; } grouped.push(c); group_length += 1; } let grouped: String = grouped.chars().rev().collect(); // Remove trailing zeroes let decimal = decimal.trim_end_matches('0'); if decimal.len() == 0 { format!("{sign}{grouped}") } else { format!("{sign}{grouped}.{decimal}") } } #[derive(Debug, PartialEq)] struct SiUnit(f64, &'static str); fn si_unit(quantity: MetricQuantity) -> SiUnit { let absolute = quantity.amount.abs(); match quantity.unit { Metric::Metre => { if absolute >= 1000.0 { return SiUnit(1000.0, "km"); } else if absolute >= 1.0 { return SiUnit(1.0, "m"); } else if absolute >= 0.01 { return SiUnit(0.01, "cm"); } else { return SiUnit(0.001, "mm"); } } Metric::Gram => { if absolute >= 1000.0 { return SiUnit(1000.0, "kg"); } else { return SiUnit(1.0, "g"); } } } } #[cfg(test)] mod test { use super::*; #[test] fn quantities() { assert_eq!("0.1 mm", &format(MetricQuantity { amount: 0.000_1, unit: Metric::Metre, })); assert_eq!("-1 m", &format(MetricQuantity { amount: -1.0, unit: Metric::Metre, })); assert_eq!("1.5 m", &format(MetricQuantity { amount: 1.5, unit: Metric::Metre, })); assert_eq!("1 000 km", &format(MetricQuantity { amount: 1_000_000.0, unit: Metric::Metre, })); assert_eq!("-5 g", &format(MetricQuantity { amount: -5.0, unit: Metric::Gram, })); assert_eq!("3.2 kg", &format(MetricQuantity { amount: 3_200.0, unit: Metric::Gram, })); } #[test] fn numbers() { assert_eq!(&format_number(0.0001), "0.0001"); assert_eq!(&format_number(0.001), "0.001"); assert_eq!(&format_number(0.01), "0.01"); assert_eq!(&format_number(0.1), "0.1"); assert_eq!(&format_number(1.0), "1"); assert_eq!(&format_number(10.0), "10"); assert_eq!(&format_number(100.0), "100"); assert_eq!(&format_number(1_000.0), "1 000"); assert_eq!(&format_number(10_000.0), "10 000"); assert_eq!(&format_number(100_000.0), "100 000"); assert_eq!(&format_number(1_000_000.0), "1 000 000"); assert_eq!(&format_number(0.00001), "0"); assert_eq!(&format_number(1.0001), "1"); assert_eq!(&format_number(1.001), "1.001"); assert_eq!(&format_number(10.001), "10"); assert_eq!(&format_number(10.01), "10.01"); assert_eq!(&format_number(100.01), "100"); assert_eq!(&format_number(100.1), "100.1"); assert_eq!(&format_number(1_000.1), "1 000"); assert_eq!(&format_number(-1.0), "-1"); assert_eq!(&format_number(-100.0), "-100"); assert_eq!(&format_number(-1000.0), "-1 000"); } #[test] fn units() { assert_eq!(SiUnit(0.001, "mm"), si_unit(MetricQuantity { amount: 0.0001, unit: Metric::Metre, })); assert_eq!(SiUnit(0.001, "mm"), si_unit(MetricQuantity { amount: 0.001, unit: Metric::Metre, })); assert_eq!(SiUnit(0.01, "cm"), si_unit(MetricQuantity { amount: 0.01, unit: Metric::Metre, })); assert_eq!(SiUnit(0.01, "cm"), si_unit(MetricQuantity { amount: 0.1, unit: Metric::Metre, })); assert_eq!(SiUnit(1.0, "m"), si_unit(MetricQuantity { amount: 1.0, unit: Metric::Metre, })); assert_eq!(SiUnit(1.0, "m"), si_unit(MetricQuantity { amount: 10.0, unit: Metric::Metre, })); assert_eq!(SiUnit(1.0, "m"), si_unit(MetricQuantity { amount: 100.0, unit: Metric::Metre, })); assert_eq!(SiUnit(1000.0, "km"), si_unit(MetricQuantity { amount: 1000.0, unit: Metric::Metre, })); assert_eq!(SiUnit(1000.0, "km"), si_unit(MetricQuantity { amount: 10_000.0, unit: Metric::Metre, })); assert_eq!(SiUnit(0.001, "mm"), si_unit(MetricQuantity { amount: -0.001, unit: Metric::Metre, })); assert_eq!(SiUnit(0.01, "cm"), si_unit(MetricQuantity { amount: -0.01, unit: Metric::Metre, })); assert_eq!(SiUnit(1.0, "m"), si_unit(MetricQuantity { amount: -1.0, unit: Metric::Metre, })); assert_eq!(SiUnit(1000.0, "km"), si_unit(MetricQuantity { amount: -1000.0, unit: Metric::Metre, })); assert_eq!(SiUnit(1.0, "g"), si_unit(MetricQuantity { amount: 0.1, unit: Metric::Gram, })); assert_eq!(SiUnit(1.0, "g"), si_unit(MetricQuantity { amount: 1.0, unit: Metric::Gram, })); assert_eq!(SiUnit(1.0, "g"), si_unit(MetricQuantity { amount: 10.0, unit: Metric::Gram, })); assert_eq!(SiUnit(1.0, "g"), si_unit(MetricQuantity { amount: 100.0, unit: Metric::Gram, })); assert_eq!(SiUnit(1000.0, "kg"), si_unit(MetricQuantity { amount: 1000.0, unit: Metric::Gram, })); assert_eq!(SiUnit(1000.0, "kg"), si_unit(MetricQuantity { amount: 10_1000.0, unit: Metric::Gram, })); assert_eq!(SiUnit(1.0, "g"), si_unit(MetricQuantity { amount: -1.0, unit: Metric::Gram, })); assert_eq!(SiUnit(1000.0, "kg"), si_unit(MetricQuantity { amount: -1000.0, unit: Metric::Gram, })); } }