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, }; 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().map_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), // Mass "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), "short ton" => Ok(NonMetric::ShortTon), "short tons" => Ok(NonMetric::ShortTon), "US ton" => Ok(NonMetric::ShortTon), "US tons" => Ok(NonMetric::ShortTon), "us ton" => Ok(NonMetric::ShortTon), "us tons" => Ok(NonMetric::ShortTon), "long ton" => Ok(NonMetric::LongTon), "long tons" => Ok(NonMetric::LongTon), "imperial ton" => Ok(NonMetric::LongTon), "imperial tons" => Ok(NonMetric::LongTon), "imp ton" => Ok(NonMetric::LongTon), "imp tons" => Ok(NonMetric::LongTon), // 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), "square yard" => Ok(NonMetric::SquareYard), "square yards" => Ok(NonMetric::SquareYard), "square yd" => Ok(NonMetric::SquareYard), "sq yard" => Ok(NonMetric::SquareYard), "sq yards" => Ok(NonMetric::SquareYard), "sq yd" => Ok(NonMetric::SquareYard), "yard²" => Ok(NonMetric::SquareYard), "yards²" => Ok(NonMetric::SquareYard), "yd²" => Ok(NonMetric::SquareYard), "yard^2" => Ok(NonMetric::SquareYard), "yards^2" => Ok(NonMetric::SquareYard), "yd^2" => Ok(NonMetric::SquareYard), "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), "cubic yard" => Ok(NonMetric::CubicYard), "cubic yards" => Ok(NonMetric::CubicYard), "cubic yd" => Ok(NonMetric::CubicYard), "cu yard" => Ok(NonMetric::CubicYard), "cu yards" => Ok(NonMetric::CubicYard), "cu yd" => Ok(NonMetric::CubicYard), "yard³" => Ok(NonMetric::CubicYard), "yards³" => Ok(NonMetric::CubicYard), "yd³" => Ok(NonMetric::CubicYard), "yard^3" => Ok(NonMetric::CubicYard), "yards^3" => Ok(NonMetric::CubicYard), "yd^3" => Ok(NonMetric::CubicYard), // 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), "cup" => Ok(NonMetric::USCup), "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 "ton" => Err(ParseError::AmbiguousUnit(input, "short", "long")), "tons" => Err(ParseError::AmbiguousUnit(input, "short", "long")), "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.is_empty()); } 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 test_units(NonMetric::Inch, &[ "inch", "inches", "in", "\"", "″", ]); test_units(NonMetric::Foot, &[ "foot", "feet", "ft", "'", "′", ]); test_units(NonMetric::Yard, &[ "yard", "yards", "yd", ]); test_units(NonMetric::Mile, &[ "mile", "miles", "mi", "m", ]); // Mass test_units(NonMetric::Ounce, &[ "ounce", "ounces", "oz", ]); test_units(NonMetric::Pound, &[ "pound", "pounds", "lb", "lbs", "#", ]); test_units(NonMetric::Stone, &[ "stone", "stones", "st", ]); test_units(NonMetric::ShortTon, &[ "short ton", "short tons", "US ton", "US tons", "us ton", "us tons", ]); test_units(NonMetric::LongTon, &[ "long ton", "long tons", "imperial ton", "imperial tons", "imp ton", "imp tons", ]); // Temperature test_units(NonMetric::Fahrenheit, &[ "degree Fahrenheit", "degrees Fahrenheit", "degree fahrenheit", "degrees fahrenheit", "Fahrenheit", "fahrenheit", "°F", "F", ]); // Area test_units(NonMetric::SquareInch, &[ "square inch", "square inches", "square in", "sq inch", "sq inches", "sq in", "inch²", "inches²", "in²", "\"²", "″²", "inch^2", "inches^2", "in^2", "\"^2", ]); test_units(NonMetric::SquareFoot, &[ "square foot", "square feet", "square ft", "sq foot", "sq feet", "sq ft", "foot²", "feet²", "ft²", "'²", "′²", "foot^2", "feet^2", "ft^2", "'^2", "sf", ]); test_units(NonMetric::SquareYard, &[ "square yard", "square yards", "square yd", "sq yard", "sq yards", "sq yd", "yard²", "yards²", "yd²", "yard^2", "yards^2", "yd^2", ]); test_units(NonMetric::Acre, &[ "acre", "acres", "ac", ]); test_units(NonMetric::SquareMile, &[ "square mile", "square miles", "square mi", "sq mile", "sq miles", "sq mi", "mile²", "miles²", "mi²", "mile^2", "miles^2", "mi^2", ]); // Volume test_units(NonMetric::CubicInch, &[ "cubic inch", "cubic inches", "cubic in", "cu inch", "cu inches", "cu in", "inch³", "inches³", "in³", "inch^3", "inches^3", "in^3", ]); test_units(NonMetric::CubicFoot, &[ "cubic foot", "cubic feet", "cubic ft", "cu foot", "cu feet", "cu ft", "foot³", "feet³", "ft³", "foot^3", "feet^3", "ft^3", ]); test_units(NonMetric::CubicYard, &[ "cubic yard", "cubic yards", "cubic yd", "cu yard", "cu yards", "cu yd", "yard³", "yards³", "yd³", "yard^3", "yards^3", "yd^3", ]); // Fluid volume test_units(NonMetric::ImperialFluidOunce, &[ "imperial fluid ounce", "imperial fluid ounces", "imp fl oz", "imp fl. oz.", "imp oz. fl.", ]); test_units(NonMetric::ImperialPint, &[ "imperial pint", "imperial pints", "imp pt", "imp p", ]); test_units(NonMetric::ImperialQuart, &[ "imperial quart", "imperial quarts", "imp qt", ]); test_units(NonMetric::ImperialGallon, &[ "imperial gallon", "imperial gallons", "imp gal", ]); test_units(NonMetric::USTeaspoon, &[ "US teaspoon", "US teaspoons", "US tsp.", "US tsp", "us teaspoon", "us teaspoons", "us tsp.", "us tsp", "teaspoon", "teaspoons", "tsp.", "tsp", ]); test_units(NonMetric::USTablespoon, &[ "US tablespoon", "US tablespoons", "US Tbsp.", "US Tbsp", "us tablespoon", "us tablespoons", "us tbsp.", "us tbsp", "tablespoon", "tablespoons", "Tbsp.", "Tbsp", "tbsp.", "tbsp", ]); test_units(NonMetric::USFluidOunce, &[ "US fluid ounce", "US fluid ounces", "US fl oz", "US fl. oz.", "US oz. fl.", "us fluid ounce", "us fluid ounces", "us fl oz", "us fl. oz.", "us oz. fl.", ]); test_units(NonMetric::USCup, &[ "US cup", "US cups", "us cup", "us cups", "cup", "cups", ]); test_units(NonMetric::USLiquidPint, &[ "US liquid pint", "US liquid pints", "US pint", "US pints", "US pt", "US p", "us liquid pint", "us liquid pints", "us pint", "us pints", "us pt", "us p", ]); test_units(NonMetric::USLiquidQuart, &[ "US liquid quart", "US liquid quarts", "US quart", "US quarts", "US qt", "us liquid quart", "us liquid quarts", "us quart", "us quarts", "us qt", ]); test_units(NonMetric::USGallon, &[ "US gallon", "US gallons", "US gal", "us gallon", "us gallons", "us gal", ]); } fn test_units(unit: NonMetric, spellings: &[&str]) { for spelling in spellings { assert_eq!(parse_unit(spelling.to_string()), Ok(unit)); } } #[test] fn ambiguous_units() { test_ambiguous_units(NonMetric::ShortTon, NonMetric::LongTon, &[ "ton", "tons", ]); test_ambiguous_units(NonMetric::ImperialFluidOunce, NonMetric::USFluidOunce, &[ "fluid ounce", "fluid ounces", "fl oz", "fl. oz.", "oz. fl.", ]); test_ambiguous_units(NonMetric::ImperialPint, NonMetric::USLiquidPint, &[ "pint", "pints", "pt", "p", ]); test_ambiguous_units(NonMetric::ImperialQuart, NonMetric::USLiquidQuart, &[ "quart", "quarts", "qt", ]); test_ambiguous_units(NonMetric::ImperialGallon, NonMetric::USGallon, &[ "gallon", "gallons", "gal", ]); } fn test_ambiguous_units(unit1: NonMetric, unit2: NonMetric, spellings: &[&str]) { for spelling in spellings { let parsed = parse_unit(spelling.to_string()); if let Err(ParseError::AmbiguousUnit(unit_name, prefix1, prefix2)) = parsed { assert_eq!(&unit_name, spelling); let suggestion1 = format!("{prefix1} {unit_name}"); let suggestion2 = format!("{prefix2} {unit_name}"); assert_eq!(parse_unit(suggestion1), Ok(unit1)); assert_eq!(parse_unit(suggestion2), Ok(unit2)); } else { panic!("units passed to test_ambiguous_units() must be ambiguous"); } } } #[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()), ] ); } }