metrify/src/format.rs

435 lines
12 KiB
Rust

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, "")
} 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, "")
} 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, ""), prefixed_unit(MetricQuantity {
amount: 1.0,
unit: Metric::SquareMetre,
}));
assert_eq!(PrefixedUnit(1.0, ""), prefixed_unit(MetricQuantity {
amount: 10.0,
unit: Metric::SquareMetre,
}));
assert_eq!(PrefixedUnit(1.0, ""), prefixed_unit(MetricQuantity {
amount: 100.0,
unit: Metric::SquareMetre,
}));
assert_eq!(PrefixedUnit(1.0, ""), prefixed_unit(MetricQuantity {
amount: 1_000.0,
unit: Metric::SquareMetre,
}));
assert_eq!(PrefixedUnit(1.0, ""), prefixed_unit(MetricQuantity {
amount: 10_000.0,
unit: Metric::SquareMetre,
}));
assert_eq!(PrefixedUnit(1.0, ""), 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, ""), prefixed_unit(MetricQuantity {
amount: 1.0,
unit: Metric::CubicMetre,
}));
assert_eq!(PrefixedUnit(1.0, ""), 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,
}));
}
}