2021-02-03 23:21:37 +00:00
|
|
|
use combine::error::ParseError;
|
2020-10-31 13:52:46 +00:00
|
|
|
use combine::parser::char::{digit, letter, spaces};
|
2021-02-03 23:21:37 +00:00
|
|
|
use combine::stream::Stream;
|
2020-10-31 13:52:46 +00:00
|
|
|
use combine::{many, many1, one_of, Parser};
|
|
|
|
use thiserror::Error;
|
|
|
|
|
2021-02-03 22:55:29 +00:00
|
|
|
/// Errors for dice parsing.
|
|
|
|
#[derive(Debug, Clone, PartialEq, Copy, Error)]
|
2020-10-31 13:52:46 +00:00
|
|
|
pub enum DiceParsingError {
|
2020-10-31 14:03:18 +00:00
|
|
|
#[error("invalid amount")]
|
2020-10-31 13:52:46 +00:00
|
|
|
InvalidAmount,
|
|
|
|
|
|
|
|
#[error("modifiers not specified properly")]
|
|
|
|
InvalidModifiers,
|
|
|
|
|
|
|
|
#[error("extraneous input detected")]
|
|
|
|
UnconsumedInput,
|
|
|
|
|
2020-11-04 20:14:14 +00:00
|
|
|
#[error("{0}")]
|
2020-10-31 13:52:46 +00:00
|
|
|
InternalParseError(#[from] combine::error::StringStreamError),
|
2021-02-03 22:55:29 +00:00
|
|
|
|
2021-02-03 23:26:41 +00:00
|
|
|
#[error("number parsing error (too large?)")]
|
2021-02-03 22:55:29 +00:00
|
|
|
ConversionError,
|
2020-10-31 13:52:46 +00:00
|
|
|
}
|
|
|
|
|
2021-02-03 22:55:29 +00:00
|
|
|
impl From<std::num::ParseIntError> for DiceParsingError {
|
|
|
|
fn from(_error: std::num::ParseIntError) -> Self {
|
|
|
|
DiceParsingError::ConversionError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type ParseResult<T> = Result<T, DiceParsingError>;
|
|
|
|
|
|
|
|
/// A parsed operator for a number. Whether to add or remove it from
|
|
|
|
/// the total amount of dice rolled.
|
2020-10-31 13:52:46 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
|
|
pub enum Operator {
|
|
|
|
Plus,
|
|
|
|
Minus,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Operator {
|
2021-02-03 22:55:29 +00:00
|
|
|
/// Calculate multiplier for how to convert the number. Returns 1
|
|
|
|
/// for positive, and -1 for negative.
|
2020-10-31 13:52:46 +00:00
|
|
|
pub fn mult(&self) -> i32 {
|
|
|
|
match self {
|
|
|
|
Operator::Plus => 1,
|
|
|
|
Operator::Minus => -1,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-03 22:55:29 +00:00
|
|
|
/// One part of the dice amount in an expression. Can be a number or a
|
|
|
|
/// variable name.
|
2020-10-31 13:52:46 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
|
|
pub enum Element {
|
2021-02-03 22:55:29 +00:00
|
|
|
/// This element in the expression is a variable, which will be
|
|
|
|
/// resolved to a number by consulting the dtaabase.
|
2020-10-31 13:52:46 +00:00
|
|
|
Variable(String),
|
2021-02-03 22:55:29 +00:00
|
|
|
|
|
|
|
/// This element is a simple number, and will be added or
|
|
|
|
/// subtracted from the total dice amount depending on its
|
|
|
|
/// corresponding Operator.
|
2020-10-31 13:52:46 +00:00
|
|
|
Number(i32),
|
|
|
|
}
|
|
|
|
|
2021-02-03 22:55:29 +00:00
|
|
|
/// 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.
|
2020-10-31 13:52:46 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
|
|
pub struct Amount {
|
|
|
|
pub operator: Operator,
|
|
|
|
pub element: Element,
|
|
|
|
}
|
|
|
|
|
2021-02-03 23:21:37 +00:00
|
|
|
/// Parser that attempt to convert the text at the start of the dice
|
|
|
|
/// parsing into an Amount instance.
|
|
|
|
fn first_amount_parser<Input>() -> impl Parser<Input, Output = ParseResult<Amount>>
|
|
|
|
where
|
|
|
|
Input: Stream<Token = char>,
|
|
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
|
|
{
|
|
|
|
let map_first_amount = |value: String| {
|
|
|
|
if value.chars().all(char::is_numeric) {
|
|
|
|
let num = value.parse::<i32>()?;
|
|
|
|
Ok(Amount {
|
|
|
|
operator: Operator::Plus,
|
|
|
|
element: Element::Number(num),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
Ok(Amount {
|
|
|
|
operator: Operator::Plus,
|
|
|
|
element: Element::Variable(value),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
many1(letter())
|
|
|
|
.or(many1(digit()))
|
|
|
|
.skip(spaces().silent()) //Consume any space after first amount
|
|
|
|
.map(map_first_amount)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempt to convert some text in the middle or end of the dice roll
|
|
|
|
/// string into an Amount.
|
|
|
|
fn amount_parser<Input>() -> impl Parser<Input, Output = ParseResult<Amount>>
|
|
|
|
where
|
|
|
|
Input: Stream<Token = char>,
|
|
|
|
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
|
|
|
|
{
|
|
|
|
let plus_or_minus = one_of("+-".chars());
|
|
|
|
let parse_operator = plus_or_minus.map(|sign: char| match sign {
|
2021-02-03 22:55:29 +00:00
|
|
|
'+' => Operator::Plus,
|
|
|
|
'-' => Operator::Minus,
|
|
|
|
_ => Operator::Plus,
|
2021-02-03 23:21:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Element must either be a proper i32, or a variable name.
|
|
|
|
let map_element = |value: String| -> ParseResult<Element> {
|
|
|
|
if value.chars().all(char::is_numeric) {
|
|
|
|
let num = value.parse::<i32>()?;
|
|
|
|
Ok(Element::Number(num))
|
|
|
|
} else {
|
|
|
|
Ok(Element::Variable(value))
|
|
|
|
}
|
|
|
|
};
|
2021-02-03 22:55:29 +00:00
|
|
|
|
2021-02-03 23:21:37 +00:00
|
|
|
let parse_element = many1(letter()).or(many1(digit())).map(map_element);
|
2021-02-03 22:55:29 +00:00
|
|
|
|
2021-02-03 23:21:37 +00:00
|
|
|
let element_parser = parse_operator
|
|
|
|
.skip(spaces().silent())
|
|
|
|
.and(parse_element)
|
|
|
|
.skip(spaces().silent());
|
2021-02-03 22:55:29 +00:00
|
|
|
|
2021-02-03 23:21:37 +00:00
|
|
|
let convert_to_amount = |(operator, element_result)| match element_result {
|
2021-02-03 22:55:29 +00:00
|
|
|
Ok(element) => Ok(Amount { operator, element }),
|
|
|
|
Err(e) => Err(e),
|
2021-02-03 23:21:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
element_parser.map(convert_to_amount)
|
2021-02-03 22:55:29 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 13:52:46 +00:00
|
|
|
/// 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.
|
2021-02-03 22:55:29 +00:00
|
|
|
pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
|
2020-10-31 13:52:46 +00:00
|
|
|
let input = input.trim();
|
|
|
|
|
2021-02-03 23:21:37 +00:00
|
|
|
let remaining_amounts = many(amount_parser()).map(|amounts: Vec<ParseResult<Amount>>| amounts);
|
|
|
|
let mut parser = first_amount_parser().and(remaining_amounts);
|
2020-10-31 13:52:46 +00:00
|
|
|
|
2021-02-03 22:55:29 +00:00
|
|
|
// Collapses first amount + remaining amounts into a single Vec,
|
|
|
|
// while collecting extraneous input.
|
|
|
|
type ParsedAmountExpr = (ParseResult<Amount>, Vec<ParseResult<Amount>>);
|
2020-10-31 13:52:46 +00:00
|
|
|
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 {
|
2021-02-03 22:55:29 +00:00
|
|
|
// Any ParseResult errors will short-circuit the collect.
|
|
|
|
results.into_iter().collect()
|
2020-10-31 13:52:46 +00:00
|
|
|
} 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)
|
|
|
|
}]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-02-03 22:55:29 +00:00
|
|
|
#[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);
|
|
|
|
}
|
|
|
|
|
2020-10-31 13:52:46 +00:00
|
|
|
#[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());
|
|
|
|
}
|
|
|
|
}
|