#![forbid(unsafe_code)]
use core::convert::Infallible;
use std::error::Error;
use std::fmt::{Display, Debug, Error as FmtError, Formatter};
use std::str::FromStr;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::{Dice, DiceParseError};
lazy_static! {
static ref PLUS_MINUS_RE: Regex = Regex::new("(?P<op>[-\u{2212}+])").expect("Couldn't compile PLUS_MINUS_RE");
static ref TIMES_RE: Regex = Regex::new("(?P<before>[\\d\\s])(?P<op>[*\u{00d7}xX])(?P<after>[\\d\\s])").expect("Couldn't compile TIMES_RE");
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
enum DiceWords {
Dice(Dice),
Bonus(u16),
Plus,
Minus,
Times,
Multiplier(f32),
Comment(String),
Other(String),
Total(i32),
}
impl Display for DiceWords {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
match self {
DiceWords::Dice(d) => write!(fmt, "{}", d),
DiceWords::Bonus(d) => write!(fmt, "{}", d),
DiceWords::Multiplier(d) => write!(fmt, "{}", d),
DiceWords::Plus => write!(fmt, "+"),
DiceWords::Minus => write!(fmt, "\u{2212}"),
DiceWords::Times => write!(fmt, "\u{00d7}"),
DiceWords::Other(s) | DiceWords::Comment(s) => write!(fmt, "|{}|", s),
DiceWords::Total(t) => write!(fmt, "= {}", t),
}
}
}
impl FromStr for DiceWords {
type Err = Infallible;
fn from_str(line: &str) -> Result<Self, Self::Err> {
if let Ok(d) = line.parse() {
Ok(DiceWords::Dice(d))
} else if let Ok(n) = line.parse() {
Ok(DiceWords::Bonus(n))
} else if let Ok(f) = line.parse() {
Ok(DiceWords::Multiplier(f))
} else if line == "+" {
Ok(DiceWords::Plus)
} else if (line == "-") || (line == "\u{2212}") {
Ok(DiceWords::Minus)
} else if (line == "x") || (line == "X") || (line == "*") || (line == "\u{00d7}") {
Ok(DiceWords::Times)
} else if line.starts_with('#') {
Ok(DiceWords::Comment(line.to_string()))
} else {
Ok(DiceWords::Other(line.to_string()))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RollParseError {
DiceError(DiceParseError),
InvalidOrder,
MissingRoll,
Failure(String),
}
impl Display for RollParseError {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
write!(fmt, "{:?}", self)
}
}
impl Error for RollParseError {}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct RollSet {
words: Vec<DiceWords>,
}
impl FromStr for RollSet {
type Err = RollParseError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
let mut last_word = DiceWords::Other("".to_string());
let mut words = Vec::new();
let mut roll_found = false;
let mut total = 0;
let pm_repl = PLUS_MINUS_RE.replace_all(line, " $op ");
let replaced = TIMES_RE.replace_all(&pm_repl, "$before $op $after");
let mut each = replaced.split_whitespace();
while let Some(word) = each.next() {
let parsed = word.parse::<DiceWords>().unwrap();
match (&last_word, &parsed) {
(DiceWords::Dice(_), DiceWords::Dice(_)) | (DiceWords::Dice(_), DiceWords::Multiplier(_)) | (DiceWords::Dice(_), DiceWords::Bonus(_)) |
(DiceWords::Bonus(_), DiceWords::Dice(_)) | (DiceWords::Bonus(_), DiceWords::Bonus(_)) | (DiceWords::Bonus(_), DiceWords::Multiplier(_)) |
(DiceWords::Times, DiceWords::Dice(_)) |
(DiceWords::Plus, DiceWords::Plus) | (DiceWords::Plus, DiceWords::Minus) | (DiceWords::Plus, DiceWords::Times) | (DiceWords::Plus, DiceWords::Multiplier(_)) | (DiceWords::Plus, DiceWords::Comment(_)) | (DiceWords::Plus, DiceWords::Other(_)) |
(DiceWords::Minus, DiceWords::Plus) | (DiceWords::Minus, DiceWords::Minus) | (DiceWords::Minus, DiceWords::Times) | (DiceWords::Minus, DiceWords::Multiplier(_)) | (DiceWords::Minus, DiceWords::Comment(_)) | (DiceWords::Minus, DiceWords::Other(_)) |
(DiceWords::Times, DiceWords::Plus) | (DiceWords::Times, DiceWords::Minus) | (DiceWords::Times, DiceWords::Times) | (DiceWords::Times, DiceWords::Comment(_)) | (DiceWords::Times, DiceWords::Other(_)) |
(DiceWords::Multiplier(_), DiceWords::Dice(_)) | (DiceWords::Multiplier(_), DiceWords::Bonus(_)) | (DiceWords::Multiplier(_), DiceWords::Multiplier(_)) |
(DiceWords::Other(_), DiceWords::Bonus(_)) | (DiceWords::Other(_), DiceWords::Multiplier(_))
=> { return Err(RollParseError::InvalidOrder);
},
(DiceWords::Dice(_), DiceWords::Plus) | (DiceWords::Dice(_), DiceWords::Minus) | (DiceWords::Dice(_), DiceWords::Times) | (DiceWords::Dice(_), DiceWords::Other(_)) |
(DiceWords::Bonus(_), DiceWords::Plus) | (DiceWords::Bonus(_), DiceWords::Minus) | (DiceWords::Bonus(_), DiceWords::Times) | (DiceWords::Bonus(_), DiceWords::Other(_)) |
(DiceWords::Multiplier(_), DiceWords::Plus) | (DiceWords::Multiplier(_), DiceWords::Minus) | (DiceWords::Multiplier(_), DiceWords::Times) | (DiceWords::Multiplier(_), DiceWords::Other(_)) |
(DiceWords::Other(_), DiceWords::Plus) | (DiceWords::Other(_), DiceWords::Minus) | (DiceWords::Other(_), DiceWords::Times) => {
words.push(parsed.clone());
last_word = parsed;
},
(DiceWords::Plus, DiceWords::Bonus(b)) => { total += *b as i32;
words.push(parsed.clone());
last_word = parsed;
},
(DiceWords::Minus, DiceWords::Bonus(b)) => { total -= *b as i32;
words.push(parsed.clone());
last_word = parsed;
},
(DiceWords::Times, DiceWords::Bonus(b)) => {
total *= *b as i32;
last_word = DiceWords::Multiplier(*b as f32);
words.push(last_word.clone());
}
(DiceWords::Times, DiceWords::Multiplier(b)) => {
total = (total as f32 * b) as i32;
words.push(parsed.clone());
last_word = parsed;
}
(DiceWords::Dice(_), DiceWords::Comment(s)) | (DiceWords::Bonus(_), DiceWords::Comment(s)) | (DiceWords::Other(_), DiceWords::Comment(s)) | (DiceWords::Multiplier(_), DiceWords::Comment(s)) => {
words.push(DiceWords::Total(total));
last_word = DiceWords::Comment(each.fold(s.to_string(), |acc, x| acc + " " + x));
words.push(last_word.clone());
break;
},
(DiceWords::Other(_), DiceWords::Dice(d)) | (DiceWords::Plus, DiceWords::Dice(d)) | (DiceWords::Minus, DiceWords::Dice(d)) => { last_word = parsed.clone();
words.push(parsed.clone());
roll_found = true;
total += d.total();
},
(_, DiceWords::Total(_)) |
(DiceWords::Total(_), _) |
(DiceWords::Comment(_), _) => {
return Err(RollParseError::Failure(line.to_string()));
},
(DiceWords::Other(t), DiceWords::Other(s)) => { words.pop();
last_word = DiceWords::Other(format!("{} {}", t, s));
words.push(last_word.clone());
},
}
}
if !roll_found {
Err(RollParseError::MissingRoll)
} else if matches!(last_word, DiceWords::Times | DiceWords::Plus | DiceWords::Minus) {
Err(RollParseError::InvalidOrder)
} else {
if !matches!(last_word, DiceWords::Comment(_)) {
words.push(DiceWords::Total(total));
}
Ok(RollSet { words })
}
}
}
impl Display for RollSet {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
write!(fmt, "{}", self.words.iter().map(|w| format!("{}", w)).collect::<Vec<String>>().join(" "))
}
}
#[cfg(test)]
mod test {
use super::{Dice, DiceWords, RollParseError, RollSet};
use crate::expect_dice_similar;
macro_rules! expect_rollset_similar {
($text: literal, $expect: expr) => {
for (r, w) in $text.parse::<RollSet>().unwrap().words.iter().zip($expect) {
match (r, w) {
(DiceWords::Dice(parsed), DiceWords::Dice(provided)) => {
expect_dice_similar!(parsed, provided);
},
(DiceWords::Bonus(parsed), DiceWords::Bonus(provided)) => assert_eq!(parsed, provided),
(DiceWords::Multiplier(parsed), DiceWords::Multiplier(provided)) => assert_eq!(parsed, provided),
(DiceWords::Comment(parsed), DiceWords::Comment(provided)) => assert_eq!(parsed, provided),
(x, y) => assert_eq!(x, y),
}
}
}
}
#[test]
fn four() {
assert_eq!("4".parse::<RollSet>(), Err(RollParseError::InvalidOrder));
assert_eq!("4+1d4".parse::<RollSet>(), Err(RollParseError::InvalidOrder));
}
#[test]
fn exploding_1d20plus4() {
expect_rollset_similar!("1d20+4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Plus, DiceWords::Bonus(4) ]);
}
#[test]
fn r_1d6plus3() {
expect_rollset_similar!("1d6+3", &[ DiceWords::Dice(Dice::new(1, 6).unwrap()), DiceWords::Plus, DiceWords::Bonus(3) ]);
}
#[test]
fn caps_1d20plus4() {
expect_rollset_similar!("1D20+4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Plus, DiceWords::Bonus(4) ]);
}
#[test]
fn caps_1d20minus4() {
expect_rollset_similar!("1D20 - 4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Minus, DiceWords::Bonus(4) ]);
}
#[test]
fn unicrud_minus() {
expect_rollset_similar!("1d20 − 4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Minus, DiceWords::Bonus(4) ]);
}
#[test]
fn comment_1d20() {
expect_rollset_similar!("1d20 for a test", &[
DiceWords::Dice(Dice::new(1, 20).unwrap()),
DiceWords::Other("for a test".to_string()),
]);
}
#[test]
fn t_mult() {
expect_rollset_similar!("1d20 * 1.5", &[
DiceWords::Dice(Dice::new(1, 20).unwrap()),
DiceWords::Times,
DiceWords::Multiplier(1.5),
]);
}
#[test]
fn spacesplus() {
expect_rollset_similar!("1d6+4+11d99!+3dF+ 4 +3 + 2 + 1d5", &[
DiceWords::Dice(Dice::new(1, 6).unwrap()),
DiceWords::Plus,
DiceWords::Bonus(4),
DiceWords::Plus,
DiceWords::Dice(Dice::new_extended(11, 99, 0, 99, 1).unwrap()),
DiceWords::Plus,
DiceWords::Dice(Dice::new(3, 0).unwrap()),
DiceWords::Plus,
DiceWords::Bonus(4),
DiceWords::Plus,
DiceWords::Bonus(3),
DiceWords::Plus,
DiceWords::Bonus(2),
DiceWords::Plus,
DiceWords::Dice(Dice::new(1, 5).unwrap()),
]);
}
#[test]
fn spacesminus() {
expect_rollset_similar!("1d6-4+11d99-3dF+ 4 +3 - 2 - 1d5", &[
DiceWords::Dice(Dice::new(1, 6).unwrap()),
DiceWords::Minus,
DiceWords::Bonus(4),
DiceWords::Plus,
DiceWords::Dice(Dice::new(11, 99).unwrap()),
DiceWords::Minus,
DiceWords::Dice(Dice::new(3, 0).unwrap()),
DiceWords::Plus,
DiceWords::Bonus(4),
DiceWords::Plus,
DiceWords::Bonus(3),
DiceWords::Minus,
DiceWords::Bonus(2),
DiceWords::Minus,
DiceWords::Dice(Dice::new(1, 5).unwrap()),
]);
}
#[cfg(test)]
macro_rules! unwrap_dice {
($d: expr) => {
if let DiceWords::Dice(roll) = $d {
roll
} else {
panic!("not a roll: {:?}", $d);
}
}
}
#[test]
fn coinflip() {
let rs = "1d2 # 1 = foo, 2=bar".parse::<RollSet>().unwrap();
expect_dice_similar!(unwrap_dice!(&rs.words[0]), Dice::new(1, 2).unwrap());
assert_eq!(rs.words[2], DiceWords::Comment("# 1 = foo, 2=bar".to_string()));
}
#[test]
fn compact() {
let rs = "2d6+4 for a test".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 5);
let mut iter = rs.words.iter();
let roll = unwrap_dice!(iter.next().unwrap());
assert_eq!(roll.rolls().len(), 2);
assert_eq!(roll.sides(), 6);
assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
assert_eq!(iter.next().unwrap(), &DiceWords::Other("for a test".to_string()));
assert!(matches!(iter.next().unwrap(), &DiceWords::Total(_)));
}
#[test]
fn whitespace_positive() {
let rs = "2d6 + 4 for a test".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 5);
let mut iter = rs.words.iter();
let roll = unwrap_dice!(iter.next().unwrap());
assert_eq!(roll.rolls().len(), 2);
assert_eq!(roll.sides(), 6);
assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
assert_eq!(iter.next().unwrap(), &DiceWords::Other("for a test".to_string()));
assert!(matches!(iter.next().unwrap(), DiceWords::Total(_)));
}
#[test]
fn whitespace_negative() {
let rs = "2d6 - 4 for a test".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 5);
let mut iter = rs.words.iter();
let roll = unwrap_dice!(iter.next().unwrap());
assert_eq!(roll.rolls().len(), 2);
assert_eq!(roll.sides(), 6);
assert_eq!(iter.next().unwrap(), &DiceWords::Minus);
assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
assert_eq!(iter.next().unwrap(), &DiceWords::Other("for a test".to_string()));
assert!(matches!(iter.next().unwrap(), DiceWords::Total(_)));
}
#[test]
fn simple() {
let rs = "d20".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 2);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.rolls().len(), 1);
assert_eq!(roll.sides(), 20);
assert!(matches!(rs.words[1], DiceWords::Total(_)));
}
#[test]
fn no_bonus_suffix() {
let rs = "3d6 Int".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 3);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.rolls().len(), 3);
assert_eq!(roll.sides(), 6);
assert_eq!(rs.words[1], DiceWords::Other("Int".to_string()));
assert!(matches!(rs.words[2], DiceWords::Total(_)));
}
#[test]
fn only_bonus() {
let rs = "d8+2".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.rolls().len(), 1);
assert_eq!(roll.sides(), 8);
assert_eq!(rs.words[1], DiceWords::Plus);
assert_eq!(rs.words[2], DiceWords::Bonus(2));
assert!(matches!(rs.words[3], DiceWords::Total(_)));
}
#[test]
fn regular_minus() {
let rs = "d8-2".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.rolls().len(), 1);
assert_eq!(roll.sides(), 8);
assert_eq!(rs.words[1], DiceWords::Minus);
assert_eq!(rs.words[2], DiceWords::Bonus(2));
assert!(matches!(rs.words[3], DiceWords::Total(_)));
}
#[test]
fn unicrud_minus_explicit() {
let rs = "d8\u{2212}2".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.rolls().len(), 1);
assert_eq!(roll.sides(), 8);
assert_eq!(rs.words[1], DiceWords::Minus);
assert_eq!(rs.words[2], DiceWords::Bonus(2));
assert!(matches!(rs.words[3], DiceWords::Total(_)));
}
#[test]
fn test_only_penalty() {
let rs = "3d8-2".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.rolls().len(), 3);
assert_eq!(roll.sides(), 8);
assert_eq!(rs.words[1], DiceWords::Minus);
assert_eq!(rs.words[2], DiceWords::Bonus(2));
}
#[test]
fn bonus_and_roll() {
let rs = "d8+2 slashing".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 5);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.sides(), 8);
assert_eq!(roll.rolls().len(), 1);
assert_eq!(rs.words[1], DiceWords::Plus);
assert_eq!(rs.words[2], DiceWords::Bonus(2));
assert_eq!(rs.words[3], DiceWords::Other("slashing".to_string()));
}
#[test]
fn two_dice_sizes() {
let rs = "2d5 + 3d9".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let mut iter = rs.words.iter();
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.rolls().len(), 2);
assert_eq!(d.sides(), 5);
assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.rolls().len(), 3);
assert_eq!(d.sides(), 9);
}
#[test]
fn two_rolls_text() {
let rs = "2d5 + 3d9 for foo, 4d6 for bar".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 7);
let mut iter = rs.words.iter();
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.rolls().len(), 2);
assert_eq!(d.sides(), 5);
assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.rolls().len(), 3);
assert_eq!(d.sides(), 9);
assert_eq!(iter.next().unwrap(), &DiceWords::Other("for foo,".to_string()));
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.rolls().len(), 4);
assert_eq!(d.sides(), 6);
assert_eq!(iter.next().unwrap(), &DiceWords::Other("for bar".to_string()));
}
#[test]
fn roll_no_d0() {
let rsr = "3d0".parse::<RollSet>();
assert_eq!(rsr, Err(RollParseError::MissingRoll));
let rsr = "0d5".parse::<RollSet>();
assert_eq!(rsr, Err(RollParseError::MissingRoll));
}
#[test]
fn roll_d1() {
let rs = "3d1".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 2);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.sides(), 1);
assert_eq!(roll.total(), 3);
assert_eq!(rs.words[1], DiceWords::Total(3));
}
#[test]
fn roll_no_dice() {
let rsr = "4".parse::<RollSet>();
assert_eq!(rsr, Err(RollParseError::InvalidOrder));
}
#[test]
fn roll_no_dice_plus() {
let rsr = "+4".parse::<RollSet>();
assert_eq!(rsr, Err(RollParseError::MissingRoll));
}
#[test]
fn roll_simple_math() {
let rsr = "2 +4-1".parse::<RollSet>();
assert_eq!(rsr, Err(RollParseError::InvalidOrder));
}
#[test]
fn roll_bad_dice() {
assert_eq!("4d-4".parse::<RollSet>(), Err(RollParseError::MissingRoll));
assert_eq!("ddd".parse::<RollSet>(), Err(RollParseError::MissingRoll));
}
#[test]
fn leading_space() {
let rs = " 4d20 * 1.5".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let d = unwrap_dice!(&rs.words[0]);
assert_eq!(d.sides(), 20);
assert_eq!(d.rolls().len(), 4);
assert_eq!(rs.words[1], DiceWords::Times);
assert_eq!(rs.words[2], DiceWords::Multiplier(1.5));
assert!(matches!(rs.words[3], DiceWords::Total(_)));
}
#[test]
fn leading_tab() {
let rs = "\u{0009}4d20*1.5".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let d = unwrap_dice!(&rs.words[0]);
assert_eq!(d.sides(), 20);
assert_eq!(d.rolls().len(), 4);
assert_eq!(rs.words[1], DiceWords::Times);
assert_eq!(rs.words[2], DiceWords::Multiplier(1.5));
assert!(matches!(rs.words[3], DiceWords::Total(_)));
}
#[test]
fn add_constant() {
let rs = "1d1+4".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 4);
let roll = unwrap_dice!(&rs.words[0]);
assert_eq!(roll.sides(), 1);
assert_eq!(roll.total(), 1);
assert_eq!(rs.words[1..], [ DiceWords::Plus, DiceWords::Bonus(4), DiceWords::Total(5) ]);
}
#[test]
fn add_to_many() {
let rs = "d20 + d6 + 4 to hit".parse::<RollSet>().unwrap();
assert_eq!(rs.words.len(), 7);
let mut iter = rs.words.iter();
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.sides(), 20);
assert_eq!(d.rolls().len(), 1);
assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
let d = unwrap_dice!(iter.next().unwrap());
assert_eq!(d.sides(), 6);
assert_eq!(d.rolls().len(), 1);
assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
assert_eq!(iter.next().unwrap(), &DiceWords::Other("to hit".to_string()));
}
#[test]
fn add_constant_str() {
let rs = "d1+4".parse::<RollSet>().unwrap();
assert_eq!("1d1 [1] + 4 = 5".to_string(), format!("{}", rs));
}
#[test]
fn many_rollv_str() {
let rs = "d1 + 2d1 - 4 to hit".parse::<RollSet>().unwrap();
assert_eq!("1d1 [1] + 2d1 [2] \u{2212} 4 |to hit| = -1", format!("{}", rs));
}
#[test]
fn multiply_even() {
let rs = "10d1*1.5".parse::<RollSet>().unwrap();
assert_eq!("10d1 [10] \u{00d7} 1.5 = 15", format!("{}", rs));
}
#[test]
fn multiply_odd() {
let rs = "11d1*1.5".parse::<RollSet>().unwrap();
assert_eq!("11d1 [11] \u{00d7} 1.5 = 16", format!("{}", rs));
}
#[test]
fn multiply_ooo() {
let rs = "1d1*1.5 + 1d1 * 1.5".parse::<RollSet>().unwrap();
assert_eq!("1d1 [1] \u{00d7} 1.5 + 1d1 [1] \u{00d7} 1.5 = 3", format!("{}", rs));
}
#[test]
fn more_like_chat() {
let line = "/roll d1 + 2d1 \u{2212} 4 to hit; 1d1 for damage";
let chatter: Vec<&str> = line.splitn(2, char::is_whitespace).collect();
assert_eq!(chatter.len(), 2);
assert!(chatter[0].starts_with("/r"));
let rolls: Result<String, RollParseError> = chatter[1].split(';').try_fold("".to_string(), |acc, x| {
let rs = x.parse::<RollSet>()?;
if acc.is_empty() {
Ok(format!("rolls {}", rs))
} else {
Ok(acc + &format!("; {}", rs))
}
});
assert!(rolls.is_ok());
assert_eq!(rolls.unwrap(), "rolls 1d1 [1] + 2d1 [2] \u{2212} 4 |to hit| = -1; 1d1 [1] |for damage| = 1");
}
#[test]
fn dice_from_str() {
assert_eq!(Ok(Dice::new(1, 1).unwrap()), "1d1".parse());
}
#[test]
fn big_dice_bad() {
assert_eq!("d10000".parse::<RollSet>(), Err(RollParseError::MissingRoll));
assert_eq!("1d10000".parse::<RollSet>(), Err(RollParseError::MissingRoll));
assert_eq!("10001d10".parse::<RollSet>(), Err(RollParseError::MissingRoll));
}
#[test]
fn comment() {
assert_eq!(format!("{}", "4d1 # foo".parse::<RollSet>().unwrap()), "4d1 [4] = 4 |# foo|");
}
#[test]
fn ends_with_operator() {
assert!("1d6+".parse::<RollSet>().is_err());
assert!("1d6x".parse::<RollSet>().is_err());
assert!("1d6-".parse::<RollSet>().is_err());
}
#[test]
fn multiplier_other_ok() {
assert!("1d20+4+1d6-3*1.5 for ham + 4 \u{2212} 1d6 foo".parse::<RollSet>().is_ok());
}
#[test]
fn times() {
assert!("1d6x4".parse::<RollSet>().is_ok());
assert!("1d6X4".parse::<RollSet>().is_ok());
assert!("1d6*4".parse::<RollSet>().is_ok());
assert!("1d6\u{00d7}4".parse::<RollSet>().is_ok());
assert!("1d6 complex".parse::<RollSet>().is_ok());
assert!("1d6 x".parse::<RollSet>().is_err());
assert!("1d6x".parse::<RollSet>().is_err());
}
}