forked from projectmoon/tenebrous-dicebot
Parsing huge numbers are now errors, not variables.
Fixed the dice amount parsing code to propagate a parsing result through the parser, while properly handling string to i32 conversion. We now only attempt to convert to i32 if all characters in the expression are numeric. This commit also refactors the dice parsing by moving most of the parsing closures into separate functions, which makes the parsing function itself more readable. Some variable names were also changed, for further clarity. Fixes #21.
This commit is contained in:
parent
f5a8e16ce0
commit
9a5a18268c
152
src/parser.rs
152
src/parser.rs
|
@ -2,10 +2,8 @@ use combine::parser::char::{digit, letter, spaces};
|
||||||
use combine::{many, many1, one_of, Parser};
|
use combine::{many, many1, one_of, Parser};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
//******************************
|
/// Errors for dice parsing.
|
||||||
//New hotness
|
#[derive(Debug, Clone, PartialEq, Copy, Error)]
|
||||||
//******************************
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Error)]
|
|
||||||
pub enum DiceParsingError {
|
pub enum DiceParsingError {
|
||||||
#[error("invalid amount")]
|
#[error("invalid amount")]
|
||||||
InvalidAmount,
|
InvalidAmount,
|
||||||
|
@ -18,8 +16,21 @@ pub enum DiceParsingError {
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
InternalParseError(#[from] combine::error::StringStreamError),
|
InternalParseError(#[from] combine::error::StringStreamError),
|
||||||
|
|
||||||
|
#[error("number conversion error")]
|
||||||
|
ConversionError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Operator {
|
pub enum Operator {
|
||||||
Plus,
|
Plus,
|
||||||
|
@ -27,6 +38,8 @@ pub enum Operator {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
|
/// Calculate multiplier for how to convert the number. Returns 1
|
||||||
|
/// for positive, and -1 for negative.
|
||||||
pub fn mult(&self) -> i32 {
|
pub fn mult(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Operator::Plus => 1,
|
Operator::Plus => 1,
|
||||||
|
@ -35,18 +48,80 @@ impl Operator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One part of the dice amount in an expression. Can be a number or a
|
||||||
|
/// variable name.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Element {
|
pub enum Element {
|
||||||
|
/// This element in the expression is a variable, which will be
|
||||||
|
/// resolved to a number by consulting the dtaabase.
|
||||||
Variable(String),
|
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),
|
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)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Amount {
|
pub struct Amount {
|
||||||
pub operator: Operator,
|
pub operator: Operator,
|
||||||
pub element: Element,
|
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<Amount> {
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Element> {
|
||||||
|
if value.chars().all(char::is_numeric) {
|
||||||
|
let num = value.parse::<i32>()?;
|
||||||
|
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<Element>)) -> ParseResult<Amount> {
|
||||||
|
match element_result {
|
||||||
|
Ok(element) => Ok(Amount { operator, element }),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse an expression of numbers and/or variables into elements
|
/// Parse an expression of numbers and/or variables into elements
|
||||||
/// coupled with operators, where an operator is "+" or "-", and an
|
/// coupled with operators, where an operator is "+" or "-", and an
|
||||||
/// element is either a number or variable name. The first element
|
/// element is either a number or variable name. The first element
|
||||||
|
@ -57,55 +132,36 @@ pub struct Amount {
|
||||||
/// utilzing this function should layer their own checks on top of
|
/// utilzing this function should layer their own checks on top of
|
||||||
/// this; perhaps they do not want more than one expression, or some
|
/// this; perhaps they do not want more than one expression, or some
|
||||||
/// other rules.
|
/// other rules.
|
||||||
pub fn parse_amounts(input: &str) -> Result<Vec<Amount>, DiceParsingError> {
|
pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
|
||||||
let input = input.trim();
|
let input = input.trim();
|
||||||
|
|
||||||
let plus_or_minus = one_of("+-".chars());
|
// Single sub-parser for the first amount expression, because it's
|
||||||
let maybe_sign = plus_or_minus.map(|sign: char| match sign {
|
// easier. Wraps into ParseResult.
|
||||||
'+' => Operator::Plus,
|
let first_amount = many1(letter())
|
||||||
'-' => Operator::Minus,
|
|
||||||
_ => Operator::Plus,
|
|
||||||
});
|
|
||||||
|
|
||||||
//TODO make this a macro or something
|
|
||||||
let first = many1(letter())
|
|
||||||
.or(many1(digit()))
|
.or(many1(digit()))
|
||||||
.skip(spaces().silent()) //Consume any space after first amount
|
.skip(spaces().silent()) //Consume any space after first amount
|
||||||
.map(|value: String| match value.parse::<i32>() {
|
.map(map_first_amount);
|
||||||
Ok(num) => Amount {
|
|
||||||
operator: Operator::Plus,
|
|
||||||
element: Element::Number(num),
|
|
||||||
},
|
|
||||||
_ => Amount {
|
|
||||||
operator: Operator::Plus,
|
|
||||||
element: Element::Variable(value),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let variable_or_number =
|
// All of this, down to amount_parser, will convert expressions
|
||||||
many1(letter())
|
// after the first into Amounts (wrapped in a ParseResult).
|
||||||
.or(many1(digit()))
|
let plus_or_minus = one_of("+-".chars());
|
||||||
.map(|value: String| match value.parse::<i32>() {
|
let maybe_sign = plus_or_minus.map(map_operator);
|
||||||
Ok(num) => Element::Number(num),
|
|
||||||
_ => Element::Variable(value),
|
|
||||||
});
|
|
||||||
|
|
||||||
let sign_and_word = maybe_sign
|
let variable_or_number = many1(letter()).or(many1(digit())).map(map_element);
|
||||||
|
|
||||||
|
let element_parser = maybe_sign
|
||||||
.skip(spaces().silent())
|
.skip(spaces().silent())
|
||||||
.and(variable_or_number)
|
.and(variable_or_number)
|
||||||
.skip(spaces().silent())
|
.skip(spaces().silent());
|
||||||
.map(|parsed: (Operator, Element)| Amount {
|
|
||||||
operator: parsed.0,
|
|
||||||
element: parsed.1,
|
|
||||||
});
|
|
||||||
|
|
||||||
let rest = many(sign_and_word).map(|expr: Vec<_>| expr);
|
let amount_parser = element_parser.map(map_amount);
|
||||||
|
let remaining_amounts = many(amount_parser).map(|amounts: Vec<ParseResult<Amount>>| amounts);
|
||||||
|
|
||||||
let mut parser = first.and(rest);
|
let mut parser = first_amount.and(remaining_amounts);
|
||||||
|
|
||||||
//Maps the found expression into a Vec of Amount instances,
|
// Collapses first amount + remaining amounts into a single Vec,
|
||||||
//tacking the first one on.
|
// while collecting extraneous input.
|
||||||
type ParsedAmountExpr = (Amount, Vec<Amount>);
|
type ParsedAmountExpr = (ParseResult<Amount>, Vec<ParseResult<Amount>>);
|
||||||
let (results, rest) = parser
|
let (results, rest) = parser
|
||||||
.parse(input)
|
.parse(input)
|
||||||
.map(|mut results: (ParsedAmountExpr, &str)| {
|
.map(|mut results: (ParsedAmountExpr, &str)| {
|
||||||
|
@ -115,7 +171,8 @@ pub fn parse_amounts(input: &str) -> Result<Vec<Amount>, DiceParsingError> {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if rest.len() == 0 {
|
if rest.len() == 0 {
|
||||||
Ok(results)
|
// Any ParseResult errors will short-circuit the collect.
|
||||||
|
results.into_iter().collect()
|
||||||
} else {
|
} else {
|
||||||
Err(DiceParsingError::UnconsumedInput)
|
Err(DiceParsingError::UnconsumedInput)
|
||||||
}
|
}
|
||||||
|
@ -149,6 +206,15 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn parse_single_variable_amount_test() {
|
fn parse_single_variable_amount_test() {
|
||||||
let result = parse_amounts("asdf");
|
let result = parse_amounts("asdf");
|
||||||
|
|
Loading…
Reference in New Issue