use crate::die::Sides;
use std::fmt::{self, Display, Formatter};
#[derive(Debug)]
pub struct Tokenizer {
input: String,
}
impl Tokenizer {
pub fn new(input: impl Into<String>) -> Self {
Self {
input: input.into(),
}
}
pub fn tokens(&self) -> TokenizerIterator {
TokenizerIterator::new(&self.input)
}
}
#[derive(Debug)]
pub struct TokenizerIterator {
input: Vec<char>,
index: usize,
}
impl TokenizerIterator {
fn new(input: impl AsRef<str>) -> Self {
Self {
input: input.as_ref().chars().collect(),
index: 0,
}
}
fn consume(&mut self) {
self.index += 1;
}
fn peek(&mut self) -> Option<&char> {
self.input.get(self.index)
}
fn tokenize_int(&mut self) -> Token {
let mut buf = String::new();
while let Some(c) = self.peek() {
if !c.is_ascii_digit() {
break;
}
buf.push(*c);
self.consume();
}
buf.parse::<IntVal>()
.map(Token::Int)
.unwrap_or_else(|_| Token::String(buf))
}
fn tokenize_string(&mut self) -> Token {
let mut buf = String::new();
while let Some(c) = self.peek() {
if *c == ' ' {
break;
}
buf.push(*c);
self.consume();
}
Token::String(buf)
}
fn tokenize_die(&mut self) -> Token {
let mut buf = String::new();
while let Some(c) = self.peek() {
if !c.is_ascii_digit() {
break;
}
buf.push(*c);
self.consume();
}
buf.parse::<DieVal>()
.map(Token::Die)
.unwrap_or_else(|_| Token::String(format!("d{buf}")))
}
}
impl Iterator for TokenizerIterator {
type Item = Token;
fn next(&mut self) -> Option<Self::Item> {
while let Some(' ') = self.peek() {
self.consume();
}
if let Some(c) = self.peek() {
if *c == 'd' || *c == 'D' {
self.consume();
Some(self.tokenize_die())
} else if *c == '+' {
self.consume();
Some(Token::Plus)
} else if *c == '-' {
self.consume();
Some(Token::Minus)
} else if c.is_ascii_digit() {
Some(self.tokenize_int())
} else {
Some(self.tokenize_string())
}
} else {
None
}
}
}
pub type IntVal = i64;
pub type DieVal = Sides;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Token {
Die(DieVal),
Int(IntVal),
Plus,
Minus,
String(String),
End,
}
impl Token {
pub fn is_die(&self) -> bool {
matches!(self, Self::Die(_))
}
pub fn is_int(&self) -> bool {
matches!(self, Self::Int(_))
}
pub fn is_plus(&self) -> bool {
matches!(self, Self::Plus)
}
pub fn is_minus(&self) -> bool {
matches!(self, Self::Minus)
}
pub fn is_end(&self) -> bool {
matches!(self, Self::End)
}
pub fn is_empty(&self) -> bool {
match self {
Token::String(s) => s.is_empty(),
_ => false,
}
}
}
impl Display for Token {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Die(sides) => write!(f, "d{sides}"),
Self::Int(n) => write!(f, "{n}"),
Self::Plus => write!(f, "+"),
Self::Minus => write!(f, "-"),
Self::String(s) => write!(f, "{s}"),
Self::End => write!(f, "<END>"),
}
}
}
#[cfg(test)]
mod tests {
mod tokenizer {
use super::super::{Token, Tokenizer};
macro_rules! str_token {
($str:literal) => {
Token::String(String::from($str))
};
}
fn tokens(expr: impl Into<String>) -> Vec<Token> {
Tokenizer::new(expr).tokens().collect()
}
#[test]
fn tokenizes_an_integer() {
assert_eq!(vec![Token::Int(4)], tokens("4"));
}
#[test]
fn tokenizes_a_negative_integer() {
assert_eq!(vec![Token::Minus, Token::Int(4)], tokens("-4"));
}
#[test]
fn tokenizes_a_die_with_no_quantity() {
assert_eq!(vec![Token::Die(20)], tokens("d20"));
}
#[test]
fn tokenizes_a_die_with_a_quantity() {
assert_eq!(vec![Token::Int(2), Token::Die(20)], tokens("2d20"),);
}
#[test]
fn tokenizes_a_die_with_a_modifier() {
assert_eq!(
vec![Token::Die(20), Token::Plus, Token::Int(4)],
tokens("d20 + 4"),
)
}
#[test]
fn tokenizes_a_die_with_a_modifier_with_no_spaces() {
assert_eq!(
vec![Token::Die(20), Token::Plus, Token::Int(4)],
tokens("d20+4"),
)
}
#[test]
fn tokenizes_dice_with_a_modifier() {
assert_eq!(
vec![Token::Int(2), Token::Die(20), Token::Plus, Token::Int(4)],
tokens("2d20 + 4"),
)
}
#[test]
fn tokenizes_dice_with_a_modifier_with_no_spaces() {
assert_eq!(
vec![Token::Int(2), Token::Die(20), Token::Plus, Token::Int(4)],
tokens("2d20+4"),
)
}
#[test]
fn tokenizes_dice_with_a_negative_modifier() {
assert_eq!(
vec![Token::Int(2), Token::Die(20), Token::Minus, Token::Int(4)],
tokens("2d20 - 4"),
)
}
#[test]
fn tokenizes_dice_with_a_negative_modifier_with_no_spaces() {
assert_eq!(
vec![Token::Int(2), Token::Die(20), Token::Minus, Token::Int(4)],
tokens("2d20-4"),
)
}
#[test]
fn tokenizes_a_random_string() {
assert_eq!(vec![str_token!("aaabbbcccddd")], tokens("aaabbbcccddd"));
}
#[test]
fn tokenizes_multiple_random_strings() {
assert_eq!(
vec![str_token!("aaabbbcccddd"), str_token!("xxxyyyzzz")],
tokens("aaabbbcccddd xxxyyyzzz")
);
}
#[test]
fn tokenizes_a_random_string_on_the_end() {
assert_eq!(
vec![
Token::Int(2),
Token::Die(20),
Token::Plus,
str_token!("abcd")
],
tokens("2d20 +abcd")
);
}
#[test]
fn tokenizes_a_random_string_on_at_the_start() {
assert_eq!(
vec![
str_token!("abcd"),
Token::Plus,
Token::Int(2),
Token::Die(20),
],
tokens("abcd +2d20")
);
}
#[test]
fn tokenizes_an_invalid_die_representation() {
assert_eq!(vec![Token::Die(20), str_token!("xxx")], tokens("d20xxx"))
}
#[test]
fn tokenizes_a_lone_d() {
assert_eq!(
vec![Token::Int(4), Token::Plus, str_token!("d")],
tokens("4 + d")
);
}
#[test]
fn tokenizes_a_lone_d_followed_by_a_space() {
assert_eq!(
vec![Token::Int(4), Token::Plus, str_token!("d")],
tokens("4 + d ")
);
}
#[test]
fn tokenizes_a_d_preceded_by_a_string() {
assert_eq!(
vec![Token::Int(4), Token::Plus, str_token!("ad")],
tokens("4 + ad")
);
}
#[test]
fn tokenizes_a_die_separated_by_a_space() {
assert_eq!(vec![str_token!("d"), Token::Int(20)], tokens("d 20"));
}
#[test]
fn tokenizes_dice_separated_by_a_space() {
assert_eq!(vec![Token::Int(4), Token::Die(20)], tokens("4 d20"));
}
#[test]
fn tokenizes_a_decimal_die() {
assert_eq!(vec![Token::Die(2), str_token!(".5")], tokens("d2.5"))
}
#[test]
fn tokenizes_an_empty_string() {
assert_eq!(Vec::<Token>::new(), tokens(""));
}
}
}