mod conversions; mod format; mod parse; mod units; use conversions::convert; use format::format; use parse::{parse, ParseError}; use units::{Metric, MetricQuantity, NonMetric}; pub fn run(input: &str) -> Result { let non_metric = match parse(input) { Ok(non_metric) => non_metric, Err(ParseError::NotValidNumber(string)) => { return Err(format!("Not a valid number: {string}")); } Err(ParseError::UnexpectedUnit(unit)) => { return Err(format!("Unexpected unit: {unit}")); } Err(ParseError::UnknownUnit(unit)) => { return Err(format!("Unknown unit: {unit}")); } Err(ParseError::ExpectedUnit) => { return Err("Expected a unit".to_string()); } Err(ParseError::AmbiguousUnit(unit, prefix1, prefix2)) => { return Err(format!("Ambiguous unit '{unit}', use either '{prefix1} {unit}' or '{prefix2} {unit}'")); } }; if non_metric.is_empty() { return Err("Expected quantity or quantities to convert".to_string()); } let metric: Vec = non_metric.clone().into_iter().map(convert).collect(); // Make sure the results of the conversions can be summed together // This is the case if the units after conversion are the same and // they are not temperature units (i.e. degrees Celsius) let mut metric_unit = None; for index in 0..metric.len() { match metric_unit { Some(metric_unit) => { if metric[index].unit != metric_unit { let first_unit_name = unit_to_name(non_metric[0].unit); let unit_name = unit_to_name(non_metric[index].unit); return Err(format!("Incompatible units: {first_unit_name}, {unit_name}")); } } None => { metric_unit = Some(metric[index].unit); } } } if metric.len() > 1 && metric_unit == Some(Metric::Celsius) { return Err("Cannot sum together temperatures".to_string()); } let amount = metric.into_iter().map(|quantity| { quantity.amount }).sum(); let quantity = MetricQuantity { amount, unit: metric_unit.expect("we must have at least one quantity by this point"), }; Ok(format(quantity)) } fn unit_to_name(unit: NonMetric) -> &'static str { match unit { // Length NonMetric::Inch => "inches", NonMetric::Foot => "feet", NonMetric::Yard => "yards", NonMetric::Mile => "miles", // Mass NonMetric::Ounce => "ounces", NonMetric::Pound => "pounds", NonMetric::Stone => "stones", NonMetric::ShortTon => "short tons", NonMetric::LongTon => "long tons", // Temperature NonMetric::Fahrenheit => "degrees Fahrenheit", // Area NonMetric::SquareInch => "square inches", NonMetric::SquareFoot => "square feet", NonMetric::SquareYard => "square yards", NonMetric::Acre => "acres", NonMetric::SquareMile => "square miles", // Volume NonMetric::CubicInch => "cubic inches", NonMetric::CubicFoot => "cubic feet", NonMetric::CubicYard => "cubic yards", // Fluid volume NonMetric::ImperialFluidOunce => "imperial fluid ounces", NonMetric::ImperialPint => "imperial pints", NonMetric::ImperialQuart => "imperial quarts", NonMetric::ImperialGallon => "imperial gallons", NonMetric::USTeaspoon => "US teaspoons", NonMetric::USTablespoon => "US tablespoons", NonMetric::USFluidOunce => "US fluid ounces", NonMetric::USCup => "US cups", NonMetric::USLiquidPint => "US liquid pints", NonMetric::USLiquidQuart => "US liquid quarts", NonMetric::USGallon => "US gallons", } } #[cfg(test)] mod test { use super::*; #[test] fn errors() { assert_eq!(run("1.0."), Err("Not a valid number: 1.0.".to_string())); assert_eq!(run("ft"), Err("Unexpected unit: ft".to_string())); assert_eq!(run("1 tf"), Err("Unknown unit: tf".to_string())); assert_eq!(run("1"), Err("Expected a unit".to_string())); assert_eq!(run(""), Err("Expected quantity or quantities to convert".to_string())); assert_eq!(run("1 fl oz"), Err("Ambiguous unit 'fl oz', use either 'imp fl oz' or 'US fl oz'".to_string())); assert_eq!(run("6 ft 1 lbs"), Err("Incompatible units: feet, pounds".to_string())); assert_eq!(run("0 °F 0 °F"), Err("Cannot sum together temperatures".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())); // Mass 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())); assert_eq!(run("1 short ton"), Ok("907.2 kg".to_string())); assert_eq!(run("1 long ton"), Ok("1 016 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())); // Area assert_eq!(run("1 in²"), Ok("6.452 cm²".to_string())); assert_eq!(run("1 ft²"), Ok("929 cm²".to_string())); assert_eq!(run("1 yd²"), Ok("8 361 cm²".to_string())); assert_eq!(run("1 acre"), Ok("4 047 m²".to_string())); assert_eq!(run("1 mi²"), Ok("2.59 km²".to_string())); // Volume assert_eq!(run("1 in³"), Ok("16.39 cm³".to_string())); assert_eq!(run("1 ft³"), Ok("28 317 cm³".to_string())); assert_eq!(run("1 yd³"), Ok("764 555 cm³".to_string())); // Fluid volume assert_eq!(run("1 imp fl oz"), Ok("2.841 cl".to_string())); assert_eq!(run("1 imp pt"), Ok("5.683 dl".to_string())); assert_eq!(run("1 imp qt"), Ok("1.137 l".to_string())); assert_eq!(run("1 imp gal"), Ok("4.546 l".to_string())); assert_eq!(run("1 tsp"), Ok("4.929 ml".to_string())); assert_eq!(run("1 Tbsp"), Ok("1.479 cl".to_string())); assert_eq!(run("1 US fl oz"), Ok("2.957 cl".to_string())); assert_eq!(run("1 US cup"), Ok("2.366 dl".to_string())); assert_eq!(run("1 US pt"), Ok("4.732 dl".to_string())); assert_eq!(run("1 US qt"), Ok("9.464 dl".to_string())); assert_eq!(run("1 US gal"), Ok("3.785 l".to_string())); } }