use combine::parser::char::{digit, letter, spaces}; use combine::{many, many1, one_of, Parser}; use thiserror::Error; /// Errors for dice parsing. #[derive(Debug, Clone, PartialEq, Copy, Error)] pub enum DiceParsingError { #[error("invalid amount")] InvalidAmount, #[error("modifiers not specified properly")] InvalidModifiers, #[error("extraneous input detected")] UnconsumedInput, #[error("{0}")] InternalParseError(#[from] combine::error::StringStreamError), #[error("number conversion error")] ConversionError, } impl From for DiceParsingError { fn from(_error: std::num::ParseIntError) -> Self { DiceParsingError::ConversionError } } type ParseResult = Result; /// A parsed operator for a number. Whether to add or remove it from /// the total amount of dice rolled. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Operator { Plus, Minus, } impl Operator { /// Calculate multiplier for how to convert the number. Returns 1 /// for positive, and -1 for negative. pub fn mult(&self) -> i32 { match self { Operator::Plus => 1, Operator::Minus => -1, } } } /// One part of the dice amount in an expression. Can be a number or a /// variable name. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Element { /// This element in the expression is a variable, which will be /// resolved to a number by consulting the dtaabase. Variable(String), /// This element is a simple number, and will be added or /// subtracted from the total dice amount depending on its /// corresponding Operator. Number(i32), } /// One part of the parsed dice rolling expression. Combines an /// operator and an element into one struct. Examples of Amounts would /// be "+4" or "- myvariable", which translate to Operator::Plus and /// Element::Number(4), and Operator::Minus and /// Element::Variable("myvariable"), respectively. #[derive(Debug, PartialEq, Eq, Clone)] pub struct Amount { pub operator: Operator, pub element: Element, } /// Converts "+" or" -" into an Operator. No sign at all is an implied /// Plus. Part of the parser. fn map_operator(sign: char) -> Operator { match sign { '+' => Operator::Plus, '-' => Operator::Minus, _ => Operator::Plus, } } /// Attempt to convert the text at the start of the expression, /// extracted by the Combine parser, into an Amount instance. Part of /// the parser. fn map_first_amount(value: String) -> ParseResult { if value.chars().all(char::is_numeric) { let num = value.parse::()?; Ok(Amount { operator: Operator::Plus, element: Element::Number(num), }) } else { Ok(Amount { operator: Operator::Plus, element: Element::Variable(value), }) } } /// Attempt to convert some text in the middle or end of the string, /// extracted by the Combine parser, into an Element. Part of the /// parser. fn map_element(value: String) -> ParseResult { if value.chars().all(char::is_numeric) { let num = value.parse::()?; Ok(Element::Number(num)) } else { Ok(Element::Variable(value)) } } /// Collect an Operator and Element into an Amount, but only if the /// Element was successfully parsed. Part of the parser. fn map_amount((operator, element_result): (Operator, ParseResult)) -> ParseResult { match element_result { Ok(element) => Ok(Amount { operator, element }), Err(e) => Err(e), } } /// Parse an expression of numbers and/or variables into elements /// coupled with operators, where an operator is "+" or "-", and an /// element is either a number or variable name. The first element /// should not have an operator, but every one after that should. /// Accepts expressions like "8", "10 + variablename", "variablename - /// 3", etc. This function is currently common to systems that don't /// deal with XdY rolls. Support for that will be added later. Parsers /// utilzing this function should layer their own checks on top of /// this; perhaps they do not want more than one expression, or some /// other rules. pub fn parse_amounts(input: &str) -> ParseResult> { let input = input.trim(); // Single sub-parser for the first amount expression, because it's // easier. Wraps into ParseResult. let first_amount = many1(letter()) .or(many1(digit())) .skip(spaces().silent()) //Consume any space after first amount .map(map_first_amount); // All of this, down to amount_parser, will convert expressions // after the first into Amounts (wrapped in a ParseResult). let plus_or_minus = one_of("+-".chars()); let maybe_sign = plus_or_minus.map(map_operator); let variable_or_number = many1(letter()).or(many1(digit())).map(map_element); let element_parser = maybe_sign .skip(spaces().silent()) .and(variable_or_number) .skip(spaces().silent()); let amount_parser = element_parser.map(map_amount); let remaining_amounts = many(amount_parser).map(|amounts: Vec>| amounts); let mut parser = first_amount.and(remaining_amounts); // Collapses first amount + remaining amounts into a single Vec, // while collecting extraneous input. type ParsedAmountExpr = (ParseResult, Vec>); let (results, rest) = parser .parse(input) .map(|mut results: (ParsedAmountExpr, &str)| { let mut amounts = vec![(results.0).0]; amounts.append(&mut (results.0).1); (amounts, results.1) })?; if rest.len() == 0 { // Any ParseResult errors will short-circuit the collect. results.into_iter().collect() } else { Err(DiceParsingError::UnconsumedInput) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_single_number_amount_test() { let result = parse_amounts("1"); assert!(result.is_ok()); assert_eq!( result.unwrap(), vec![Amount { operator: Operator::Plus, element: Element::Number(1) }] ); let result = parse_amounts("10"); assert!(result.is_ok()); assert_eq!( result.unwrap(), vec![Amount { operator: Operator::Plus, element: Element::Number(10) }] ); } #[test] fn parsing_huge_number_should_error() { // A number outside the bounds of i32 should not be a valid // parse. let result = parse_amounts("159875294375198734982379875392"); assert!(result.is_err()); assert!(result.unwrap_err() == DiceParsingError::ConversionError); } #[test] fn parse_single_variable_amount_test() { let result = parse_amounts("asdf"); assert!(result.is_ok()); assert_eq!( result.unwrap(), vec![Amount { operator: Operator::Plus, element: Element::Variable("asdf".to_string()) }] ); let result = parse_amounts("nosis"); assert!(result.is_ok()); assert_eq!( result.unwrap(), vec![Amount { operator: Operator::Plus, element: Element::Variable("nosis".to_string()) }] ); } #[test] fn parse_complex_amount_expression() { assert!(parse_amounts("1 + myvariable - 2").is_ok()); } }