metrify/src/parse.rs

925 lines
23 KiB
Rust
Raw Normal View History

2023-05-14 00:04:47 +00:00
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),
2023-05-14 00:04:47 +00:00
}
pub fn parse(input: &str) -> Result<Vec<NonMetricQuantity>, 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)?;
2023-05-14 00:04:47 +00:00
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)?;
2023-05-14 00:04:47 +00:00
let quantity = NonMetricQuantity {
amount: amount.take().expect("must have read a number to be in this state"),
2023-06-01 16:55:09 +00:00
unit,
2023-05-14 00:04:47 +00:00
};
quantities.push(quantity);
state = Expect::Number;
}
}
}
match state {
Expect::Number => {}
2023-05-14 00:04:47 +00:00
Expect::Unit => {
return Err(ParseError::ExpectedUnit);
}
}
Ok(quantities)
}
fn parse_number(input: String) -> Result<f64, ParseError> {
let no_whitespace: String = input.chars().filter(|c| !c.is_whitespace()).collect();
2023-06-01 16:55:09 +00:00
no_whitespace.parse().map_err(|_| ParseError::NotValidNumber(input))
}
fn parse_unit(input: String) -> Result<NonMetric, ParseError> {
match input.as_str() {
2023-05-14 00:04:47 +00:00
// 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),
2023-05-14 00:04:47 +00:00
2023-06-01 00:00:10 +00:00
// Mass
"ounce" => Ok(NonMetric::Ounce),
"ounces" => Ok(NonMetric::Ounce),
"oz" => Ok(NonMetric::Ounce),
2023-05-14 00:04:47 +00:00
"pound" => Ok(NonMetric::Pound),
"pounds" => Ok(NonMetric::Pound),
"lb" => Ok(NonMetric::Pound),
"lbs" => Ok(NonMetric::Pound),
"#" => Ok(NonMetric::Pound),
2023-05-14 00:04:47 +00:00
"stone" => Ok(NonMetric::Stone),
"stones" => Ok(NonMetric::Stone),
"st" => Ok(NonMetric::Stone),
2023-05-14 00:04:47 +00:00
2023-05-31 23:38:49 +00:00
"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),
2023-05-31 23:48:34 +00:00
"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),
2023-05-28 17:32:31 +00:00
// 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),
2023-05-28 17:32:31 +00:00
"°F" => Ok(NonMetric::Fahrenheit),
"F" => Ok(NonMetric::Fahrenheit),
2023-05-28 23:55:43 +00:00
// Area
2023-05-29 18:58:29 +00:00
"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),
2023-05-28 23:55:43 +00:00
"inch²" => Ok(NonMetric::SquareInch),
"inches²" => Ok(NonMetric::SquareInch),
"in²" => Ok(NonMetric::SquareInch),
2023-05-29 18:58:29 +00:00
"\"²" => 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),
2023-05-28 23:55:43 +00:00
2023-05-29 19:03:08 +00:00
"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),
2023-05-29 00:01:56 +00:00
"foot²" => Ok(NonMetric::SquareFoot),
"feet²" => Ok(NonMetric::SquareFoot),
"ft²" => Ok(NonMetric::SquareFoot),
2023-05-29 19:03:08 +00:00
"" => 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),
2023-05-29 00:01:56 +00:00
2023-06-01 16:24:11 +00:00
"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),
2023-05-29 00:18:30 +00:00
"acre" => Ok(NonMetric::Acre),
"acres" => Ok(NonMetric::Acre),
"ac" => Ok(NonMetric::Acre),
2023-05-29 19:05:08 +00:00
"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),
2023-05-29 00:27:55 +00:00
"mile²" => Ok(NonMetric::SquareMile),
"miles²" => Ok(NonMetric::SquareMile),
"mi²" => Ok(NonMetric::SquareMile),
2023-05-29 19:05:08 +00:00
"mile^2" => Ok(NonMetric::SquareMile),
"miles^2" => Ok(NonMetric::SquareMile),
"mi^2" => Ok(NonMetric::SquareMile),
2023-05-29 00:27:55 +00:00
2023-05-29 19:29:10 +00:00
// 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),
2023-05-29 19:40:03 +00:00
"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),
2023-06-01 16:31:55 +00:00
"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),
2023-05-30 17:01:35 +00:00
// Fluid volume
2023-05-30 17:34:38 +00:00
"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),
2023-05-31 19:38:50 +00:00
"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),
2023-05-31 19:29:58 +00:00
"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),
2023-05-31 19:19:55 +00:00
"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),
2023-05-31 19:10:49 +00:00
"US cup" => Ok(NonMetric::USCup),
"US cups" => Ok(NonMetric::USCup),
"us cup" => Ok(NonMetric::USCup),
"us cups" => Ok(NonMetric::USCup),
2023-06-01 00:09:00 +00:00
"cup" => Ok(NonMetric::USCup),
"cups" => Ok(NonMetric::USCup),
2023-05-31 19:10:49 +00:00
2023-05-31 14:49:24 +00:00
"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),
2023-05-31 14:42:18 +00:00
"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),
2023-05-31 14:35:19 +00:00
"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
2023-05-31 23:55:47 +00:00
"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)),
2023-05-14 00:04:47 +00:00
}
}
#[derive(Debug, PartialEq)]
enum Token {
Number(String),
Unit(String),
}
enum TokState {
Neutral,
Number,
Unit(bool),
2023-05-14 00:04:47 +00:00
}
fn tokenize(input: &str) -> Vec<Token> {
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);
2023-05-14 00:04:47 +00:00
}
}
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);
2023-05-14 00:04:47 +00:00
token = String::new();
token.push(c);
}
}
TokState::Unit(after_caret) => {
if !after_caret && (c.is_ascii_digit() || c == '-') {
2023-05-29 18:43:02 +00:00
tokens.push(Token::Unit(token.trim().to_string()));
2023-05-14 00:04:47 +00:00
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);
2023-05-14 00:04:47 +00:00
} else {
2023-05-29 18:43:02 +00:00
token.push(c);
2023-05-14 00:04:47 +00:00
}
}
}
}
match state {
2023-06-01 16:55:09 +00:00
TokState::Neutral => { assert!(token.is_empty()); }
2023-05-14 00:04:47 +00:00
TokState::Number => { tokens.push(Token::Number(token.trim().to_string())); }
TokState::Unit(_) => { tokens.push(Token::Unit(token.trim().to_string())); }
2023-05-14 00:04:47 +00:00
}
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 },
]));
2023-05-14 00:04:47 +00:00
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")));
2023-05-14 00:04:47 +00:00
}
#[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));
}
2023-05-14 00:04:47 +00:00
#[test]
fn units() {
// Length
2023-05-31 23:31:59 +00:00
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",
]);
2023-05-14 00:04:47 +00:00
2023-06-01 00:00:10 +00:00
// Mass
2023-05-31 23:31:59 +00:00
test_units(NonMetric::Ounce, &[
"ounce",
"ounces",
"oz",
]);
test_units(NonMetric::Pound, &[
"pound",
"pounds",
"lb",
"lbs",
"#",
]);
test_units(NonMetric::Stone, &[
"stone",
"stones",
"st",
]);
2023-05-14 00:04:47 +00:00
2023-05-31 23:38:49 +00:00
test_units(NonMetric::ShortTon, &[
"short ton",
"short tons",
"US ton",
"US tons",
"us ton",
"us tons",
]);
2023-05-31 23:48:34 +00:00
test_units(NonMetric::LongTon, &[
"long ton",
"long tons",
"imperial ton",
"imperial tons",
"imp ton",
"imp tons",
]);
2023-05-28 17:32:31 +00:00
// Temperature
2023-05-31 23:31:59 +00:00
test_units(NonMetric::Fahrenheit, &[
"degree Fahrenheit",
"degrees Fahrenheit",
"degree fahrenheit",
"degrees fahrenheit",
"Fahrenheit",
"fahrenheit",
"°F",
"F",
]);
2023-05-28 17:32:31 +00:00
2023-05-28 23:55:43 +00:00
// Area
2023-05-31 23:31:59 +00:00
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",
]);
2023-06-01 16:24:11 +00:00
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",
]);
2023-05-31 23:31:59 +00:00
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",
]);
2023-05-29 00:27:55 +00:00
2023-05-29 19:29:10 +00:00
// Volume
2023-05-31 23:31:59 +00:00
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",
]);
2023-05-29 19:40:03 +00:00
2023-06-01 16:31:55 +00:00
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",
]);
2023-05-30 17:01:35 +00:00
// Fluid volume
2023-05-31 23:31:59 +00:00
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",
2023-06-01 00:09:00 +00:00
"cup",
"cups",
2023-05-31 23:31:59 +00:00
]);
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));
}
}
2023-05-31 14:35:19 +00:00
#[test]
fn ambiguous_units() {
2023-05-31 23:55:47 +00:00
test_ambiguous_units(NonMetric::ShortTon, NonMetric::LongTon, &[
"ton",
"tons",
]);
2023-05-31 23:18:55 +00:00
test_ambiguous_units(NonMetric::ImperialFluidOunce, NonMetric::USFluidOunce, &[
"fluid ounce",
"fluid ounces",
"fl oz",
"fl. oz.",
"oz. fl.",
2023-05-31 23:18:55 +00:00
]);
2023-05-31 23:18:55 +00:00
test_ambiguous_units(NonMetric::ImperialPint, NonMetric::USLiquidPint, &[
"pint",
"pints",
"pt",
"p",
2023-05-31 23:18:55 +00:00
]);
2023-05-31 23:18:55 +00:00
test_ambiguous_units(NonMetric::ImperialQuart, NonMetric::USLiquidQuart, &[
"quart",
"quarts",
"qt",
2023-05-31 23:18:55 +00:00
]);
2023-05-31 23:18:55 +00:00
test_ambiguous_units(NonMetric::ImperialGallon, NonMetric::USGallon, &[
"gallon",
"gallons",
"gal",
2023-05-31 23:18:55 +00:00
]);
}
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 {
2023-05-31 23:18:55 +00:00
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())));
2023-05-14 00:04:47 +00:00
}
#[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())]);
2023-05-14 00:04:47 +00:00
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()),
]
);
2023-05-29 18:43:02 +00:00
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()),
]
);
2023-05-14 00:04:47 +00:00
}
}