pub(super) fn to_superscript_sequence(input: &str) -> String {
let mut out = String::new();
for ec in input.chars() {
match ec {
'0' => out.push('\u{2070}'),
'1' => out.push('\u{00B9}'),
'2' => out.push('\u{00B2}'),
'3' => out.push('\u{00B3}'),
'4' => out.push('\u{2074}'),
'5' => out.push('\u{2075}'),
'6' => out.push('\u{2076}'),
'7' => out.push('\u{2077}'),
'8' => out.push('\u{2078}'),
'9' => out.push('\u{2079}'),
'+' => out.push('\u{207A}'),
'-' => out.push('\u{207B}'),
'n' => out.push('\u{207F}'),
'k' => out.push('\u{1D4F}'),
'm' => out.push('\u{1D50}'),
'x' => out.push('\u{02E3}'),
'(' => out.push('\u{207D}'),
')' => out.push('\u{207E}'),
'/' => out.push('\u{2044}'),
'.' => out.push('\u{00B7}'),
_ => out.push(ec),
}
}
out
}
pub(super) fn to_subscript_sequence(input: &str) -> Option<String> {
let mut out = String::new();
for ch in input.chars() {
let mapped = match ch {
'0' => '\u{2080}',
'1' => '\u{2081}',
'2' => '\u{2082}',
'3' => '\u{2083}',
'4' => '\u{2084}',
'5' => '\u{2085}',
'6' => '\u{2086}',
'7' => '\u{2087}',
'8' => '\u{2088}',
'9' => '\u{2089}',
'a' => '\u{2090}',
'e' => '\u{2091}',
'o' => '\u{2092}',
'x' => '\u{2093}',
'h' => '\u{2095}',
'k' => '\u{2096}',
'l' => '\u{2097}',
'm' => '\u{2098}',
'n' => '\u{2099}',
'p' => '\u{209A}',
's' => '\u{209B}',
't' => '\u{209C}',
'i' => '\u{1D62}',
'r' => '\u{1D63}',
'u' => '\u{1D64}',
'v' => '\u{1D65}',
'+' => '\u{208A}',
'-' => '\u{208B}',
'(' => '\u{208D}',
')' => '\u{208E}',
_ => return None,
};
out.push(mapped);
}
Some(out)
}
pub(super) fn needs_grouping_in_fraction(expr: &str) -> bool {
let chars: Vec<char> = expr.chars().collect();
if chars.is_empty() {
return false;
}
if chars.first() == Some(&'(') && chars.last() == Some(&')') {
let mut depth = 0i32;
let mut returned_to_zero_before_end = false;
for (idx, &c) in chars.iter().enumerate() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 && idx < chars.len() - 1 {
returned_to_zero_before_end = true;
}
}
_ => {}
}
}
if !returned_to_zero_before_end {
return false;
}
}
let mut depth = 0usize;
let mut paren_groups = 0usize;
for &c in &chars {
match c {
'(' | '[' | '{' => {
if depth == 0 {
paren_groups += 1;
}
depth += 1;
}
')' | ']' | '}' => depth = depth.saturating_sub(1),
'+' | '-' | '\u{00D7}' | '\u{00F7}' | '\u{2212}' if depth == 0 => return true,
' ' | '\u{2202}' if depth == 0 => return true,
_ => {}
}
}
if paren_groups >= 2 {
return true;
}
if chars.first() == Some(&'d') && chars.len() >= 2 {
let rest = &chars[1..];
let is_differential = rest.iter().all(|&c| {
c.is_ascii_alphabetic()
|| c == '^'
|| c == '_'
|| ('\u{00B2}'..='\u{00B3}').contains(&c)
|| c == '\u{00B9}'
|| ('\u{2070}'..='\u{2079}').contains(&c)
|| ('\u{2080}'..='\u{2089}').contains(&c)
});
if is_differential {
return false;
}
}
let base_chars: Vec<char> = chars
.iter()
.copied()
.filter(|&c| {
!c.is_ascii_digit()
&& !c.is_ascii_alphabetic()
&& c != '^'
&& c != '_'
&& !('\u{00B9}'..='\u{00B3}').contains(&c)
&& !('\u{2070}'..='\u{2079}').contains(&c)
&& !('\u{2080}'..='\u{2089}').contains(&c)
})
.collect();
if base_chars.is_empty() {
let alpha_count = chars.iter().filter(|&&c| c.is_ascii_alphabetic()).count();
let digit_count = chars.iter().filter(|&&c| c.is_ascii_digit()).count();
if alpha_count == 1 && digit_count == 0 {
return false;
}
if alpha_count == 0 {
return false;
}
if alpha_count >= 2 {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn superscript_table_full_coverage() {
let cases = [
("0", "\u{2070}"),
("1", "\u{00B9}"),
("2", "\u{00B2}"),
("3", "\u{00B3}"),
("4", "\u{2074}"),
("5", "\u{2075}"),
("6", "\u{2076}"),
("7", "\u{2077}"),
("8", "\u{2078}"),
("9", "\u{2079}"),
("+", "\u{207A}"),
("-", "\u{207B}"),
("n", "\u{207F}"),
("k", "\u{1D4F}"),
("m", "\u{1D50}"),
("x", "\u{02E3}"),
("(", "\u{207D}"),
(")", "\u{207E}"),
("/", "\u{2044}"),
(".", "\u{00B7}"),
("z", "z"), ];
for (input, expected) in cases {
assert_eq!(to_superscript_sequence(input), expected, "input={input}");
}
}
#[test]
fn subscript_table_full_coverage() {
let mapped = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'e', 'o', 'x', 'h', 'k', 'l',
'm', 'n', 'p', 's', 't', 'i', 'r', 'u', 'v', '+', '-', '(', ')',
];
for c in mapped {
assert!(
to_subscript_sequence(&c.to_string()).is_some(),
"char {c:?}"
);
}
assert!(to_subscript_sequence("aeo").is_some());
assert!(to_subscript_sequence("z").is_none());
assert!(to_subscript_sequence("a1z").is_none());
}
#[test]
fn fraction_grouping_decision_matrix() {
assert!(!needs_grouping_in_fraction(""));
assert!(!needs_grouping_in_fraction("(x+1)"));
assert!(needs_grouping_in_fraction("(a)(b)"));
assert!(needs_grouping_in_fraction("a+b"));
assert!(needs_grouping_in_fraction("a-b"));
assert!(needs_grouping_in_fraction("a\u{00D7}b"));
assert!(needs_grouping_in_fraction("a\u{00F7}b"));
assert!(needs_grouping_in_fraction("a\u{2212}b"));
assert!(needs_grouping_in_fraction("a b"));
assert!(needs_grouping_in_fraction("\u{2202}f"));
assert!(!needs_grouping_in_fraction("dx"));
assert!(!needs_grouping_in_fraction("dxy"));
assert!(needs_grouping_in_fraction("(x)(y)(z)"));
assert!(!needs_grouping_in_fraction("a"));
assert!(!needs_grouping_in_fraction("123"));
assert!(needs_grouping_in_fraction("ab"));
}
}