use crate::units::{NonMetric, NonMetricQuantity}; enum Expect { Number, Unit, } #[derive(Debug, PartialEq)] pub enum ParseError { NotValidNumber(String), UnexpectedUnit(String), UnknownUnit(String), ExpectedUnit, AmbiguousUnit(String, &'static str, &'static str), } pub fn parse(input: &str) -> Result, ParseError> { let mut quantities = Vec::new(); let mut state = Expect::Number; let mut amount = None; for token in tokenize(input) { match (&state, token) { (Expect::Number, Token::Number(number)) => { let number = parse_number(number)?; amount = Some(number); state = Expect::Unit; } (Expect::Number, Token::Unit(unit)) => { return Err(ParseError::UnexpectedUnit(unit)); } (Expect::Unit, Token::Number(_)) => { unreachable!("token stream can't contain two numbers in a row"); } (Expect::Unit, Token::Unit(unit)) => { let unit = parse_unit(unit)?; let quantity = NonMetricQuantity { amount: amount.take().expect("must have read a number to be in this state"), unit: unit, }; quantities.push(quantity); state = Expect::Number; } } } match state { Expect::Number => {} Expect::Unit => { return Err(ParseError::ExpectedUnit); } } Ok(quantities) } fn parse_number(input: String) -> Result { 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 { match input.as_str() { // Length "inch" => Ok(NonMetric::Inch), "inches" => Ok(NonMetric::Inch), "in" => Ok(NonMetric::Inch), "\"" => Ok(NonMetric::Inch), "″" => Ok(NonMetric::Inch), "foot" => Ok(NonMetric::Foot), "feet" => Ok(NonMetric::Foot), "ft" => Ok(NonMetric::Foot), "'" => Ok(NonMetric::Foot), "′" => Ok(NonMetric::Foot), "yard" => Ok(NonMetric::Yard), "yards" => Ok(NonMetric::Yard), "yd" => Ok(NonMetric::Yard), "mile" => Ok(NonMetric::Mile), "miles" => Ok(NonMetric::Mile), "mi" => Ok(NonMetric::Mile), "m" => Ok(NonMetric::Mile), // Weight "ounce" => Ok(NonMetric::Ounce), "ounces" => Ok(NonMetric::Ounce), "oz" => Ok(NonMetric::Ounce), "pound" => Ok(NonMetric::Pound), "pounds" => Ok(NonMetric::Pound), "lb" => Ok(NonMetric::Pound), "lbs" => Ok(NonMetric::Pound), "#" => Ok(NonMetric::Pound), "stone" => Ok(NonMetric::Stone), "stones" => Ok(NonMetric::Stone), "st" => Ok(NonMetric::Stone), // Temperature "degree Fahrenheit" => Ok(NonMetric::Fahrenheit), "degrees Fahrenheit" => Ok(NonMetric::Fahrenheit), "degree fahrenheit" => Ok(NonMetric::Fahrenheit), "degrees fahrenheit" => Ok(NonMetric::Fahrenheit), "Fahrenheit" => Ok(NonMetric::Fahrenheit), "fahrenheit" => Ok(NonMetric::Fahrenheit), "°F" => Ok(NonMetric::Fahrenheit), "F" => Ok(NonMetric::Fahrenheit), // Area "square inch" => Ok(NonMetric::SquareInch), "square inches" => Ok(NonMetric::SquareInch), "square in" => Ok(NonMetric::SquareInch), "sq inch" => Ok(NonMetric::SquareInch), "sq inches" => Ok(NonMetric::SquareInch), "sq in" => Ok(NonMetric::SquareInch), "inch²" => Ok(NonMetric::SquareInch), "inches²" => Ok(NonMetric::SquareInch), "in²" => Ok(NonMetric::SquareInch), "\"²" => Ok(NonMetric::SquareInch), "″²" => Ok(NonMetric::SquareInch), "inch^2" => Ok(NonMetric::SquareInch), "inches^2" => Ok(NonMetric::SquareInch), "in^2" => Ok(NonMetric::SquareInch), "\"^2" => Ok(NonMetric::SquareInch), "square foot" => Ok(NonMetric::SquareFoot), "square feet" => Ok(NonMetric::SquareFoot), "square ft" => Ok(NonMetric::SquareFoot), "sq foot" => Ok(NonMetric::SquareFoot), "sq feet" => Ok(NonMetric::SquareFoot), "sq ft" => Ok(NonMetric::SquareFoot), "foot²" => Ok(NonMetric::SquareFoot), "feet²" => Ok(NonMetric::SquareFoot), "ft²" => Ok(NonMetric::SquareFoot), "'²" => Ok(NonMetric::SquareFoot), "′²" => Ok(NonMetric::SquareFoot), "foot^2" => Ok(NonMetric::SquareFoot), "feet^2" => Ok(NonMetric::SquareFoot), "ft^2" => Ok(NonMetric::SquareFoot), "'^2" => Ok(NonMetric::SquareFoot), "sf" => Ok(NonMetric::SquareFoot), "acre" => Ok(NonMetric::Acre), "acres" => Ok(NonMetric::Acre), "ac" => Ok(NonMetric::Acre), "square mile" => Ok(NonMetric::SquareMile), "square miles" => Ok(NonMetric::SquareMile), "square mi" => Ok(NonMetric::SquareMile), "sq mile" => Ok(NonMetric::SquareMile), "sq miles" => Ok(NonMetric::SquareMile), "sq mi" => Ok(NonMetric::SquareMile), "mile²" => Ok(NonMetric::SquareMile), "miles²" => Ok(NonMetric::SquareMile), "mi²" => Ok(NonMetric::SquareMile), "mile^2" => Ok(NonMetric::SquareMile), "miles^2" => Ok(NonMetric::SquareMile), "mi^2" => Ok(NonMetric::SquareMile), // Volume "cubic inch" => Ok(NonMetric::CubicInch), "cubic inches" => Ok(NonMetric::CubicInch), "cubic in" => Ok(NonMetric::CubicInch), "cu inch" => Ok(NonMetric::CubicInch), "cu inches" => Ok(NonMetric::CubicInch), "cu in" => Ok(NonMetric::CubicInch), "inch³" => Ok(NonMetric::CubicInch), "inches³" => Ok(NonMetric::CubicInch), "in³" => Ok(NonMetric::CubicInch), "inch^3" => Ok(NonMetric::CubicInch), "inches^3" => Ok(NonMetric::CubicInch), "in^3" => Ok(NonMetric::CubicInch), "cubic foot" => Ok(NonMetric::CubicFoot), "cubic feet" => Ok(NonMetric::CubicFoot), "cubic ft" => Ok(NonMetric::CubicFoot), "cu foot" => Ok(NonMetric::CubicFoot), "cu feet" => Ok(NonMetric::CubicFoot), "cu ft" => Ok(NonMetric::CubicFoot), "foot³" => Ok(NonMetric::CubicFoot), "feet³" => Ok(NonMetric::CubicFoot), "ft³" => Ok(NonMetric::CubicFoot), "foot^3" => Ok(NonMetric::CubicFoot), "feet^3" => Ok(NonMetric::CubicFoot), "ft^3" => Ok(NonMetric::CubicFoot), // Fluid volume "imperial fluid ounce" => Ok(NonMetric::ImperialFluidOunce), "imperial fluid ounces" => Ok(NonMetric::ImperialFluidOunce), "imp fl oz" => Ok(NonMetric::ImperialFluidOunce), "imp fl. oz." => Ok(NonMetric::ImperialFluidOunce), "imp oz. fl." => Ok(NonMetric::ImperialFluidOunce), "imperial pint" => Ok(NonMetric::ImperialPint), "imperial pints" => Ok(NonMetric::ImperialPint), "imp pt" => Ok(NonMetric::ImperialPint), "imp p" => Ok(NonMetric::ImperialPint), "imperial quart" => Ok(NonMetric::ImperialQuart), "imperial quarts" => Ok(NonMetric::ImperialQuart), "imp qt" => Ok(NonMetric::ImperialQuart), "imperial gallon" => Ok(NonMetric::ImperialGallon), "imperial gallons" => Ok(NonMetric::ImperialGallon), "imp gal" => Ok(NonMetric::ImperialGallon), "US teaspoon" => Ok(NonMetric::USTeaspoon), "US teaspoons" => Ok(NonMetric::USTeaspoon), "US tsp." => Ok(NonMetric::USTeaspoon), "US tsp" => Ok(NonMetric::USTeaspoon), "us teaspoon" => Ok(NonMetric::USTeaspoon), "us teaspoons" => Ok(NonMetric::USTeaspoon), "us tsp." => Ok(NonMetric::USTeaspoon), "us tsp" => Ok(NonMetric::USTeaspoon), "teaspoon" => Ok(NonMetric::USTeaspoon), "teaspoons" => Ok(NonMetric::USTeaspoon), "tsp." => Ok(NonMetric::USTeaspoon), "tsp" => Ok(NonMetric::USTeaspoon), "US tablespoon" => Ok(NonMetric::USTablespoon), "US tablespoons" => Ok(NonMetric::USTablespoon), "US Tbsp." => Ok(NonMetric::USTablespoon), "US Tbsp" => Ok(NonMetric::USTablespoon), "us tablespoon" => Ok(NonMetric::USTablespoon), "us tablespoons" => Ok(NonMetric::USTablespoon), "us tbsp." => Ok(NonMetric::USTablespoon), "us tbsp" => Ok(NonMetric::USTablespoon), "tablespoon" => Ok(NonMetric::USTablespoon), "tablespoons" => Ok(NonMetric::USTablespoon), "Tbsp." => Ok(NonMetric::USTablespoon), "Tbsp" => Ok(NonMetric::USTablespoon), "tbsp." => Ok(NonMetric::USTablespoon), "tbsp" => Ok(NonMetric::USTablespoon), "US fluid ounce" => Ok(NonMetric::USFluidOunce), "US fluid ounces" => Ok(NonMetric::USFluidOunce), "US fl oz" => Ok(NonMetric::USFluidOunce), "US fl. oz." => Ok(NonMetric::USFluidOunce), "US oz. fl." => Ok(NonMetric::USFluidOunce), "us fluid ounce" => Ok(NonMetric::USFluidOunce), "us fluid ounces" => Ok(NonMetric::USFluidOunce), "us fl oz" => Ok(NonMetric::USFluidOunce), "us fl. oz." => Ok(NonMetric::USFluidOunce), "us oz. fl." => Ok(NonMetric::USFluidOunce), "US cup" => Ok(NonMetric::USCup), "US cups" => Ok(NonMetric::USCup), "us cup" => Ok(NonMetric::USCup), "us cups" => Ok(NonMetric::USCup), "US liquid pint" => Ok(NonMetric::USLiquidPint), "US liquid pints" => Ok(NonMetric::USLiquidPint), "US pint" => Ok(NonMetric::USLiquidPint), "US pints" => Ok(NonMetric::USLiquidPint), "US pt" => Ok(NonMetric::USLiquidPint), "US p" => Ok(NonMetric::USLiquidPint), "us liquid pint" => Ok(NonMetric::USLiquidPint), "us liquid pints" => Ok(NonMetric::USLiquidPint), "us pint" => Ok(NonMetric::USLiquidPint), "us pints" => Ok(NonMetric::USLiquidPint), "us pt" => Ok(NonMetric::USLiquidPint), "us p" => Ok(NonMetric::USLiquidPint), "US liquid quart" => Ok(NonMetric::USLiquidQuart), "US liquid quarts" => Ok(NonMetric::USLiquidQuart), "US quart" => Ok(NonMetric::USLiquidQuart), "US quarts" => Ok(NonMetric::USLiquidQuart), "US qt" => Ok(NonMetric::USLiquidQuart), "us liquid quart" => Ok(NonMetric::USLiquidQuart), "us liquid quarts" => Ok(NonMetric::USLiquidQuart), "us quart" => Ok(NonMetric::USLiquidQuart), "us quarts" => Ok(NonMetric::USLiquidQuart), "us qt" => Ok(NonMetric::USLiquidQuart), "US gallon" => Ok(NonMetric::USGallon), "US gallons" => Ok(NonMetric::USGallon), "US gal" => Ok(NonMetric::USGallon), "us gallon" => Ok(NonMetric::USGallon), "us gallons" => Ok(NonMetric::USGallon), "us gal" => Ok(NonMetric::USGallon), // Ambiguous units "fluid ounce" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "fluid ounces" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "fl oz" => Err(ParseError::AmbiguousUnit(input, "imp", "US")), "fl. oz." => Err(ParseError::AmbiguousUnit(input, "imp", "US")), "oz. fl." => Err(ParseError::AmbiguousUnit(input, "imp", "US")), "pint" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "pints" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "pt" => Err(ParseError::AmbiguousUnit(input, "imp", "US")), "p" => Err(ParseError::AmbiguousUnit(input, "imp", "US")), "quart" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "quarts" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "qt" => Err(ParseError::AmbiguousUnit(input, "imp", "US")), "gallon" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "gallons" => Err(ParseError::AmbiguousUnit(input, "imperial", "US")), "gal" => Err(ParseError::AmbiguousUnit(input, "imp", "US")), // Unknown unit _ => Err(ParseError::UnknownUnit(input)), } } #[derive(Debug, PartialEq)] enum Token { Number(String), Unit(String), } enum TokState { Neutral, Number, Unit(bool), } fn tokenize(input: &str) -> Vec { let mut tokens = Vec::new(); let mut token = String::new(); let mut state = TokState::Neutral; for c in input.chars() { match state { TokState::Neutral => { if c.is_ascii_digit() || c == '-' { token.push(c); state = TokState::Number; } else if !c.is_whitespace() { token.push(c); state = TokState::Unit(false); } } TokState::Number => { if c.is_ascii_digit() || c.is_whitespace() || c == '.' { token.push(c); } else { tokens.push(Token::Number(token.trim().to_string())); state = TokState::Unit(false); token = String::new(); token.push(c); } } TokState::Unit(after_caret) => { if !after_caret && (c.is_ascii_digit() || c == '-') { tokens.push(Token::Unit(token.trim().to_string())); state = TokState::Number; token = String::new(); token.push(c); } else if c == '^' { token.push(c); state = TokState::Unit(true); } else if c.is_whitespace() { token.push(c); state = TokState::Unit(false); } else { token.push(c); } } } } match state { TokState::Neutral => { assert!(token.len() == 0); } TokState::Number => { tokens.push(Token::Number(token.trim().to_string())); } TokState::Unit(_) => { tokens.push(Token::Unit(token.trim().to_string())); } } tokens } #[cfg(test)] mod test { use super::*; #[test] fn parsing() { assert_eq!(parse(""), Ok(vec![])); assert_eq!(parse("5 ft"), Ok(vec![ NonMetricQuantity { amount: 5.0, unit: NonMetric::Foot }, ])); assert_eq!(parse("5 ft 8 in"), Ok(vec![ NonMetricQuantity { amount: 5.0, unit: NonMetric::Foot }, 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("ft"), Err(ParseError::UnexpectedUnit("ft".to_string()))); assert_eq!(parse("5 tf"), Err(ParseError::UnknownUnit("tf".to_string()))); assert_eq!(parse("12"), Err(ParseError::ExpectedUnit)); assert_eq!(parse("1 gallon"), Err(ParseError::AmbiguousUnit("gallon".to_string(), "imperial", "US"))); } #[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] fn units() { // Length assert_eq!(parse_unit("inch".to_string()), Ok(NonMetric::Inch)); assert_eq!(parse_unit("inches".to_string()), Ok(NonMetric::Inch)); assert_eq!(parse_unit("in".to_string()), Ok(NonMetric::Inch)); assert_eq!(parse_unit("\"".to_string()), Ok(NonMetric::Inch)); assert_eq!(parse_unit("″".to_string()), Ok(NonMetric::Inch)); assert_eq!(parse_unit("foot".to_string()), Ok(NonMetric::Foot)); assert_eq!(parse_unit("feet".to_string()), Ok(NonMetric::Foot)); assert_eq!(parse_unit("ft".to_string()), Ok(NonMetric::Foot)); assert_eq!(parse_unit("'".to_string()), Ok(NonMetric::Foot)); assert_eq!(parse_unit("′".to_string()), Ok(NonMetric::Foot)); assert_eq!(parse_unit("yard".to_string()), Ok(NonMetric::Yard)); assert_eq!(parse_unit("yards".to_string()), Ok(NonMetric::Yard)); assert_eq!(parse_unit("yd".to_string()), Ok(NonMetric::Yard)); assert_eq!(parse_unit("mile".to_string()), Ok(NonMetric::Mile)); assert_eq!(parse_unit("miles".to_string()), Ok(NonMetric::Mile)); assert_eq!(parse_unit("mi".to_string()), Ok(NonMetric::Mile)); assert_eq!(parse_unit("m".to_string()), Ok(NonMetric::Mile)); // Weight assert_eq!(parse_unit("ounce".to_string()), Ok(NonMetric::Ounce)); assert_eq!(parse_unit("ounces".to_string()), Ok(NonMetric::Ounce)); assert_eq!(parse_unit("oz".to_string()), Ok(NonMetric::Ounce)); assert_eq!(parse_unit("pound".to_string()), Ok(NonMetric::Pound)); assert_eq!(parse_unit("pounds".to_string()), Ok(NonMetric::Pound)); assert_eq!(parse_unit("lb".to_string()), Ok(NonMetric::Pound)); assert_eq!(parse_unit("lbs".to_string()), Ok(NonMetric::Pound)); assert_eq!(parse_unit("#".to_string()), Ok(NonMetric::Pound)); assert_eq!(parse_unit("stone".to_string()), Ok(NonMetric::Stone)); assert_eq!(parse_unit("stones".to_string()), Ok(NonMetric::Stone)); assert_eq!(parse_unit("st".to_string()), Ok(NonMetric::Stone)); // Temperature assert_eq!(parse_unit("degree Fahrenheit".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("degrees Fahrenheit".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("degree fahrenheit".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("degrees fahrenheit".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("Fahrenheit".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("fahrenheit".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("°F".to_string()), Ok(NonMetric::Fahrenheit)); assert_eq!(parse_unit("F".to_string()), Ok(NonMetric::Fahrenheit)); // Area assert_eq!(parse_unit("square inch".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("square inches".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("square in".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("sq inch".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("sq inches".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("sq in".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("inch²".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("inches²".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("in²".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("\"²".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("″²".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("inch^2".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("inches^2".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("in^2".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("\"^2".to_string()), Ok(NonMetric::SquareInch)); assert_eq!(parse_unit("square foot".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("square feet".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("square ft".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("sq foot".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("sq feet".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("sq ft".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("foot²".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("feet²".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("ft²".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("'²".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("′²".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("foot^2".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("feet^2".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("ft^2".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("'^2".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("sf".to_string()), Ok(NonMetric::SquareFoot)); assert_eq!(parse_unit("acre".to_string()), Ok(NonMetric::Acre)); assert_eq!(parse_unit("acres".to_string()), Ok(NonMetric::Acre)); assert_eq!(parse_unit("ac".to_string()), Ok(NonMetric::Acre)); assert_eq!(parse_unit("square mile".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("square miles".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("square mi".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("sq mile".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("sq miles".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("sq mi".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("mile²".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("miles²".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("mi²".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("mile^2".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("miles^2".to_string()), Ok(NonMetric::SquareMile)); assert_eq!(parse_unit("mi^2".to_string()), Ok(NonMetric::SquareMile)); // Volume assert_eq!(parse_unit("cubic inch".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("cubic inches".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("cubic in".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("cu inch".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("cu inches".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("cu in".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("inch³".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("inches³".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("in³".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("inch^3".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("inches^3".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("in^3".to_string()), Ok(NonMetric::CubicInch)); assert_eq!(parse_unit("cubic foot".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("cubic feet".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("cubic ft".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("cu foot".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("cu feet".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("cu ft".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("foot³".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("feet³".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("ft³".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("foot^3".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("feet^3".to_string()), Ok(NonMetric::CubicFoot)); assert_eq!(parse_unit("ft^3".to_string()), Ok(NonMetric::CubicFoot)); // Fluid volume assert_eq!(parse_unit("imperial fluid ounce".to_string()), Ok(NonMetric::ImperialFluidOunce)); assert_eq!(parse_unit("imperial fluid ounces".to_string()), Ok(NonMetric::ImperialFluidOunce)); assert_eq!(parse_unit("imp fl oz".to_string()), Ok(NonMetric::ImperialFluidOunce)); assert_eq!(parse_unit("imp fl. oz.".to_string()), Ok(NonMetric::ImperialFluidOunce)); assert_eq!(parse_unit("imp oz. fl.".to_string()), Ok(NonMetric::ImperialFluidOunce)); assert_eq!(parse_unit("imperial pint".to_string()), Ok(NonMetric::ImperialPint)); assert_eq!(parse_unit("imperial pints".to_string()), Ok(NonMetric::ImperialPint)); assert_eq!(parse_unit("imp pt".to_string()), Ok(NonMetric::ImperialPint)); assert_eq!(parse_unit("imp p".to_string()), Ok(NonMetric::ImperialPint)); assert_eq!(parse_unit("imperial quart".to_string()), Ok(NonMetric::ImperialQuart)); assert_eq!(parse_unit("imperial quarts".to_string()), Ok(NonMetric::ImperialQuart)); assert_eq!(parse_unit("imp qt".to_string()), Ok(NonMetric::ImperialQuart)); assert_eq!(parse_unit("imperial gallon".to_string()), Ok(NonMetric::ImperialGallon)); assert_eq!(parse_unit("imperial gallons".to_string()), Ok(NonMetric::ImperialGallon)); assert_eq!(parse_unit("imp gal".to_string()), Ok(NonMetric::ImperialGallon)); assert_eq!(parse_unit("US teaspoon".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("US teaspoons".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("US tsp.".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("US tsp".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("us teaspoon".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("us teaspoons".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("us tsp.".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("us tsp".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("teaspoon".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("teaspoons".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("tsp.".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("tsp".to_string()), Ok(NonMetric::USTeaspoon)); assert_eq!(parse_unit("US tablespoon".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("US tablespoons".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("US Tbsp.".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("US Tbsp".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("us tablespoon".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("us tablespoons".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("us tbsp.".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("us tbsp".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("tablespoon".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("tablespoons".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("Tbsp.".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("Tbsp".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("tbsp.".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("tbsp".to_string()), Ok(NonMetric::USTablespoon)); assert_eq!(parse_unit("US fluid ounce".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("US fluid ounces".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("US fl oz".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("US fl. oz.".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("US oz. fl.".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("us fluid ounce".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("us fluid ounces".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("us fl oz".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("us fl. oz.".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("us oz. fl.".to_string()), Ok(NonMetric::USFluidOunce)); assert_eq!(parse_unit("US cup".to_string()), Ok(NonMetric::USCup)); assert_eq!(parse_unit("US cups".to_string()), Ok(NonMetric::USCup)); assert_eq!(parse_unit("us cup".to_string()), Ok(NonMetric::USCup)); assert_eq!(parse_unit("us cups".to_string()), Ok(NonMetric::USCup)); assert_eq!(parse_unit("US liquid pint".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("US liquid pints".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("US pint".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("US pints".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("US pt".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("US p".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("us liquid pint".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("us liquid pints".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("us pint".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("us pints".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("us pt".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("us p".to_string()), Ok(NonMetric::USLiquidPint)); assert_eq!(parse_unit("US liquid quart".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("US liquid quarts".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("US quart".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("US quarts".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("US qt".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("us liquid quart".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("us liquid quarts".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("us quart".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("us quarts".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("us qt".to_string()), Ok(NonMetric::USLiquidQuart)); assert_eq!(parse_unit("US gallon".to_string()), Ok(NonMetric::USGallon)); assert_eq!(parse_unit("US gallons".to_string()), Ok(NonMetric::USGallon)); assert_eq!(parse_unit("US gal".to_string()), Ok(NonMetric::USGallon)); assert_eq!(parse_unit("us gallon".to_string()), Ok(NonMetric::USGallon)); assert_eq!(parse_unit("us gallons".to_string()), Ok(NonMetric::USGallon)); assert_eq!(parse_unit("us gal".to_string()), Ok(NonMetric::USGallon)); } #[test] fn ambiguous_units() { assert_eq!( parse_unit("fluid ounce".to_string()), Err(ParseError::AmbiguousUnit("fluid ounce".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("fluid ounces".to_string()), Err(ParseError::AmbiguousUnit("fluid ounces".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("fl oz".to_string()), Err(ParseError::AmbiguousUnit("fl oz".to_string(), "imp", "US")) ); assert_eq!( parse_unit("fl. oz.".to_string()), Err(ParseError::AmbiguousUnit("fl. oz.".to_string(), "imp", "US")) ); assert_eq!( parse_unit("oz. fl.".to_string()), Err(ParseError::AmbiguousUnit("oz. fl.".to_string(), "imp", "US")) ); assert_eq!( parse_unit("pint".to_string()), Err(ParseError::AmbiguousUnit("pint".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("pints".to_string()), Err(ParseError::AmbiguousUnit("pints".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("pt".to_string()), Err(ParseError::AmbiguousUnit("pt".to_string(), "imp", "US")) ); assert_eq!( parse_unit("p".to_string()), Err(ParseError::AmbiguousUnit("p".to_string(), "imp", "US")) ); assert_eq!( parse_unit("quart".to_string()), Err(ParseError::AmbiguousUnit("quart".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("quarts".to_string()), Err(ParseError::AmbiguousUnit("quarts".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("qt".to_string()), Err(ParseError::AmbiguousUnit("qt".to_string(), "imp", "US")) ); assert_eq!( parse_unit("gallon".to_string()), Err(ParseError::AmbiguousUnit("gallon".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("gallons".to_string()), Err(ParseError::AmbiguousUnit("gallons".to_string(), "imperial", "US")) ); assert_eq!( parse_unit("gal".to_string()), Err(ParseError::AmbiguousUnit("gal".to_string(), "imp", "US")) ); } #[test] fn ambiguous_unit_suggestions() { let ambiguous_units = [ "fluid ounce", "fluid ounces", "fl oz", "fl. oz.", "oz. fl.", "pint", "pints", "pt", "p", "quart", "quarts", "qt", "gallon", "gallons", "gal", ]; for unit in ambiguous_units { let parsed = parse_unit(unit.to_string()); if let Err(ParseError::AmbiguousUnit(unit, prefix1, prefix2)) = parsed { let suggestion1 = format!("{prefix1} {unit}"); let suggestion2 = format!("{prefix2} {unit}"); assert!(parse_unit(suggestion1).is_ok()); assert!(parse_unit(suggestion2).is_ok()); } else { unreachable!(); } } } #[test] fn unknown_unit() { assert_eq!(parse_unit("hutenosa".to_string()), Err(ParseError::UnknownUnit("hutenosa".to_string()))); } #[test] fn tokens() { assert_eq!(tokenize(""), vec![]); 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\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("ft"), vec![Token::Unit("ft".to_string())]); assert_eq!( tokenize("10 ft"), vec![ Token::Number("10".to_string()), Token::Unit("ft".to_string()), ] ); assert_eq!( tokenize("5 ft 7 in"), vec![ Token::Number("5".to_string()), Token::Unit("ft".to_string()), Token::Number("7".to_string()), Token::Unit("in".to_string()), ] ); assert_eq!( tokenize("5\"7'"), vec![ Token::Number("5".to_string()), Token::Unit("\"".to_string()), Token::Number("7".to_string()), Token::Unit("'".to_string()), ] ); assert_eq!( tokenize(" 2.2lbs "), vec![ Token::Number("2.2".to_string()), Token::Unit("lbs".to_string()), ] ); assert_eq!( tokenize("sq ft"), vec![ Token::Unit("sq ft".to_string()), ] ); assert_eq!( tokenize("sq ft2"), vec![ Token::Unit("sq ft".to_string()), Token::Number("2".to_string()), ] ); assert_eq!( tokenize("ft^2"), vec![ Token::Unit("ft^2".to_string()), ] ); assert_eq!( tokenize("ft^22"), vec![ Token::Unit("ft^22".to_string()), ] ); assert_eq!( tokenize("ft^2 2"), vec![ Token::Unit("ft^2".to_string()), Token::Number("2".to_string()), ] ); assert_eq!( tokenize("ft^2 s^-1 lb 2"), vec![ Token::Unit("ft^2 s^-1 lb".to_string()), Token::Number("2".to_string()), ] ); } }