use crate::meta::graph_with_tags_b;
use super::*;
pub use test_case::test_case;
pub mod meta;
pub fn has_tag<'a>(tag: &'a str) -> impl Sequence<Vec<&str>> {
FirstTokenSeq::new(move |tok: &Token<'_, Vec<&str>>| tok.data.contains(&tag))
}
pub fn calc_tokens<'a>(text: &'a str) -> Vec<Token<Vec<&'a str>>> {
let mut tox = Token::token_vec_from_str(text, |_, _| vec![]);
let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
replace_all_matches_once(
&MultipleSeq::new(vec![
Box::new(ChooseSeq::from_str(alphabet)),
Box::new(RepeatedSeq::new(Box::new(ChooseSeq::from_str(alphabet)))),
]),
&ShallowTransform { data: vec!["word"] },
&mut tox,
);
replace_all_matches_once(
&int_seq(),
&ShallowTransform {
data: vec!["int", "positive", "number", "expr"],
},
&mut tox,
);
let decimal_seq = MultipleSeq::new(vec![
Box::new(has_tag("int")),
Box::new(RawSeq::new(".")),
Box::new(has_tag("int")),
Box::new(RepeatedSeq::new(Box::new(has_tag("int")))),
]);
replace_all_matches_once(
&decimal_seq,
&ShallowTransform {
data: vec!["decimal", "positive", "number", "expr"],
},
&mut tox,
);
let negative_seq = MultipleSeq::new(vec![
Box::new(RawSeq::new("-")),
Box::new(has_tag("positive")),
]);
replace_all_matches_once(
&negative_seq,
&DeepTransform {
data: vec!["negative", "number", "expr"],
},
&mut tox,
);
let whitespace_seq = FirstTokenSeq::new(|tok: &Token<'_, Vec<&str>>| {
tok.content()
.chars()
.nth(0)
.is_some_and(|c| c.is_whitespace())
});
let paren_seq = MultipleSeq::new(vec![
Box::new(RawSeq::new("(")),
Box::new(has_tag("expr")),
Box::new(RawSeq::new(")")),
]);
let call_seq = MultipleSeq::new(vec![Box::new(has_tag("word")), Box::new(has_tag("parens"))]);
let md_seq = MultipleSeq::new(vec![
Box::new(has_tag("expr")),
Box::new(ChooseSeq::from_str("*/")),
Box::new(has_tag("expr")),
]);
let as_seq = MultipleSeq::new(vec![
Box::new(has_tag("expr")),
Box::new(ChooseSeq::from_str("+-")),
Box::new(has_tag("expr")),
]);
replace_all_matches(&whitespace_seq, &RemoveTransform {}, &mut tox);
repeat_until_no_change(
&vec![
&|c| {
replace_first_match(
&paren_seq,
&DeepTransform {
data: vec!["parens", "expr"],
},
c,
)
},
&|c| {
replace_first_match(
&call_seq,
&DeepTransform {
data: vec!["call", "expr"],
},
c,
)
},
&|c| {
replace_first_match(
&md_seq,
&DeepTransform {
data: vec!["oper", "expr"],
},
c,
)
},
&|c| {
replace_first_match(
&as_seq,
&DeepTransform {
data: vec!["oper", "expr"],
},
c,
)
},
],
&mut tox,
);
tox
}
pub fn eval(token: &Token<'_, Vec<&str>>) -> Option<f64> {
if !token.data.contains(&"expr") {
None
} else if token.data.contains(&"parens") {
if let TokenType::Branch(children) = &token.t_type {
eval(children.get(1)?)
} else {
None
}
} else if token.data.contains(&"number") {
Some(
token
.content()
.parse()
.expect("f64 was recognized as correct but didn't parse in Rust"),
)
} else if token.data.contains(&"oper") {
if let TokenType::Branch(children) = &token.t_type {
match children.get(1)?.content() {
"+" => Some(eval(children.get(0)?)? + eval(children.get(2)?)?),
"-" => Some(eval(children.get(0)?)? - eval(children.get(2)?)?),
"*" => Some(eval(children.get(0)?)? * eval(children.get(2)?)?),
"/" => Some(eval(children.get(0)?)? / eval(children.get(2)?)?),
_ => None,
}
} else {
None
}
} else if token.data.contains(&"call") {
if let TokenType::Branch(children) = &token.t_type {
match children.first()?.content() {
"sqrt" => Some(eval(children.get(1)?)?.sqrt()),
"abs" => Some(eval(children.get(1)?)?.abs()),
"ln" => Some(eval(children.get(1)?)?.ln()),
_ => None,
}
} else {
None
}
} else {
None
}
}
pub fn eval_first(tokens: &Vec<Token<'_, Vec<&str>>>) -> Option<f64> {
graph_with_tags_b(tokens);
if tokens.len() < 2 {
eval(tokens.first()?)
} else {
None
}
}
pub fn eval_text(text: &str) -> Option<f64> {
eval_first(&calc_tokens(text))
}
#[test_case("1", Some(1.0); "one-digit number")]
#[test_case("a", None; "basic letter fail")]
#[test_case("", None; "no text")]
#[test_case(" 101", Some(101.0); "leading space")]
#[test_case("0101", None; "leading zero")]
#[test_case("123", Some(123.0); "multiple-digit number")]
#[test_case("123.0", Some(123.0); "multiple-digit number with decimal")]
#[test_case("123.4567", Some(123.4567); "long decimal")]
#[test_case("123.40070", Some(123.4007); "even longer decimal")]
#[test_case("0", Some(0.0); "zero")]
#[test_case("-1", Some(-1.0); "negative integer")]
#[test_case("-123.0", Some(-123.0); "negative decimal")]
#[test_case("1 + 1", Some(2.0); "basic integer addition")]
#[test_case("1 - 1", Some(0.0); "basic integer subtracting")]
#[test_case("2 * 3", Some(6.0); "basic integer multiplication")]
#[test_case("6 / 3", Some(2.0); "basic integer division")]
#[test_case("1 + 2 * 3 + 1", Some(8.0); "order of operations")]
#[test_case("(1)", Some(1.0); "basic parenthetical")]
#[test_case("1 + 1 * ((((50))))", Some(51.0); "order of operations with nested parens")]
#[test_case("sqrt(1)", Some(1.0); "basic application")]
#[test_case("sqrt(abs(ln(1)))", Some(0.0); "nested applications")]
#[test_case("sqrt(abs(ln(1)", None; "no trailing end-parens")]
pub fn eval_test(text: &str, expected: Option<f64>) {
assert_eq!(eval_text(text), expected)
}
pub fn int_seq() -> impl Sequence<Vec<&'static str>> {
let nonzero_digit = ChooseSeq::from_str("123456789");
let digit = ChooseSeq::from_str("0123456789");
ChooseSeq::new(vec![
Box::new(MultipleSeq::new(vec![
Box::new(nonzero_digit),
Box::new(RepeatedSeq::new(Box::new(digit))),
])),
Box::new(RawSeq::new("0")),
])
}
#[test_case("1", true; "one-digit int")]
#[test_case("1234567890", true; "all digits")]
#[test_case("0", true; "special case")]
#[test_case("01", false; "leading zero")]
pub fn int_test(text: &str, should_match: bool) {
let tox = Token::token_vec_from_str(text, &|_, _| vec![]);
assert_eq!(
match_all_tokens(
&int_seq(),
tox.iter().collect::<Vec<&Token<Vec<&str>>>>().as_slice()
),
should_match
);
}