#[cfg(test)]
use crate::rules::math::parser::BracketKind;
use crate::rules::math::parser::MathToken;
use super::math_token_rule::{MathEncodeState, MathTokenEngine, MathTokenResult, MathTokenRule};
use super::{rule_1, rule_6, rule_12};
#[cfg(not(tarpaulin_include))]
fn is_fraction_slash(tok: Option<&MathToken>) -> bool {
matches!(
tok,
Some(MathToken::Operator('/') | MathToken::MathSymbol('\u{2044}'))
)
}
pub fn slash_as_fraction_symbol(tokens: &[MathToken], i: usize) -> bool {
let left = tokens.get(i.saturating_sub(1));
let right = tokens.get(i + 1);
matches!(
(left, right),
(
Some(MathToken::UpperVariable(_)),
Some(MathToken::UpperVariable(_))
)
) || matches!(
(left, right),
(Some(MathToken::Number(l)), Some(MathToken::Number(r)))
if l.chars().all(|c| c.is_ascii_digit())
&& r.chars().all(|c| c.is_ascii_digit())
)
}
pub struct FractionReversalRule;
pub struct GroupedFractionReversalRule;
impl MathTokenRule for GroupedFractionReversalRule {
fn name(&self) -> &'static str {
"GroupedFractionReversalRule"
}
fn priority(&self) -> u16 {
10
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::OpenParen(_)))
&& rule_6::find_matching_paren(tokens, index).is_some_and(|close| {
matches!(
tokens.get(close + 1),
Some(MathToken::Operator('/') | MathToken::MathSymbol('\u{2044}'))
)
})
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
let Some(left_close) = rule_6::find_matching_paren(tokens, index) else {
return Ok(MathTokenResult::Skip);
};
if !is_fraction_slash(tokens.get(left_close + 1)) {
return Ok(MathTokenResult::Skip);
}
let right_start = left_close + 2;
let left_kind = if let Some(MathToken::OpenParen(k)) = tokens.get(index) {
*k
} else {
unreachable!("matches() guarantees OpenParen at index")
};
if matches!(tokens.get(right_start), Some(MathToken::OpenParen(_))) {
let Some(right_close) = rule_6::find_matching_paren(tokens, right_start) else {
return Ok(MathTokenResult::Skip);
};
rule_6::encode_open_paren(left_kind, result);
engine.encode_tokens(&tokens[index + 1..left_close], result)?;
rule_6::encode_close_paren(left_kind, result);
result.push(12);
engine.encode_tokens(&tokens[right_start..=right_close], result)?;
state.prev_was_number = false;
return Ok(MathTokenResult::Consumed(right_close + 1 - index));
}
let right_end = find_simple_right_end(tokens, right_start);
if right_end == right_start {
return Ok(MathTokenResult::Skip);
}
rule_6::encode_open_paren(left_kind, result);
engine.encode_tokens(&tokens[index + 1..left_close], result)?;
rule_6::encode_close_paren(left_kind, result);
result.push(12);
engine.encode_tokens(&tokens[right_start..right_end], result)?;
state.prev_was_number = false;
Ok(MathTokenResult::Consumed(right_end - index))
}
}
fn find_simple_right_end(tokens: &[MathToken], start: usize) -> usize {
let mut i = start;
while i < tokens.len() {
match &tokens[i] {
MathToken::Number(_)
| MathToken::Variable(_)
| MathToken::UpperVariable(_)
| MathToken::Superscript(_)
| MathToken::Subscript(_)
| MathToken::Prime => {
i += 1;
}
_ => break,
}
}
i
}
impl MathTokenRule for FractionReversalRule {
fn name(&self) -> &'static str {
"FractionReversalRule"
}
fn priority(&self) -> u16 {
10
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::Number(_)))
&& matches!(tokens.get(index + 1), Some(MathToken::Operator('/')))
&& matches!(tokens.get(index + 2), Some(MathToken::Number(_)))
&& !slash_as_fraction_symbol(tokens, index + 1)
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
let (Some(MathToken::Number(left)), Some(MathToken::Number(right))) =
(tokens.get(index), tokens.get(index + 2))
else {
return Ok(MathTokenResult::Skip);
};
rule_1::encode_number_with_prefix(right, false, result);
result.push(12);
rule_1::encode_number_with_prefix(left, false, result);
state.prev_was_number = false;
Ok(MathTokenResult::Consumed(3))
}
}
pub struct VariableFractionInListRule;
impl MathTokenRule for VariableFractionInListRule {
fn name(&self) -> &'static str {
"VariableFractionInListRule"
}
fn priority(&self) -> u16 {
10
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::Variable(_)))
&& matches!(tokens.get(index + 1), Some(MathToken::Operator('/')))
&& matches!(tokens.get(index + 2), Some(MathToken::Variable(_)))
&& {
let prev = rule_12::prev_non_space(tokens, index);
matches!(
prev,
None | Some(MathToken::OpenParen(_)) | Some(MathToken::Operator(','))
)
}
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
let (Some(MathToken::Variable(num)), Some(MathToken::Variable(den))) =
(tokens.get(index), tokens.get(index + 2))
else {
return Ok(MathTokenResult::Skip);
};
let mut den_end = index + 3;
while matches!(
tokens.get(den_end),
Some(MathToken::Subscript(_)) | Some(MathToken::Superscript(_))
) {
den_end += 1;
}
result.push(crate::english::encode_english(den.to_ascii_lowercase())?);
if den_end > index + 3 {
engine.encode_tokens(&tokens[index + 3..den_end], result)?;
}
result.push(12); result.push(crate::english::encode_english(num.to_ascii_lowercase())?);
state.prev_was_number = false;
Ok(MathTokenResult::Consumed(den_end - index))
}
}
pub struct ConditionalProbFractionRule;
impl MathTokenRule for ConditionalProbFractionRule {
fn name(&self) -> &'static str {
"ConditionalProbFractionRule"
}
fn priority(&self) -> u16 {
10
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::Number(_)))
&& matches!(tokens.get(index + 1), Some(MathToken::Operator('/')))
&& matches!(tokens.get(index + 2), Some(MathToken::Number(_)))
&& matches!(
rule_12::prev_non_space(tokens, index),
Some(MathToken::Operator('='))
)
&& tokens
.iter()
.any(|token| matches!(token, MathToken::MathSymbol('|')))
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
let (Some(MathToken::Number(left)), Some(MathToken::Number(right))) =
(tokens.get(index), tokens.get(index + 2))
else {
return Ok(MathTokenResult::Skip);
};
rule_1::encode_number_literal(right, result);
result.push(12);
rule_1::encode_number_literal(left, result);
state.prev_was_number = false;
Ok(MathTokenResult::Consumed(3))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::math::math_token_rule::{MathContext, MathEncodeState, MathTokenEngine};
fn dummy_engine() -> MathTokenEngine {
MathTokenEngine::with_context(MathContext::default())
}
fn enc(input: &str) -> Vec<u8> {
crate::encode(input).unwrap_or_default()
}
#[rstest::rstest]
#[case::upper_over_upper(
vec![MathToken::UpperVariable('A'), MathToken::Operator('/'), MathToken::UpperVariable('B')],
true,
)]
#[case::digit_over_digit(
vec![MathToken::Number("3".into()), MathToken::Operator('/'), MathToken::Number("4".into())],
true,
)]
#[case::var_over_var_not_fraction(
vec![MathToken::Variable('x'), MathToken::Operator('/'), MathToken::Variable('y')],
false,
)]
fn slash_as_fraction_symbol_paths(#[case] tokens: Vec<MathToken>, #[case] expected: bool) {
assert_eq!(slash_as_fraction_symbol(&tokens, 1), expected);
}
#[test]
fn grouped_fraction_reversal_metadata() {
let r = GroupedFractionReversalRule;
assert_eq!(r.priority(), 10);
assert_eq!(r.name(), "GroupedFractionReversalRule");
}
#[test]
fn grouped_fraction_reversal_matches() {
let r = GroupedFractionReversalRule;
let tokens = vec![
MathToken::OpenParen(BracketKind::Grouping),
MathToken::Variable('a'),
MathToken::Operator('+'),
MathToken::Variable('b'),
MathToken::CloseParen(BracketKind::Grouping),
MathToken::Operator('/'),
MathToken::Variable('c'),
];
let state = MathEncodeState::with_context(false, MathContext::default());
assert!(r.matches(&tokens, 0, &state));
assert!(!r.matches(&tokens, 1, &state));
}
#[test]
fn grouped_fraction_reversal_simple_right_side() {
let bytes = enc("$(a+b)/c$");
assert!(!bytes.is_empty());
}
#[test]
fn grouped_fraction_reversal_paren_right_side() {
let bytes = enc("$(a+b)/(c+d)$");
assert!(!bytes.is_empty());
}
#[rstest::rstest]
#[case::advances_until_operator(
vec![
MathToken::Number("1".into()),
MathToken::Variable('x'),
MathToken::Prime,
MathToken::Operator('+'),
MathToken::Number("2".into()),
],
0,
3,
)]
#[case::operator_stops_immediately(
vec![
MathToken::Number("1".into()),
MathToken::Variable('x'),
MathToken::Prime,
MathToken::Operator('+'),
MathToken::Number("2".into()),
],
3,
3,
)]
#[case::empty_returns_index(vec![], 0, 0)]
fn find_simple_right_end_traverses_simple_tokens(
#[case] tokens: Vec<MathToken>,
#[case] index: usize,
#[case] expected: usize,
) {
assert_eq!(find_simple_right_end(&tokens, index), expected);
}
#[test]
fn fraction_reversal_metadata() {
let r = FractionReversalRule;
assert_eq!(r.priority(), 10);
assert_eq!(r.name(), "FractionReversalRule");
}
#[test]
fn fraction_reversal_matches_only_non_fraction_symbol_context() {
let r = FractionReversalRule;
let state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::Number("3".into()),
MathToken::Operator('/'),
MathToken::Number("4".into()),
];
assert!(!r.matches(&toks, 0, &state));
}
#[test]
fn variable_fraction_in_list_metadata() {
let r = VariableFractionInListRule;
assert_eq!(r.priority(), 10);
assert_eq!(r.name(), "VariableFractionInListRule");
}
#[test]
fn variable_fraction_in_list_matches_after_open_paren() {
let r = VariableFractionInListRule;
let state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('f'),
MathToken::Operator('/'),
MathToken::Variable('x'),
MathToken::CloseParen(BracketKind::MathParen),
];
assert!(r.matches(&toks, 1, &state));
}
#[test]
fn variable_fraction_in_list_apply() {
let r = VariableFractionInListRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('f'),
MathToken::Operator('/'),
MathToken::Variable('x'),
MathToken::CloseParen(BracketKind::MathParen),
];
let mut result = Vec::new();
let engine = dummy_engine();
let r2 = r.apply(&toks, 1, &mut result, &mut state, &engine);
assert!(r2.is_ok());
assert!(!result.is_empty());
}
#[test]
fn variable_fraction_in_list_with_subscript_via_pipeline() {
let bytes = enc("$(f/x_{1})$");
assert!(!bytes.is_empty());
}
#[test]
fn conditional_prob_metadata() {
let r = ConditionalProbFractionRule;
assert_eq!(r.priority(), 10);
assert_eq!(r.name(), "ConditionalProbFractionRule");
}
#[test]
fn conditional_prob_matches_with_divider_present() {
let r = ConditionalProbFractionRule;
let state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::Variable('p'),
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('a'),
MathToken::MathSymbol('|'),
MathToken::Variable('b'),
MathToken::CloseParen(BracketKind::MathParen),
MathToken::Operator('='),
MathToken::Number("1".into()),
MathToken::Operator('/'),
MathToken::Number("2".into()),
];
assert!(r.matches(&toks, 7, &state));
}
#[test]
fn conditional_prob_apply_emits_bytes() {
let r = ConditionalProbFractionRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::MathSymbol('|'),
MathToken::Operator('='),
MathToken::Number("1".into()),
MathToken::Operator('/'),
MathToken::Number("2".into()),
];
let mut result = Vec::new();
let engine = dummy_engine();
r.apply(&toks, 2, &mut result, &mut state, &engine)
.expect("apply");
assert!(!result.is_empty());
}
#[test]
fn grouped_fraction_reversal_apply_no_matching_paren_skip() {
let r = GroupedFractionReversalRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![MathToken::Variable('a'), MathToken::Operator('/')];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Skip) | Ok(_)));
}
#[test]
fn grouped_fraction_reversal_unmatched_right_paren_skip() {
let r = GroupedFractionReversalRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks: Vec<MathToken> = vec![
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('a'),
MathToken::CloseParen(BracketKind::MathParen),
MathToken::Operator('/'),
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('c'),
];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(res.is_ok());
}
#[test]
fn grouped_fraction_reversal_empty_simple_right_skip() {
let r = GroupedFractionReversalRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks: Vec<MathToken> = vec![
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('a'),
MathToken::CloseParen(BracketKind::MathParen),
MathToken::Operator('/'),
MathToken::Operator('+'), ];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Skip)));
}
#[test]
fn fraction_reversal_apply_malformed_tokens_skip() {
let r = FractionReversalRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::Variable('a'),
MathToken::Operator('/'),
MathToken::Variable('b'),
];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Skip)));
}
#[test]
fn variable_fraction_in_list_apply_malformed_skip() {
let r = VariableFractionInListRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::Number("1".into()),
MathToken::Operator('/'),
MathToken::Number("2".into()),
];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Skip)));
}
#[test]
fn conditional_prob_apply_malformed_skip() {
let r = ConditionalProbFractionRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::Variable('a'),
MathToken::Operator('/'),
MathToken::Variable('b'),
];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Skip)));
}
#[test]
fn grouped_fraction_apply_no_slash_after_paren() {
let r = GroupedFractionReversalRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('a'),
MathToken::CloseParen(BracketKind::MathParen),
MathToken::Variable('b'),
];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Skip)));
}
#[test]
fn fraction_reversal_apply_number_over_number() {
let r = FractionReversalRule;
let mut state = MathEncodeState::with_context(false, MathContext::default());
let toks = vec![
MathToken::Number("2".to_string()),
MathToken::Operator('/'),
MathToken::Number("3".to_string()),
];
let mut result = Vec::new();
let engine = dummy_engine();
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(res, Ok(MathTokenResult::Consumed(3))));
assert!(!result.is_empty(), "should emit reversed number bytes");
}
}