use super::tokens::Token;
use crate::units::parse_unit;
use chumsky::prelude::*;
pub fn parse_expression_chumsky(input: &str) -> Result<Vec<Token>, String> {
let parser = create_token_parser();
match parser.parse(input).into_result() {
Ok(tokens) => {
let mut paren_count = 0;
for token in &tokens {
match token {
Token::LeftParen => paren_count += 1,
Token::RightParen => {
paren_count -= 1;
if paren_count < 0 {
return Err("Unmatched closing parenthesis".to_string());
}
}
_ => {}
}
}
if paren_count != 0 {
return Err("Unmatched opening parenthesis".to_string());
}
for i in 0..tokens.len().saturating_sub(1) {
let current = &tokens[i];
let next = &tokens[i + 1];
let is_current_op = matches!(
current,
Token::Plus | Token::Minus | Token::Multiply | Token::Divide | Token::Power
);
let is_next_op = matches!(
next,
Token::Plus | Token::Minus | Token::Multiply | Token::Divide | Token::Power
);
if is_current_op && is_next_op {
if !matches!(next, Token::Minus) {
return Err("Invalid consecutive operators".to_string());
}
}
}
Ok(tokens)
}
Err(errs) => {
let error_msg = errs
.into_iter()
.map(|e| format!("{:?}", e))
.collect::<Vec<_>>()
.join(", ");
Err(error_msg)
}
}
}
fn create_token_parser<'a>() -> impl Parser<'a, &'a str, Vec<Token>, extra::Err<Rich<'a, char>>> {
let number = choice((
text::digits(10)
.then(just(',').then(text::digits(10)).repeated())
.then(just('.').then(text::digits(10)).or_not())
.to_slice(),
text::int(10)
.then(just('.').then(text::digits(10)).or_not())
.to_slice(),
))
.map(|s: &str| {
let cleaned = s.replace(",", "");
cleaned.parse::<f64>().unwrap_or(0.0)
});
let identifier = text::ascii::ident().map(|s: &str| s.to_string());
let percent_symbol = just('%').map(|_| "%".to_string());
let currency_symbol = choice((
just('$').to("$"),
just('€').to("€"),
just('£').to("£"),
just('Â¥').to("Â¥"),
just('₹').to("₹"),
just('â‚©').to("â‚©"),
))
.map(|s: &str| s.to_string());
let compound_identifier = text::ascii::ident()
.then(
just('/')
.padded() .then(text::ascii::ident()),
)
.try_map(|(base, (_, suffix)): (&str, (char, &str)), span| {
let compound = format!("{}/{}", base, suffix);
if parse_unit(&compound).is_some() {
Ok(compound)
} else {
Err(Rich::custom(
span,
"Invalid compound identifier - not a valid unit",
))
}
});
let currency_rate = currency_symbol
.then(just('/'))
.then(text::ascii::ident())
.try_map(
|((currency_str, _), time_str): ((String, char), &str), span| {
let compound = format!("{}/{}", currency_str, time_str);
if parse_unit(&compound).is_some() {
Ok(compound)
} else {
Err(Rich::custom(span, "Invalid currency rate unit"))
}
},
);
let line_ref = just("line")
.then(text::int(10))
.map(|(_, num_str): (_, &str)| {
if let Ok(line_num) = num_str.parse::<usize>() {
if line_num > 0 {
Token::LineReference(line_num - 1)
} else {
Token::LineReference(0)
}
} else {
Token::LineReference(0)
}
});
let keyword = choice((
text::keyword("to").to(Token::To),
text::keyword("in").to(Token::In),
text::keyword("of").to(Token::Of),
));
let operator = choice((
just('+').to(Token::Plus),
just('-').to(Token::Minus),
just('*').to(Token::Multiply),
just('/').to(Token::Divide),
just('^').to(Token::Power),
just('(').to(Token::LeftParen),
just(')').to(Token::RightParen),
just('=').to(Token::Assign),
));
let unit_identifier = choice((
currency_rate, compound_identifier,
identifier,
percent_symbol,
currency_symbol,
));
let number_with_unit = number
.then(
just(' ')
.repeated()
.then(unit_identifier)
.try_map(|(_, unit_str): ((), String), span| {
if unit_str == "to" || unit_str == "in" || unit_str == "of" {
Err(Rich::custom(span, "Keywords are not units"))
} else if let Some(unit) = parse_unit(&unit_str) {
Ok(unit)
} else {
Err(Rich::custom(span, format!("Unknown unit: {}", unit_str)))
}
})
.or_not(),
)
.map(|(num, unit_opt)| {
if let Some(unit) = unit_opt {
Token::NumberWithUnit(num, unit)
} else {
Token::Number(num)
}
});
#[allow(clippy::type_complexity)]
let currency_rate_amount = currency_symbol
.then(just(' ').repeated()) .then(number)
.then(just('/'))
.then(text::ascii::ident())
.try_map(|parsed: ((((String, ()), f64), char), &str), span| {
let ((((currency_str, _), amount), _), time_str) = parsed;
let compound = format!("{}/{}", currency_str, time_str);
if let Some(unit) = parse_unit(&compound) {
Ok(Token::NumberWithUnit(amount, unit))
} else {
Err(Rich::custom(span, "Invalid currency rate unit"))
}
});
let currency_amount = currency_symbol
.then(just(' ').repeated()) .then(number)
.map(|((currency_str, _), amount)| {
if let Some(unit) = parse_unit(¤cy_str) {
Token::NumberWithUnit(amount, unit)
} else {
Token::Number(amount) }
});
let standalone_unit = unit_identifier.try_map(|word: String, span| {
if let Some(unit) = parse_unit(&word) {
Ok(Token::NumberWithUnit(1.0, unit))
} else {
Err(Rich::custom(span, "Not a unit"))
}
});
let function = identifier
.then_ignore(just(' ').repeated())
.then_ignore(just('(').rewind())
.try_map(|name: String, span| match name.to_lowercase().as_str() {
"sqrt" => Ok(Token::Function(name)),
_ => Err(Rich::custom(span, "Unknown function")),
});
let variable = identifier.map(|word: String| Token::Variable(word));
let token = choice((
line_ref, keyword, currency_rate_amount, currency_amount, number_with_unit, operator, function, standalone_unit, variable, ));
let punctuation = choice((
just(':'),
just(';'),
just(','),
just('!'),
just('?'),
just('.'), just('"'),
just('\''),
just('`'),
just('|'),
just('&'),
just('#'),
just('@'),
just('~'),
just('['),
just(']'),
just('{'),
just('}'),
just('<'),
just('>'),
));
let element = choice((token.map(Some), punctuation.to(None)));
element
.padded()
.repeated()
.collect::<Vec<_>>()
.map(|elements| elements.into_iter().flatten().collect())
.then_ignore(end())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::units::Unit;
#[test]
fn test_number_parsing() {
let result = parse_expression_chumsky("42");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::Number(42.0)));
}
#[test]
fn test_number_with_unit() {
let result = parse_expression_chumsky("5 GiB");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::NumberWithUnit(5.0, Unit::GiB)));
}
#[test]
fn test_simple_arithmetic() {
let result = parse_expression_chumsky("2 + 3");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(tokens[0], Token::Number(2.0)));
assert!(matches!(tokens[1], Token::Plus));
assert!(matches!(tokens[2], Token::Number(3.0)));
}
#[test]
fn test_line_reference() {
let result = parse_expression_chumsky("line1 + 4");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(tokens[0], Token::LineReference(0)));
assert!(matches!(tokens[1], Token::Plus));
assert!(matches!(tokens[2], Token::Number(4.0)));
}
#[test]
fn test_complex_expressions() {
let result = parse_expression_chumsky("line1 * 2 GiB + 500 MiB to KiB");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 7);
assert!(matches!(tokens[0], Token::LineReference(0)));
assert!(matches!(tokens[1], Token::Multiply));
assert!(matches!(tokens[2], Token::NumberWithUnit(2.0, Unit::GiB)));
assert!(matches!(tokens[3], Token::Plus));
assert!(matches!(tokens[4], Token::NumberWithUnit(500.0, Unit::MiB)));
assert!(matches!(tokens[5], Token::To));
assert!(matches!(tokens[6], Token::NumberWithUnit(1.0, Unit::KiB)));
}
#[test]
fn test_parentheses() {
let result = parse_expression_chumsky("(5 + 3) * 2");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 7);
assert!(matches!(tokens[0], Token::LeftParen));
assert!(matches!(tokens[1], Token::Number(5.0)));
assert!(matches!(tokens[2], Token::Plus));
assert!(matches!(tokens[3], Token::Number(3.0)));
assert!(matches!(tokens[4], Token::RightParen));
assert!(matches!(tokens[5], Token::Multiply));
assert!(matches!(tokens[6], Token::Number(2.0)));
}
#[test]
fn test_conversion() {
let result = parse_expression_chumsky("1 GiB to KiB");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(tokens[0], Token::NumberWithUnit(1.0, Unit::GiB)));
assert!(matches!(tokens[1], Token::To));
assert!(matches!(tokens[2], Token::NumberWithUnit(1.0, Unit::KiB)));
}
#[test]
fn test_in_keyword() {
let result = parse_expression_chumsky("24 MiB * 32 in KiB");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 5);
assert!(matches!(tokens[0], Token::NumberWithUnit(24.0, Unit::MiB)));
assert!(matches!(tokens[1], Token::Multiply));
assert!(matches!(tokens[2], Token::Number(32.0)));
assert!(matches!(tokens[3], Token::In));
assert!(matches!(tokens[4], Token::NumberWithUnit(1.0, Unit::KiB)));
}
#[test]
fn test_time_rate_multiplication() {
let result = parse_expression_chumsky("1 hour * 10 GiB/s");
println!("Tokens for '1 hour * 10 GiB/s': {:?}", result);
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(tokens[0], Token::NumberWithUnit(1.0, _)));
assert!(matches!(tokens[1], Token::Multiply));
assert!(matches!(tokens[2], Token::NumberWithUnit(10.0, _)));
}
#[test]
fn test_comma_separated_numbers() {
let result = parse_expression_chumsky("1,000 GiB");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(1000.0, Unit::GiB)
));
let result = parse_expression_chumsky("1,234.56 MB");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(1234.56, Unit::MB)
));
let result = parse_expression_chumsky("1,000,000 bytes");
assert!(result.is_ok(), "Parsing failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(1000000.0, Unit::Byte)
));
}
#[test]
fn test_numbers_without_spaces() {
let result = parse_expression_chumsky("5GiB");
assert!(result.is_ok(), "Parsing '5GiB' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::NumberWithUnit(5.0, Unit::GiB)));
let result = parse_expression_chumsky("100MB");
assert!(result.is_ok(), "Parsing '100MB' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::NumberWithUnit(100.0, Unit::MB)));
let result = parse_expression_chumsky("2.5TiB");
assert!(result.is_ok(), "Parsing '2.5TiB' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::NumberWithUnit(2.5, Unit::TiB)));
let result = parse_expression_chumsky("1,000GiB");
assert!(result.is_ok(), "Parsing '1,000GiB' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(1000.0, Unit::GiB)
));
let result = parse_expression_chumsky("10GiB/s");
assert!(result.is_ok(), "Parsing '10GiB/s' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(10.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[0] {
assert_eq!(**unit1, Unit::GiB);
assert_eq!(**unit2, Unit::Second);
}
let result = parse_expression_chumsky("1,000GiB + 512MiB");
assert!(
result.is_ok(),
"Parsing '1,000GiB + 512MiB' failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(1000.0, Unit::GiB)
));
assert!(matches!(tokens[1], Token::Plus));
assert!(matches!(tokens[2], Token::NumberWithUnit(512.0, Unit::MiB)));
}
#[test]
fn test_edge_case_numbers() {
let result = parse_expression_chumsky("0");
assert!(result.is_ok(), "Parsing '0' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::Number(0.0)));
let result = parse_expression_chumsky("0 GiB");
assert!(result.is_ok(), "Parsing '0 GiB' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::NumberWithUnit(0.0, Unit::GiB)));
let result = parse_expression_chumsky("0.5 MB");
assert!(result.is_ok(), "Parsing '0.5 MB' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(tokens[0], Token::NumberWithUnit(0.5, Unit::MB)));
let result = parse_expression_chumsky("999,999,999.99 TB");
assert!(result.is_ok(), "Parsing large number failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(999999999.99, Unit::TB)
));
let result = parse_expression_chumsky("0.000001 seconds");
assert!(result.is_ok(), "Parsing small decimal failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(0.000001, Unit::Second)
));
}
#[test]
fn test_all_operators() {
let result = parse_expression_chumsky("1 + 2 - 3 * 4 / 5");
assert!(result.is_ok(), "Parsing all operators failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 9);
assert!(matches!(tokens[0], Token::Number(1.0)));
assert!(matches!(tokens[1], Token::Plus));
assert!(matches!(tokens[2], Token::Number(2.0)));
assert!(matches!(tokens[3], Token::Minus));
assert!(matches!(tokens[4], Token::Number(3.0)));
assert!(matches!(tokens[5], Token::Multiply));
assert!(matches!(tokens[6], Token::Number(4.0)));
assert!(matches!(tokens[7], Token::Divide));
assert!(matches!(tokens[8], Token::Number(5.0)));
}
#[test]
fn test_nested_parentheses() {
let result = parse_expression_chumsky("((1 + 2) * (3 - 4)) / 5");
assert!(
result.is_ok(),
"Parsing nested parentheses failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 15);
assert!(matches!(tokens[0], Token::LeftParen));
assert!(matches!(tokens[1], Token::LeftParen));
assert!(matches!(tokens[2], Token::Number(1.0)));
assert!(matches!(tokens[13], Token::Divide));
assert!(matches!(tokens[14], Token::Number(5.0)));
}
#[test]
fn test_multiple_line_references() {
let result = parse_expression_chumsky("line1 + line2 * line10");
assert!(
result.is_ok(),
"Parsing multiple line refs failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 5);
assert!(matches!(tokens[0], Token::LineReference(0)));
assert!(matches!(tokens[1], Token::Plus));
assert!(matches!(tokens[2], Token::LineReference(1)));
assert!(matches!(tokens[3], Token::Multiply));
assert!(matches!(tokens[4], Token::LineReference(9)));
}
#[test]
fn test_all_unit_types() {
let result = parse_expression_chumsky("1 B + 2 KB + 3 MB + 4 GB + 5 TB + 6 PB + 7 EB");
assert!(result.is_ok(), "Parsing data units failed: {:?}", result);
let result = parse_expression_chumsky("1 KiB + 2 MiB + 3 GiB + 4 TiB + 5 PiB + 6 EiB");
assert!(
result.is_ok(),
"Parsing binary data units failed: {:?}",
result
);
let result = parse_expression_chumsky("1 ns + 2 us + 3 ms + 4 s + 5 min + 6 h + 7 day");
assert!(result.is_ok(), "Parsing time units failed: {:?}", result);
let result = parse_expression_chumsky("1 B/s + 2 KB/s + 3 GiB/s");
assert!(result.is_ok(), "Parsing rate units failed: {:?}", result);
let result = parse_expression_chumsky("1 QPS + 2 QPM + 3 QPH + 4 req/s");
assert!(result.is_ok(), "Parsing QPS units failed: {:?}", result);
let result = parse_expression_chumsky("1 bit + 2 Kb + 3 Mb + 4 Gb");
assert!(result.is_ok(), "Parsing bit units failed: {:?}", result);
}
#[test]
fn test_keyword_combinations() {
let result = parse_expression_chumsky("1 GiB to MB in KiB");
assert!(result.is_ok(), "Parsing keywords failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 5);
assert!(matches!(tokens[1], Token::To));
assert!(matches!(tokens[3], Token::In));
let result = parse_expression_chumsky("line1 to GiB");
assert!(
result.is_ok(),
"Parsing line ref + keyword failed: {:?}",
result
);
let result = parse_expression_chumsky("(1 GiB + 512 MiB) * 2 to TB");
assert!(
result.is_ok(),
"Parsing complex + keyword failed: {:?}",
result
);
}
#[test]
fn test_whitespace_variations() {
let result = parse_expression_chumsky(" 1 + 2 ");
assert!(result.is_ok(), "Parsing extra spaces failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
let result = parse_expression_chumsky("1\t+\t2");
assert!(result.is_ok(), "Parsing tabs failed: {:?}", result);
let result = parse_expression_chumsky("1+2*3");
assert!(result.is_ok(), "Parsing no spaces failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 5);
}
#[test]
fn test_exponentiation_parsing() {
let result = parse_expression_chumsky("2^3");
assert!(result.is_ok(), "Parsing '2^3' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(tokens[0], Token::Number(2.0)));
assert!(matches!(tokens[1], Token::Power));
assert!(matches!(tokens[2], Token::Number(3.0)));
let result = parse_expression_chumsky("2 ^ 3");
assert!(
result.is_ok(),
"Parsing '2 ^ 3' with spaces failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
let result = parse_expression_chumsky("2^3^2");
assert!(result.is_ok(), "Parsing '2^3^2' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 5);
}
#[test]
fn test_function_parsing() {
let result = parse_expression_chumsky("sqrt(4)");
assert!(result.is_ok(), "Parsing 'sqrt(4)' failed: {:?}", result);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 4);
assert!(matches!(tokens[0], Token::Function(ref name) if name == "sqrt"));
assert!(matches!(tokens[1], Token::LeftParen));
assert!(matches!(tokens[2], Token::Number(4.0)));
assert!(matches!(tokens[3], Token::RightParen));
let result = parse_expression_chumsky("sqrt (9)");
assert!(
result.is_ok(),
"Parsing 'sqrt (9)' with space failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 4);
assert!(matches!(tokens[0], Token::Function(ref name) if name == "sqrt"));
let result = parse_expression_chumsky("2 + sqrt(16)");
assert!(
result.is_ok(),
"Parsing '2 + sqrt(16)' failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 6); }
#[test]
fn test_compound_units_with_spaces() {
let result = parse_expression_chumsky("100 MB / s");
assert!(
result.is_ok(),
"Parsing 'MB / s' with spaces failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(100.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[0] {
assert_eq!(**unit1, Unit::MB);
assert_eq!(**unit2, Unit::Second);
}
let result = parse_expression_chumsky("100 MB/s");
assert!(
result.is_ok(),
"Parsing 'MB/s' without spaces failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 1);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(100.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[0] {
assert_eq!(**unit1, Unit::MB);
assert_eq!(**unit2, Unit::Second);
}
let result = parse_expression_chumsky("25 QPS to req / min");
assert!(
result.is_ok(),
"Parsing QPS conversion with spaces failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(25.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[0] {
assert_eq!(**unit1, Unit::Query);
assert_eq!(**unit2, Unit::Second);
}
assert!(matches!(tokens[1], Token::To));
assert!(matches!(
tokens[2],
Token::NumberWithUnit(1.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[2] {
assert_eq!(**unit1, Unit::Request);
assert_eq!(**unit2, Unit::Minute);
}
let result = parse_expression_chumsky("50 req / s + 30 requests / min");
assert!(
result.is_ok(),
"Parsing request rates with spaces failed: {:?}",
result
);
let tokens = result.unwrap();
assert_eq!(tokens.len(), 3);
assert!(matches!(
tokens[0],
Token::NumberWithUnit(50.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[0] {
assert_eq!(**unit1, Unit::Request);
assert_eq!(**unit2, Unit::Second);
}
assert!(matches!(tokens[1], Token::Plus));
assert!(matches!(
tokens[2],
Token::NumberWithUnit(30.0, Unit::RateUnit(_, _))
));
if let Token::NumberWithUnit(_, Unit::RateUnit(ref unit1, ref unit2)) = tokens[2] {
assert_eq!(**unit1, Unit::Request);
assert_eq!(**unit2, Unit::Minute);
}
}
#[test]
fn test_error_cases() {
let result = parse_expression_chumsky("1 invalidunit");
assert!(result.is_ok(), "Should parse as number + variable");
let result = parse_expression_chumsky("line0");
assert!(
result.is_ok(),
"line0 should be valid (0-indexed internally)"
);
let result = parse_expression_chumsky("1 + 2)");
assert!(result.is_err(), "Should fail on unmatched parentheses");
let result = parse_expression_chumsky("1 ++ 2");
assert!(result.is_err(), "Should fail on double operators");
let result = parse_expression_chumsky("1.2.3");
assert!(result.is_ok(), "Should parse as separate tokens: 1.2 and 3");
let tokens = result.unwrap();
assert_eq!(tokens.len(), 2); }
#[test]
fn test_case_sensitivity() {
let result = parse_expression_chumsky("1 gib + 2 GIB + 3 GiB");
assert!(result.is_ok(), "Case sensitivity test failed: {:?}", result);
let result = parse_expression_chumsky("1 GiB to mb");
assert!(result.is_ok(), "Keyword case test failed: {:?}", result);
let result = parse_expression_chumsky("1 GiB in kb");
assert!(result.is_ok(), "Keyword case test failed: {:?}", result);
}
#[test]
fn test_complex_real_world_expressions() {
let result = parse_expression_chumsky("(50PB + 10EB) / 1000 to TB/s");
assert!(
result.is_ok(),
"Complex data center calc failed: {:?}",
result
);
let result = parse_expression_chumsky("(100QPS + 50req/s) * 1hour to queries");
assert!(result.is_ok(), "Complex QPS calc failed: {:?}", result);
let result = parse_expression_chumsky("1000GiB / 10min + 500MB/s * 2h");
assert!(result.is_ok(), "Mixed unit calc failed: {:?}", result);
let result = parse_expression_chumsky("(line1 + line2) * 2.5 to GiB/s");
assert!(result.is_ok(), "Complex line ref calc failed: {:?}", result);
}
}