use crate::units::{Metric, MetricQuantity}; pub fn format(quantity: MetricQuantity) -> String { let PrefixedUnit(multiplier, unit) = prefixed_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.is_empty() { format!("{sign}{grouped}") } else { format!("{sign}{grouped}.{decimal}") } } #[derive(Debug, PartialEq)] struct PrefixedUnit(f64, &'static str); fn prefixed_unit(quantity: MetricQuantity) -> PrefixedUnit { let absolute = quantity.amount.abs(); match quantity.unit { Metric::Metre => { if absolute >= 1000.0 { PrefixedUnit(1000.0, "km") } else if absolute >= 1.0 { PrefixedUnit(1.0, "m") } else if absolute >= 0.01 { PrefixedUnit(0.01, "cm") } else { PrefixedUnit(0.001, "mm") } } Metric::Gram => { if absolute >= 1000.0 { PrefixedUnit(1000.0, "kg") } else { PrefixedUnit(1.0, "g") } } Metric::Celsius => PrefixedUnit(1.0, "°C"), Metric::SquareMetre => { if absolute >= 1000.0 * 1000.0 { PrefixedUnit(1000.0 * 1000.0, "km²") } else if absolute >= 1.0 { PrefixedUnit(1.0, "m²") } else if absolute >= 0.01 * 0.01 { PrefixedUnit(0.01 * 0.01, "cm²") } else { PrefixedUnit(0.001 * 0.001, "mm²") } } Metric::CubicMetre => { if absolute >= 1000.0 * 1000.0 * 1000.0 { PrefixedUnit(1000.0 * 1000.0 * 1000.0, "km³") } else if absolute >= 1.0 { PrefixedUnit(1.0, "m³") } else if absolute >= 0.000_001 { // 0.01 * 0.01 * 0.01 rounds wrong PrefixedUnit(0.000_001, "cm³") } else { PrefixedUnit(0.001 * 0.001 * 0.001, "mm³") } } Metric::Litre => { if absolute >= 1.0 { PrefixedUnit(1.0, "l") } else if absolute >= 0.1 { PrefixedUnit(0.1, "dl") } else if absolute >= 0.01 { PrefixedUnit(0.01, "cl") } else { PrefixedUnit(0.001, "ml") } } } } #[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, })); assert_eq!("1 000 °C", &format(MetricQuantity { amount: 1_000.0, unit: Metric::Celsius, })); } #[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 metres() { assert_eq!(PrefixedUnit(0.001, "mm"), prefixed_unit(MetricQuantity { amount: 0.0001, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(0.001, "mm"), prefixed_unit(MetricQuantity { amount: 0.001, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(0.01, "cm"), prefixed_unit(MetricQuantity { amount: 0.01, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(0.01, "cm"), prefixed_unit(MetricQuantity { amount: 0.1, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1.0, "m"), prefixed_unit(MetricQuantity { amount: 1.0, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1.0, "m"), prefixed_unit(MetricQuantity { amount: 10.0, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1.0, "m"), prefixed_unit(MetricQuantity { amount: 100.0, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1000.0, "km"), prefixed_unit(MetricQuantity { amount: 1000.0, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1000.0, "km"), prefixed_unit(MetricQuantity { amount: 10_000.0, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(0.001, "mm"), prefixed_unit(MetricQuantity { amount: -0.001, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(0.01, "cm"), prefixed_unit(MetricQuantity { amount: -0.01, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1.0, "m"), prefixed_unit(MetricQuantity { amount: -1.0, unit: Metric::Metre, })); assert_eq!(PrefixedUnit(1000.0, "km"), prefixed_unit(MetricQuantity { amount: -1000.0, unit: Metric::Metre, })); } #[test] fn grams() { assert_eq!(PrefixedUnit(1.0, "g"), prefixed_unit(MetricQuantity { amount: 0.1, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1.0, "g"), prefixed_unit(MetricQuantity { amount: 1.0, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1.0, "g"), prefixed_unit(MetricQuantity { amount: 10.0, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1.0, "g"), prefixed_unit(MetricQuantity { amount: 100.0, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1000.0, "kg"), prefixed_unit(MetricQuantity { amount: 1000.0, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1000.0, "kg"), prefixed_unit(MetricQuantity { amount: 10_1000.0, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1.0, "g"), prefixed_unit(MetricQuantity { amount: -1.0, unit: Metric::Gram, })); assert_eq!(PrefixedUnit(1000.0, "kg"), prefixed_unit(MetricQuantity { amount: -1000.0, unit: Metric::Gram, })); } #[test] fn celsius() { assert_eq!(PrefixedUnit(1.0, "°C"), prefixed_unit(MetricQuantity { amount: 0.0001, unit: Metric::Celsius, })); assert_eq!(PrefixedUnit(1.0, "°C"), prefixed_unit(MetricQuantity { amount: -1.0, unit: Metric::Celsius, })); assert_eq!(PrefixedUnit(1.0, "°C"), prefixed_unit(MetricQuantity { amount: 1_000.0, unit: Metric::Celsius, })); } #[test] fn square_metres() { assert_eq!(PrefixedUnit(0.000_001, "mm²"), prefixed_unit(MetricQuantity { amount: 0.000_000_1, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_001, "mm²"), prefixed_unit(MetricQuantity { amount: 0.000_001, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_001, "mm²"), prefixed_unit(MetricQuantity { amount: 0.000_01, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_1, "cm²"), prefixed_unit(MetricQuantity { amount: 0.000_1, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_1, "cm²"), prefixed_unit(MetricQuantity { amount: 0.001, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_1, "cm²"), prefixed_unit(MetricQuantity { amount: 0.01, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_1, "cm²"), prefixed_unit(MetricQuantity { amount: 0.01, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(0.000_1, "cm²"), prefixed_unit(MetricQuantity { amount: 0.1, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1.0, "m²"), prefixed_unit(MetricQuantity { amount: 1.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1.0, "m²"), prefixed_unit(MetricQuantity { amount: 10.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1.0, "m²"), prefixed_unit(MetricQuantity { amount: 100.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1.0, "m²"), prefixed_unit(MetricQuantity { amount: 1_000.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1.0, "m²"), prefixed_unit(MetricQuantity { amount: 10_000.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1.0, "m²"), prefixed_unit(MetricQuantity { amount: 100_000.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1_000_000.0, "km²"), prefixed_unit(MetricQuantity { amount: 1_000_000.0, unit: Metric::SquareMetre, })); assert_eq!(PrefixedUnit(1_000_000.0, "km²"), prefixed_unit(MetricQuantity { amount: 10_000_000.0, unit: Metric::SquareMetre, })); } #[test] fn cubic_metres() { assert_eq!(PrefixedUnit(0.000_000_001, "mm³"), prefixed_unit(MetricQuantity { amount: 0.000_000_000_1, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(0.000_000_001, "mm³"), prefixed_unit(MetricQuantity { amount: 0.000_000_001, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(0.000_000_001, "mm³"), prefixed_unit(MetricQuantity { amount: 0.000_000_1, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(0.000_001, "cm³"), prefixed_unit(MetricQuantity { amount: 0.000_001, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(0.000_001, "cm³"), prefixed_unit(MetricQuantity { amount: 0.1, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(1.0, "m³"), prefixed_unit(MetricQuantity { amount: 1.0, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(1.0, "m³"), prefixed_unit(MetricQuantity { amount: 100_000_000.0, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(1_000_000_000.0, "km³"), prefixed_unit(MetricQuantity { amount: 1_000_000_000.0, unit: Metric::CubicMetre, })); assert_eq!(PrefixedUnit(1_000_000_000.0, "km³"), prefixed_unit(MetricQuantity { amount: 10_000_000_000.0, unit: Metric::CubicMetre, })); } #[test] fn litre() { assert_eq!(PrefixedUnit(0.001, "ml"), prefixed_unit(MetricQuantity { amount: 0.000_1, unit: Metric::Litre, })); assert_eq!(PrefixedUnit(0.001, "ml"), prefixed_unit(MetricQuantity { amount: 0.001, unit: Metric::Litre, })); assert_eq!(PrefixedUnit(0.01, "cl"), prefixed_unit(MetricQuantity { amount: 0.01, unit: Metric::Litre, })); assert_eq!(PrefixedUnit(0.1, "dl"), prefixed_unit(MetricQuantity { amount: 0.1, unit: Metric::Litre, })); assert_eq!(PrefixedUnit(1.0, "l"), prefixed_unit(MetricQuantity { amount: 1.0, unit: Metric::Litre, })); assert_eq!(PrefixedUnit(1.0, "l"), prefixed_unit(MetricQuantity { amount: 10.0, unit: Metric::Litre, })); } }