Compare commits
6 Commits
188df1fa5c
...
81082fd11d
Author | SHA1 | Date |
---|---|---|
Juhani Krekelä | 81082fd11d | |
Juhani Krekelä | b3fa9a01f1 | |
Juhani Krekelä | f8db69e3b2 | |
Juhani Krekelä | d77a84fef1 | |
Juhani Krekelä | 3dee08d6d4 | |
Juhani Krekelä | 54118ca879 |
|
@ -2,12 +2,13 @@ use crate::units::{Metric, MetricQuantity, NonMetric, NonMetricQuantity};
|
||||||
|
|
||||||
pub fn convert(from: NonMetricQuantity) -> MetricQuantity {
|
pub fn convert(from: NonMetricQuantity) -> MetricQuantity {
|
||||||
let conversion = get_conversion(from.unit);
|
let conversion = get_conversion(from.unit);
|
||||||
let amount = from.amount * conversion.to.amount / conversion.from;
|
let amount = (from.amount - conversion.offset) * conversion.to.amount / conversion.from;
|
||||||
let unit = conversion.to.unit;
|
let unit = conversion.to.unit;
|
||||||
MetricQuantity { amount, unit }
|
MetricQuantity { amount, unit }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Conversion {
|
struct Conversion {
|
||||||
|
offset: f64,
|
||||||
from: f64,
|
from: f64,
|
||||||
to: MetricQuantity,
|
to: MetricQuantity,
|
||||||
}
|
}
|
||||||
|
@ -22,34 +23,47 @@ fn get_conversion(unit: NonMetric) -> Conversion {
|
||||||
match unit {
|
match unit {
|
||||||
// Length
|
// Length
|
||||||
NonMetric::Inch => Conversion {
|
NonMetric::Inch => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: inch_from,
|
from: inch_from,
|
||||||
to: MetricQuantity { amount: inch_to, unit: Metric::Metre },
|
to: MetricQuantity { amount: inch_to, unit: Metric::Metre },
|
||||||
},
|
},
|
||||||
NonMetric::Foot => Conversion {
|
NonMetric::Foot => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: inch_from,
|
from: inch_from,
|
||||||
to: MetricQuantity { amount: 12.0 * inch_to, unit: Metric::Metre },
|
to: MetricQuantity { amount: 12.0 * inch_to, unit: Metric::Metre },
|
||||||
},
|
},
|
||||||
NonMetric::Yard => Conversion {
|
NonMetric::Yard => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: inch_from,
|
from: inch_from,
|
||||||
to: MetricQuantity { amount: 3.0 * 12.0 * inch_to, unit: Metric::Metre },
|
to: MetricQuantity { amount: 3.0 * 12.0 * inch_to, unit: Metric::Metre },
|
||||||
},
|
},
|
||||||
NonMetric::Mile => Conversion {
|
NonMetric::Mile => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: inch_from,
|
from: inch_from,
|
||||||
to: MetricQuantity { amount: 1760.0 * 3.0 * 12.0 * inch_to, unit: Metric::Metre },
|
to: MetricQuantity { amount: 1760.0 * 3.0 * 12.0 * inch_to, unit: Metric::Metre },
|
||||||
},
|
},
|
||||||
// Weight
|
// Weight
|
||||||
NonMetric::Ounce => Conversion {
|
NonMetric::Ounce => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: 16.0 * pound_from,
|
from: 16.0 * pound_from,
|
||||||
to: MetricQuantity { amount: pound_to, unit: Metric::Gram },
|
to: MetricQuantity { amount: pound_to, unit: Metric::Gram },
|
||||||
},
|
},
|
||||||
NonMetric::Pound => Conversion {
|
NonMetric::Pound => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: pound_from,
|
from: pound_from,
|
||||||
to: MetricQuantity { amount: pound_to, unit: Metric::Gram },
|
to: MetricQuantity { amount: pound_to, unit: Metric::Gram },
|
||||||
},
|
},
|
||||||
NonMetric::Stone => Conversion {
|
NonMetric::Stone => Conversion {
|
||||||
|
offset: 0.0,
|
||||||
from: pound_from,
|
from: pound_from,
|
||||||
to: MetricQuantity { amount: 14.0 * pound_to, unit: Metric::Gram },
|
to: MetricQuantity { amount: 14.0 * pound_to, unit: Metric::Gram },
|
||||||
},
|
},
|
||||||
|
// Temperature
|
||||||
|
NonMetric::Fahrenheit => Conversion {
|
||||||
|
offset: 32.0,
|
||||||
|
from: 9.0,
|
||||||
|
to: MetricQuantity { amount: 5.0, unit: Metric::Celcius },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +94,24 @@ mod test {
|
||||||
run_tests(&tests, Metric::Gram);
|
run_tests(&tests, Metric::Gram);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temperature() {
|
||||||
|
assert_eq!(convert(NonMetricQuantity {
|
||||||
|
amount: -40.0,
|
||||||
|
unit: NonMetric::Fahrenheit,
|
||||||
|
}), MetricQuantity {
|
||||||
|
amount: -40.0,
|
||||||
|
unit: Metric::Celcius,
|
||||||
|
});
|
||||||
|
assert_eq!(convert(NonMetricQuantity {
|
||||||
|
amount: 32.0,
|
||||||
|
unit: NonMetric::Fahrenheit,
|
||||||
|
}), MetricQuantity {
|
||||||
|
amount: 0.0,
|
||||||
|
unit: Metric::Celcius,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn run_tests(tests: &[Test], unit: Metric) {
|
fn run_tests(tests: &[Test], unit: Metric) {
|
||||||
for test in tests {
|
for test in tests {
|
||||||
let from = NonMetricQuantity {
|
let from = NonMetricQuantity {
|
||||||
|
|
309
src/format.rs
309
src/format.rs
|
@ -1,66 +1,89 @@
|
||||||
use crate::units::{Metric, MetricQuantity};
|
use crate::units::{Metric, MetricQuantity};
|
||||||
|
|
||||||
pub fn format(quantity: MetricQuantity) -> String {
|
pub fn format(quantity: MetricQuantity) -> String {
|
||||||
let (amount, prefix) = si_prefix(quantity.amount);
|
let PrefixedUnit(multiplier, unit) = prefixed_unit(quantity);
|
||||||
let unit = abbreviation(quantity.unit);
|
let amount = quantity.amount / multiplier;
|
||||||
|
let amount = format_number(amount);
|
||||||
format!("{amount} {prefix}{unit}")
|
format!("{amount} {unit}")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
fn format_number(number: f64) -> String {
|
||||||
struct SiPrefix {
|
let sign = if number < 0.0 { "-" } else { "" };
|
||||||
prefix: &'static str,
|
let number = number.abs();
|
||||||
size: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SI_PREFIXES: [SiPrefix; 16] = [
|
// Lower the number of decimal digits as the number grows, to try
|
||||||
SiPrefix { prefix: "z", size: 0.000_000_000_000_000_000_001 },
|
// to maintain four significant figures
|
||||||
SiPrefix { prefix: "a", size: 0.000_000_000_000_000_001 },
|
let precision = if number < 1.0 {
|
||||||
SiPrefix { prefix: "f", size: 0.000_000_000_000_001 },
|
4
|
||||||
SiPrefix { prefix: "p", size: 0.000_000_000_001 },
|
} else if number < 10.0 {
|
||||||
SiPrefix { prefix: "n", size: 0.000_000_001 },
|
3
|
||||||
SiPrefix { prefix: "µ", size: 0.000_001 },
|
} else if number < 100.0 {
|
||||||
SiPrefix { prefix: "m", size: 0.001 },
|
2
|
||||||
SiPrefix { prefix: "c", size: 0.01 }, // Included for cm
|
} else if number < 1000.0 {
|
||||||
SiPrefix { prefix: "", size: 1.0 },
|
1
|
||||||
SiPrefix { prefix: "k", size: 1_000.0 },
|
} else {
|
||||||
SiPrefix { prefix: "M", size: 1_000_000.0 },
|
0
|
||||||
SiPrefix { prefix: "G", size: 1_000_000_000.0 },
|
};
|
||||||
SiPrefix { prefix: "T", size: 1_000_000_000_000.0 },
|
|
||||||
SiPrefix { prefix: "P", size: 1_000_000_000_000_000.0 },
|
|
||||||
SiPrefix { prefix: "E", size: 1_000_000_000_000_000_000.0 },
|
|
||||||
SiPrefix { prefix: "Z", size: 1_000_000_000_000_000_000_000.0 },
|
|
||||||
// Yotta and above cannot be represented exactly as a f64
|
|
||||||
];
|
|
||||||
|
|
||||||
fn si_prefix(amount: f64) -> (f64, &'static str) {
|
// Split number into integer and decimal parts for further processing
|
||||||
let absolute = amount.abs();
|
let formatted = format!("{number:.precision$}");
|
||||||
if absolute < SI_PREFIXES[0].size {
|
let mut formatted = formatted.split('.');
|
||||||
let prefix = SI_PREFIXES[0];
|
let integer = formatted.next().expect("f64 formatted with .precision$ must have a part before '.'");
|
||||||
return (amount / prefix.size, prefix.prefix);
|
let decimal = if precision > 0 {
|
||||||
}
|
formatted.next().expect("f64 formatted with .precision$ must have a part after '.' if precision > 0")
|
||||||
if absolute >= SI_PREFIXES[SI_PREFIXES.len() - 1].size {
|
} else {
|
||||||
let prefix = SI_PREFIXES[SI_PREFIXES.len() - 1];
|
""
|
||||||
return (amount / prefix.size, prefix.prefix);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Find the correct prefix SI_PREFIX[index] such that:
|
// Group the integer part into groups of three, e.g. 1000 → 10 000
|
||||||
// SI_PREFIXES[index].size ≤ absolute < SI_PREFIXES[index + 1].size
|
let mut grouped = String::new();
|
||||||
for index in 0..SI_PREFIXES.len() {
|
let mut group_length = 0;
|
||||||
if SI_PREFIXES[index].size <= absolute &&
|
for c in integer.chars().rev() {
|
||||||
absolute < SI_PREFIXES[index + 1].size {
|
if group_length == 3 {
|
||||||
let prefix = SI_PREFIXES[index];
|
grouped.push(' ');
|
||||||
return (amount / prefix.size, prefix.prefix);
|
group_length = 0;
|
||||||
}
|
}
|
||||||
|
grouped.push(c);
|
||||||
|
group_length += 1;
|
||||||
}
|
}
|
||||||
|
let grouped: String = grouped.chars().rev().collect();
|
||||||
|
|
||||||
unreachable!();
|
// Remove trailing zeroes
|
||||||
|
let decimal = decimal.trim_end_matches('0');
|
||||||
|
|
||||||
|
if decimal.len() == 0 {
|
||||||
|
format!("{sign}{grouped}")
|
||||||
|
} else {
|
||||||
|
format!("{sign}{grouped}.{decimal}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abbreviation(unit: Metric) -> &'static str {
|
#[derive(Debug, PartialEq)]
|
||||||
match unit {
|
struct PrefixedUnit(f64, &'static str);
|
||||||
Metric::Metre => "m",
|
|
||||||
Metric::Gram => "g",
|
fn prefixed_unit(quantity: MetricQuantity) -> PrefixedUnit {
|
||||||
|
let absolute = quantity.amount.abs();
|
||||||
|
|
||||||
|
match quantity.unit {
|
||||||
|
Metric::Metre => {
|
||||||
|
if absolute >= 1000.0 {
|
||||||
|
return PrefixedUnit(1000.0, "km");
|
||||||
|
} else if absolute >= 1.0 {
|
||||||
|
return PrefixedUnit(1.0, "m");
|
||||||
|
} else if absolute >= 0.01 {
|
||||||
|
return PrefixedUnit(0.01, "cm");
|
||||||
|
} else {
|
||||||
|
return PrefixedUnit(0.001, "mm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Metric::Gram => {
|
||||||
|
if absolute >= 1000.0 {
|
||||||
|
return PrefixedUnit(1000.0, "kg");
|
||||||
|
} else {
|
||||||
|
return PrefixedUnit(1.0, "g");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Metric::Celcius => PrefixedUnit(1.0, "°C"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,47 +93,167 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quantities() {
|
fn quantities() {
|
||||||
assert_eq!("1 m", &format(MetricQuantity {
|
assert_eq!("0.1 mm", &format(MetricQuantity {
|
||||||
amount: 1.0,
|
amount: 0.000_1,
|
||||||
unit: Metric::Metre,
|
unit: Metric::Metre,
|
||||||
}));
|
}));
|
||||||
assert_eq!("5 kg", &format(MetricQuantity {
|
assert_eq!("-1 m", &format(MetricQuantity {
|
||||||
amount: 5_000.0,
|
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,
|
unit: Metric::Gram,
|
||||||
}));
|
}));
|
||||||
assert_eq!("25.5 cm", &format(MetricQuantity {
|
assert_eq!("3.2 kg", &format(MetricQuantity {
|
||||||
amount: 0.255,
|
amount: 3_200.0,
|
||||||
unit: Metric::Metre,
|
unit: Metric::Gram,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!("1 000 °C", &format(MetricQuantity {
|
||||||
|
amount: 1_000.0,
|
||||||
|
unit: Metric::Celcius,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prefixes() {
|
fn numbers() {
|
||||||
assert_eq!(si_prefix(0.000_000_000_000_000_000_0005), (0.5, "z"));
|
assert_eq!(&format_number(0.0001), "0.0001");
|
||||||
assert_eq!(si_prefix(0.000_000_000_000_000_000_001), (1.0, "z"));
|
assert_eq!(&format_number(0.001), "0.001");
|
||||||
assert_eq!(si_prefix(0.000_000_000_000_000_000_01), (10.0, "z"));
|
assert_eq!(&format_number(0.01), "0.01");
|
||||||
assert_eq!(si_prefix(0.000_000_000_000_000_00_01), (100.0, "z"));
|
assert_eq!(&format_number(0.1), "0.1");
|
||||||
assert_eq!(si_prefix(0.000_000_000_000_000_001), (1.0, "a"));
|
assert_eq!(&format_number(1.0), "1");
|
||||||
assert_eq!(si_prefix(0.000_000_000_000_001), (1.0, "f"));
|
assert_eq!(&format_number(10.0), "10");
|
||||||
assert_eq!(si_prefix(0.000_000_000_001), (1.0, "p"));
|
assert_eq!(&format_number(100.0), "100");
|
||||||
assert_eq!(si_prefix(0.000_000_001), (1.0, "n"));
|
assert_eq!(&format_number(1_000.0), "1 000");
|
||||||
assert_eq!(si_prefix(0.000_001), (1.0, "µ"));
|
assert_eq!(&format_number(10_000.0), "10 000");
|
||||||
assert_eq!(si_prefix(0.001), (1.0, "m"));
|
assert_eq!(&format_number(100_000.0), "100 000");
|
||||||
assert_eq!(si_prefix(0.01), (1.0, "c"));
|
assert_eq!(&format_number(1_000_000.0), "1 000 000");
|
||||||
assert_eq!(si_prefix(1.0), (1.0, ""));
|
|
||||||
assert_eq!(si_prefix(1_000.0), (1.0, "k"));
|
|
||||||
assert_eq!(si_prefix(1_000_000.0), (1.0, "M"));
|
|
||||||
assert_eq!(si_prefix(1_000_000_000.0), (1.0, "G"));
|
|
||||||
assert_eq!(si_prefix(1_000_000_000_000.0), (1.0, "T"));
|
|
||||||
assert_eq!(si_prefix(1_000_000_000_000_000.0), (1.0, "P"));
|
|
||||||
assert_eq!(si_prefix(1_000_000_000_000_000_000.0), (1.0, "E"));
|
|
||||||
assert_eq!(si_prefix(10_000_000_000_000_000_000.0), (10.0, "E"));
|
|
||||||
assert_eq!(si_prefix(100_000_000_000_000_000_000.0), (100.0, "E"));
|
|
||||||
assert_eq!(si_prefix(1_000_000_000_000_000_000_000.0), (1.0, "Z"));
|
|
||||||
assert_eq!(si_prefix(2_000_000_000_000_000_000_000.0), (2.0, "Z"));
|
|
||||||
|
|
||||||
assert_eq!(si_prefix(-0.000_000_000_000_000_000_0005), (-0.5, "z"));
|
assert_eq!(&format_number(0.00001), "0");
|
||||||
assert_eq!(si_prefix(-0.5), (-50.0, "c"));
|
assert_eq!(&format_number(1.0001), "1");
|
||||||
assert_eq!(si_prefix(-2_000_000_000_000_000_000_000.0), (-2.0, "Z"));
|
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!(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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(PrefixedUnit(1.0, "°C"), prefixed_unit(MetricQuantity {
|
||||||
|
amount: 0.0001,
|
||||||
|
unit: Metric::Celcius,
|
||||||
|
}));
|
||||||
|
assert_eq!(PrefixedUnit(1.0, "°C"), prefixed_unit(MetricQuantity {
|
||||||
|
amount: -1.0,
|
||||||
|
unit: Metric::Celcius,
|
||||||
|
}));
|
||||||
|
assert_eq!(PrefixedUnit(1.0, "°C"), prefixed_unit(MetricQuantity {
|
||||||
|
amount: 1_000.0,
|
||||||
|
unit: Metric::Celcius,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/lib.rs
19
src/lib.rs
|
@ -70,6 +70,8 @@ fn unit_to_name(unit: NonMetric) -> &'static str {
|
||||||
NonMetric::Ounce => "ounces",
|
NonMetric::Ounce => "ounces",
|
||||||
NonMetric::Pound => "pounds",
|
NonMetric::Pound => "pounds",
|
||||||
NonMetric::Stone => "stones",
|
NonMetric::Stone => "stones",
|
||||||
|
// Temperature
|
||||||
|
NonMetric::Fahrenheit => "degrees Fahrenheit",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,4 +88,21 @@ mod test {
|
||||||
assert_eq!(run(""), Err("Expected quantity or quantities to convert".to_string()));
|
assert_eq!(run(""), Err("Expected quantity or quantities to convert".to_string()));
|
||||||
assert_eq!(run("6 ft 1 lbs"), Err("Incompatible units: feet, pounds".to_string()));
|
assert_eq!(run("6 ft 1 lbs"), Err("Incompatible units: feet, pounds".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conversions() {
|
||||||
|
// Length
|
||||||
|
assert_eq!(run("1 in"), Ok("2.54 cm".to_string()));
|
||||||
|
assert_eq!(run("1 ft"), Ok("30.48 cm".to_string()));
|
||||||
|
assert_eq!(run("1 yard"), Ok("91.44 cm".to_string()));
|
||||||
|
assert_eq!(run("1 mile"), Ok("1.609 km".to_string()));
|
||||||
|
// Weight
|
||||||
|
assert_eq!(run("1 oz"), Ok("28.35 g".to_string()));
|
||||||
|
assert_eq!(run("1 lb"), Ok("453.6 g".to_string()));
|
||||||
|
assert_eq!(run("1 st"), Ok("6.35 kg".to_string()));
|
||||||
|
// Temperature
|
||||||
|
assert_eq!(run("-40 °F"), Ok("-40 °C".to_string()));
|
||||||
|
assert_eq!(run("0 °F"), Ok("-17.78 °C".to_string()));
|
||||||
|
assert_eq!(run("32 °F"), Ok("0 °C".to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
163
src/parse.rs
163
src/parse.rs
|
@ -21,12 +21,7 @@ pub fn parse(input: &str) -> Result<Vec<NonMetricQuantity>, ParseError> {
|
||||||
for token in tokenize(input) {
|
for token in tokenize(input) {
|
||||||
match (&state, token) {
|
match (&state, token) {
|
||||||
(Expect::Number, Token::Number(number)) => {
|
(Expect::Number, Token::Number(number)) => {
|
||||||
let number = match number.trim().parse() {
|
let number = parse_number(number)?;
|
||||||
Ok(number) => number,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(ParseError::NotValidNumber(number));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
amount = Some(number);
|
amount = Some(number);
|
||||||
state = Expect::Unit;
|
state = Expect::Unit;
|
||||||
}
|
}
|
||||||
|
@ -37,12 +32,7 @@ pub fn parse(input: &str) -> Result<Vec<NonMetricQuantity>, ParseError> {
|
||||||
unreachable!("token stream can't contain two numbers in a row");
|
unreachable!("token stream can't contain two numbers in a row");
|
||||||
}
|
}
|
||||||
(Expect::Unit, Token::Unit(unit)) => {
|
(Expect::Unit, Token::Unit(unit)) => {
|
||||||
let unit = match parse_unit(&unit) {
|
let unit = parse_unit(unit)?;
|
||||||
Some(unit) => unit,
|
|
||||||
None => {
|
|
||||||
return Err(ParseError::UnknownUnit(unit));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let quantity = NonMetricQuantity {
|
let quantity = NonMetricQuantity {
|
||||||
amount: amount.take().expect("must have read a number to be in this state"),
|
amount: amount.take().expect("must have read a number to be in this state"),
|
||||||
unit: unit,
|
unit: unit,
|
||||||
|
@ -63,46 +53,55 @@ pub fn parse(input: &str) -> Result<Vec<NonMetricQuantity>, ParseError> {
|
||||||
Ok(quantities)
|
Ok(quantities)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_unit(input: &str) -> Option<NonMetric> {
|
fn parse_number(input: String) -> Result<f64, ParseError> {
|
||||||
match input {
|
let no_whitespace: String = input.chars().filter(|c| !c.is_whitespace()).collect();
|
||||||
|
no_whitespace.parse().or_else(|_| Err(ParseError::NotValidNumber(input)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unit(input: String) -> Result<NonMetric, ParseError> {
|
||||||
|
match input.as_str() {
|
||||||
// Length
|
// Length
|
||||||
"inch" => Some(NonMetric::Inch),
|
"inch" => Ok(NonMetric::Inch),
|
||||||
"inches" => Some(NonMetric::Inch),
|
"inches" => Ok(NonMetric::Inch),
|
||||||
"in" => Some(NonMetric::Inch),
|
"in" => Ok(NonMetric::Inch),
|
||||||
"\"" => Some(NonMetric::Inch),
|
"\"" => Ok(NonMetric::Inch),
|
||||||
"″" => Some(NonMetric::Inch),
|
"″" => Ok(NonMetric::Inch),
|
||||||
|
|
||||||
"foot" => Some(NonMetric::Foot),
|
"foot" => Ok(NonMetric::Foot),
|
||||||
"feet" => Some(NonMetric::Foot),
|
"feet" => Ok(NonMetric::Foot),
|
||||||
"ft" => Some(NonMetric::Foot),
|
"ft" => Ok(NonMetric::Foot),
|
||||||
"'" => Some(NonMetric::Foot),
|
"'" => Ok(NonMetric::Foot),
|
||||||
"′" => Some(NonMetric::Foot),
|
"′" => Ok(NonMetric::Foot),
|
||||||
|
|
||||||
"yard" => Some(NonMetric::Yard),
|
"yard" => Ok(NonMetric::Yard),
|
||||||
"yards" => Some(NonMetric::Yard),
|
"yards" => Ok(NonMetric::Yard),
|
||||||
"yd" => Some(NonMetric::Yard),
|
"yd" => Ok(NonMetric::Yard),
|
||||||
|
|
||||||
"mile" => Some(NonMetric::Mile),
|
"mile" => Ok(NonMetric::Mile),
|
||||||
"miles" => Some(NonMetric::Mile),
|
"miles" => Ok(NonMetric::Mile),
|
||||||
"mi" => Some(NonMetric::Mile),
|
"mi" => Ok(NonMetric::Mile),
|
||||||
"m" => Some(NonMetric::Mile),
|
"m" => Ok(NonMetric::Mile),
|
||||||
|
|
||||||
// Weight
|
// Weight
|
||||||
"ounce" => Some(NonMetric::Ounce),
|
"ounce" => Ok(NonMetric::Ounce),
|
||||||
"ounces" => Some(NonMetric::Ounce),
|
"ounces" => Ok(NonMetric::Ounce),
|
||||||
"oz" => Some(NonMetric::Ounce),
|
"oz" => Ok(NonMetric::Ounce),
|
||||||
|
|
||||||
"pound" => Some(NonMetric::Pound),
|
"pound" => Ok(NonMetric::Pound),
|
||||||
"pounds" => Some(NonMetric::Pound),
|
"pounds" => Ok(NonMetric::Pound),
|
||||||
"lb" => Some(NonMetric::Pound),
|
"lb" => Ok(NonMetric::Pound),
|
||||||
"lbs" => Some(NonMetric::Pound),
|
"lbs" => Ok(NonMetric::Pound),
|
||||||
"#" => Some(NonMetric::Pound),
|
"#" => Ok(NonMetric::Pound),
|
||||||
|
|
||||||
"stone" => Some(NonMetric::Stone),
|
"stone" => Ok(NonMetric::Stone),
|
||||||
"stones" => Some(NonMetric::Stone),
|
"stones" => Ok(NonMetric::Stone),
|
||||||
"st" => Some(NonMetric::Stone),
|
"st" => Ok(NonMetric::Stone),
|
||||||
|
|
||||||
_ => None,
|
// Temperature
|
||||||
|
"°F" => Ok(NonMetric::Fahrenheit),
|
||||||
|
"F" => Ok(NonMetric::Fahrenheit),
|
||||||
|
|
||||||
|
_ => Err(ParseError::UnknownUnit(input)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +186,9 @@ mod test {
|
||||||
NonMetricQuantity { amount: 5.0, unit: NonMetric::Foot },
|
NonMetricQuantity { amount: 5.0, unit: NonMetric::Foot },
|
||||||
NonMetricQuantity { amount: 8.0, unit: NonMetric::Inch },
|
NonMetricQuantity { amount: 8.0, unit: NonMetric::Inch },
|
||||||
]));
|
]));
|
||||||
|
assert_eq!(parse("20 000 lbs"), Ok(vec![
|
||||||
|
NonMetricQuantity { amount: 20_000.0, unit: NonMetric::Pound },
|
||||||
|
]));
|
||||||
|
|
||||||
assert_eq!(parse("12.0."), Err(ParseError::NotValidNumber("12.0.".to_string())));
|
assert_eq!(parse("12.0."), Err(ParseError::NotValidNumber("12.0.".to_string())));
|
||||||
assert_eq!(parse("ft"), Err(ParseError::UnexpectedUnit("ft".to_string())));
|
assert_eq!(parse("ft"), Err(ParseError::UnexpectedUnit("ft".to_string())));
|
||||||
|
@ -194,47 +196,62 @@ mod test {
|
||||||
assert_eq!(parse("12"), Err(ParseError::ExpectedUnit));
|
assert_eq!(parse("12"), Err(ParseError::ExpectedUnit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numbers() {
|
||||||
|
assert_eq!(parse_number("".to_string()), Err(ParseError::NotValidNumber("".to_string())));
|
||||||
|
assert_eq!(parse_number("1".to_string()), Ok(1.0));
|
||||||
|
assert_eq!(parse_number("1.0".to_string()), Ok(1.0));
|
||||||
|
assert_eq!(parse_number("0.1".to_string()), Ok(0.1));
|
||||||
|
assert_eq!(parse_number("0.1.".to_string()), Err(ParseError::NotValidNumber("0.1.".to_string())));
|
||||||
|
assert_eq!(parse_number("-10".to_string()), Ok(-10.0));
|
||||||
|
assert_eq!(parse_number("10\t00\u{1680}000".to_string()), Ok(10_00_000.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn units() {
|
fn units() {
|
||||||
// Length
|
// Length
|
||||||
assert_eq!(parse_unit("inch"), Some(NonMetric::Inch));
|
assert_eq!(parse_unit("inch".to_string()), Ok(NonMetric::Inch));
|
||||||
assert_eq!(parse_unit("inches"), Some(NonMetric::Inch));
|
assert_eq!(parse_unit("inches".to_string()), Ok(NonMetric::Inch));
|
||||||
assert_eq!(parse_unit("in"), Some(NonMetric::Inch));
|
assert_eq!(parse_unit("in".to_string()), Ok(NonMetric::Inch));
|
||||||
assert_eq!(parse_unit("\""), Some(NonMetric::Inch));
|
assert_eq!(parse_unit("\"".to_string()), Ok(NonMetric::Inch));
|
||||||
assert_eq!(parse_unit("″"), Some(NonMetric::Inch));
|
assert_eq!(parse_unit("″".to_string()), Ok(NonMetric::Inch));
|
||||||
|
|
||||||
assert_eq!(parse_unit("foot"), Some(NonMetric::Foot));
|
assert_eq!(parse_unit("foot".to_string()), Ok(NonMetric::Foot));
|
||||||
assert_eq!(parse_unit("feet"), Some(NonMetric::Foot));
|
assert_eq!(parse_unit("feet".to_string()), Ok(NonMetric::Foot));
|
||||||
assert_eq!(parse_unit("ft"), Some(NonMetric::Foot));
|
assert_eq!(parse_unit("ft".to_string()), Ok(NonMetric::Foot));
|
||||||
assert_eq!(parse_unit("'"), Some(NonMetric::Foot));
|
assert_eq!(parse_unit("'".to_string()), Ok(NonMetric::Foot));
|
||||||
assert_eq!(parse_unit("′"), Some(NonMetric::Foot));
|
assert_eq!(parse_unit("′".to_string()), Ok(NonMetric::Foot));
|
||||||
|
|
||||||
assert_eq!(parse_unit("yard"), Some(NonMetric::Yard));
|
assert_eq!(parse_unit("yard".to_string()), Ok(NonMetric::Yard));
|
||||||
assert_eq!(parse_unit("yards"), Some(NonMetric::Yard));
|
assert_eq!(parse_unit("yards".to_string()), Ok(NonMetric::Yard));
|
||||||
assert_eq!(parse_unit("yd"), Some(NonMetric::Yard));
|
assert_eq!(parse_unit("yd".to_string()), Ok(NonMetric::Yard));
|
||||||
|
|
||||||
assert_eq!(parse_unit("mile"), Some(NonMetric::Mile));
|
assert_eq!(parse_unit("mile".to_string()), Ok(NonMetric::Mile));
|
||||||
assert_eq!(parse_unit("miles"), Some(NonMetric::Mile));
|
assert_eq!(parse_unit("miles".to_string()), Ok(NonMetric::Mile));
|
||||||
assert_eq!(parse_unit("mi"), Some(NonMetric::Mile));
|
assert_eq!(parse_unit("mi".to_string()), Ok(NonMetric::Mile));
|
||||||
assert_eq!(parse_unit("m"), Some(NonMetric::Mile));
|
assert_eq!(parse_unit("m".to_string()), Ok(NonMetric::Mile));
|
||||||
|
|
||||||
// Weight
|
// Weight
|
||||||
assert_eq!(parse_unit("ounce"), Some(NonMetric::Ounce));
|
assert_eq!(parse_unit("ounce".to_string()), Ok(NonMetric::Ounce));
|
||||||
assert_eq!(parse_unit("ounces"), Some(NonMetric::Ounce));
|
assert_eq!(parse_unit("ounces".to_string()), Ok(NonMetric::Ounce));
|
||||||
assert_eq!(parse_unit("oz"), Some(NonMetric::Ounce));
|
assert_eq!(parse_unit("oz".to_string()), Ok(NonMetric::Ounce));
|
||||||
|
|
||||||
assert_eq!(parse_unit("pound"), Some(NonMetric::Pound));
|
assert_eq!(parse_unit("pound".to_string()), Ok(NonMetric::Pound));
|
||||||
assert_eq!(parse_unit("pounds"), Some(NonMetric::Pound));
|
assert_eq!(parse_unit("pounds".to_string()), Ok(NonMetric::Pound));
|
||||||
assert_eq!(parse_unit("lb"), Some(NonMetric::Pound));
|
assert_eq!(parse_unit("lb".to_string()), Ok(NonMetric::Pound));
|
||||||
assert_eq!(parse_unit("lbs"), Some(NonMetric::Pound));
|
assert_eq!(parse_unit("lbs".to_string()), Ok(NonMetric::Pound));
|
||||||
assert_eq!(parse_unit("#"), Some(NonMetric::Pound));
|
assert_eq!(parse_unit("#".to_string()), Ok(NonMetric::Pound));
|
||||||
|
|
||||||
assert_eq!(parse_unit("stone"), Some(NonMetric::Stone));
|
assert_eq!(parse_unit("stone".to_string()), Ok(NonMetric::Stone));
|
||||||
assert_eq!(parse_unit("stones"), Some(NonMetric::Stone));
|
assert_eq!(parse_unit("stones".to_string()), Ok(NonMetric::Stone));
|
||||||
assert_eq!(parse_unit("st"), Some(NonMetric::Stone));
|
assert_eq!(parse_unit("st".to_string()), Ok(NonMetric::Stone));
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
assert_eq!(parse_unit("°F".to_string()), Ok(NonMetric::Fahrenheit));
|
||||||
|
assert_eq!(parse_unit("F".to_string()), Ok(NonMetric::Fahrenheit));
|
||||||
|
|
||||||
// Unknown unit
|
// Unknown unit
|
||||||
assert_eq!(parse_unit("hutenosa"), None);
|
assert_eq!(parse_unit("hutenosa".to_string()), Err(ParseError::UnknownUnit("hutenosa".to_string())));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -243,6 +260,8 @@ mod test {
|
||||||
assert_eq!(tokenize("10"), vec![Token::Number("10".to_string())]);
|
assert_eq!(tokenize("10"), vec![Token::Number("10".to_string())]);
|
||||||
assert_eq!(tokenize(" 10 "), vec![Token::Number("10".to_string())]);
|
assert_eq!(tokenize(" 10 "), vec![Token::Number("10".to_string())]);
|
||||||
assert_eq!(tokenize("10 000"), vec![Token::Number("10 000".to_string())]);
|
assert_eq!(tokenize("10 000"), vec![Token::Number("10 000".to_string())]);
|
||||||
|
assert_eq!(tokenize("10\t000"), vec![Token::Number("10\t000".to_string())]);
|
||||||
|
assert_eq!(tokenize("10\u{1680}000"), vec![Token::Number("10\u{1680}000".to_string())]);
|
||||||
assert_eq!(tokenize("10.0.1"), vec![Token::Number("10.0.1".to_string())]);
|
assert_eq!(tokenize("10.0.1"), vec![Token::Number("10.0.1".to_string())]);
|
||||||
assert_eq!(tokenize("ft"), vec![Token::Unit("ft".to_string())]);
|
assert_eq!(tokenize("ft"), vec![Token::Unit("ft".to_string())]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
pub enum Metric {
|
pub enum Metric {
|
||||||
Metre,
|
Metre,
|
||||||
Gram,
|
Gram,
|
||||||
|
Celcius,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
@ -15,9 +16,11 @@ pub enum NonMetric {
|
||||||
Ounce,
|
Ounce,
|
||||||
Pound,
|
Pound,
|
||||||
Stone,
|
Stone,
|
||||||
|
// Temperature
|
||||||
|
Fahrenheit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct MetricQuantity {
|
pub struct MetricQuantity {
|
||||||
pub amount: f64,
|
pub amount: f64,
|
||||||
pub unit: Metric,
|
pub unit: Metric,
|
||||||
|
|
Loading…
Reference in New Issue