use crate::prelude::*;
use crate::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum BinaryOp {
Plus,
Minus,
Times,
Div,
Rem,
Pow,
LT,
GT,
EQ,
NE,
LE,
GE,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum UnaryOp {
Plus,
Minus,
Fact,
}
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum Token {
Binary(BinaryOp),
Unary(UnaryOp),
LParen,
RParen,
Comma,
Number(f64),
Var(String),
Func(String, Option<usize>),
Dice(usize, usize),
}
impl Token {
fn make_func(&mut self) -> bool {
if let Self::Var(s) = self {
*self = Self::Func((*s).clone(), None);
true
} else {
false
}
}
}
#[expect(clippy::collapsible_match, reason = "I think clippy is wrong")]
fn is_d(x: &str) -> bool {
let mut iter = x.chars();
match iter.next() {
None => return false,
Some(ch) => {
if ch == 'd' {
for c in iter {
if !c.is_ascii_digit() {
return false;
}
}
} else {
return false;
}
}
}
true
}
const fn can_binary(token: &Token) -> bool {
use Token::{Binary, Comma, Dice, Func, LParen, Number, RParen, Unary, Var};
match token {
Binary(_) => false,
Unary(_) => false,
LParen => false,
RParen => true,
Comma => false,
Number(_) => true,
Dice(_, _) => true,
Var(_) => true,
Func(_, _) => true,
}
}
fn get_alpha_prefix(data: &str) -> &str {
let end = data.chars().position(|b| !b.is_ascii_alphanumeric()).unwrap_or(data.len());
&data[..end]
}
fn get_based_number(data: &str, base: u32) -> Result<u64> {
let num = u64::from_str_radix(data, base)?;
Ok(num)
}
fn get_special(mut input: &str) -> Result<Option<(&str, u64)>> {
if input.len() < 2 {
Ok(None)
} else {
let ch = input.take_first();
let tok = get_alpha_prefix(input);
match ch {
'b' | 'B' => Ok(Some((&input[tok.len()..], get_based_number(tok, 2)?))),
'o' | 'O' => Ok(Some((&input[tok.len()..], get_based_number(tok, 8)?))),
'x' | 'X' => Ok(Some((&input[tok.len()..], get_based_number(tok, 16)?))),
'r' | 'R' => Ok(Some((&input[tok.len()..], roman::read_roman(tok.as_bytes())?))),
_ => Ok(None),
}
}
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn tokenize<S: AsRef<str>>(orig: S) -> Result<Vec<Token>> {
let orig = orig.as_ref();
let mut input = orig;
let mut res: Vec<Token> = vec![];
while !input.is_empty() {
let prev = input.len();
let ch = input.take_first();
let next = if input.is_empty() { 0 as char } else { input.first() };
match ch {
'(' => {
if res.is_empty() {
res.push(Token::LParen);
} else {
let last = res.len() - 1;
if !res[last].make_func() {
res.push(Token::LParen);
}
}
}
')' => res.push(Token::RParen),
',' => res.push(Token::Comma),
'^' => res.push(Token::Binary(BinaryOp::Pow)),
'%' => res.push(Token::Binary(BinaryOp::Rem)),
'/' => res.push(Token::Binary(BinaryOp::Div)),
'*' => res.push(Token::Binary(BinaryOp::Times)),
'+' => {
if res.is_empty() || !can_binary(&res[res.len() - 1]) {
res.push(Token::Unary(UnaryOp::Plus));
} else {
res.push(Token::Binary(BinaryOp::Plus));
}
}
'-' => {
if res.is_empty() || !can_binary(&res[res.len() - 1]) {
res.push(Token::Unary(UnaryOp::Minus));
} else {
res.push(Token::Binary(BinaryOp::Minus));
}
}
'!' => {
if next == '=' {
res.push(Token::Binary(BinaryOp::NE));
input.skip_first();
} else {
res.push(Token::Unary(UnaryOp::Fact));
}
}
'<' => {
if next == '=' {
res.push(Token::Binary(BinaryOp::LE));
input.skip_first();
} else {
res.push(Token::Binary(BinaryOp::LT));
}
}
'>' => {
if next == '=' {
res.push(Token::Binary(BinaryOp::GE));
input.skip_first();
} else {
res.push(Token::Binary(BinaryOp::GT));
}
}
'=' => {
if next == '=' {
res.push(Token::Binary(BinaryOp::EQ));
input.skip_first();
} else {
return err!("Equal can't be followed by {} in '{}'", next, orig);
}
}
_ => {
if ch.is_alphabetic() {
let mut var = String::new();
var.push(ch);
while !input.is_empty()
&& (input.first().is_alphanumeric() || input.first() == '_')
{
var.push(input.take_first());
}
if is_d(&var)
&& !res.is_empty()
&& let Token::Number(n) = res[res.len() - 1]
{
let (x, _y) = var[1..].to_f64();
let pos = res.len() - 1;
res[pos] = Token::Dice(n as usize, x as usize);
} else {
res.push(Token::Var(var));
}
} else if ch.is_ascii_alphanumeric() {
debug_assert!(
ch.is_ascii_digit(),
"is_ascii_alphanumeric but not is_ascii_digit"
);
if ch == '0'
&& let Some((data, val)) = get_special(input)?
{
res.push(Token::Number(val as f64));
input = data;
} else {
let (x, y) = orig[orig.len() - prev..].to_f64();
input = y;
res.push(Token::Number(x));
}
} else if ch.is_whitespace() {
} else {
return err!("Unrecognized character {ch} in {orig}");
}
}
}
}
Ok(res)
}