parsing: improve function handling

This commit is contained in:
2025-12-13 03:33:20 -05:00
parent 41b50eb893
commit f2d0d27345

View File

@@ -1,17 +1,61 @@
use crate::parsing::is_variable; use crate::parsing::is_variable;
use crate::SUPPORTED_FUNCTIONS;
/// Protect function names that start with variable characters (like 'e' in 'exp')
/// by replacing them with a placeholder during parsing, then restoring them after.
/// This prevents incorrect splitting like "exp" -> "e" * "xp".
fn protect_function_names(input: &str) -> (String, Vec<(&'static str, String)>) {
let mut result = input.to_string();
let mut replacements = Vec::new();
// Only protect functions that start with a variable character
// Sort by length descending to replace longer matches first (e.g., "exp" before "e")
let mut funcs: Vec<&'static str> = SUPPORTED_FUNCTIONS
.iter()
.filter(|&&func| {
func.chars()
.next()
.map(|c| is_variable(&c))
.unwrap_or(false)
})
.copied()
.collect();
funcs.sort_by(|a, b| b.len().cmp(&a.len()));
for func in funcs {
// Use a placeholder made of letters that will be treated as a function name
// The placeholder won't be split because it's all letters
let placeholder = format!("zzzfn{}", replacements.len());
result = result.replace(func, &placeholder);
replacements.push((func, placeholder));
}
(result, replacements)
}
/// Restore protected function names from their placeholders
fn restore_function_names(input: &str, replacements: &[(&'static str, String)]) -> String {
let mut result = input.to_string();
for (func, placeholder) in replacements {
result = result.replace(placeholder, func);
}
result
}
pub fn split_function(input: &str, split: SplitType) -> Vec<String> { pub fn split_function(input: &str, split: SplitType) -> Vec<String> {
// Protect function names that could be incorrectly split
let (protected, replacements) = protect_function_names(input);
split_function_chars( split_function_chars(
&input &protected
.replace("pi", "π") // replace "pi" text with pi symbol .replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents .replace("**", "^") // support alternate manner of expressing exponents
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function
.chars() .chars()
.collect::<Vec<char>>(), .collect::<Vec<char>>(),
split, split,
) )
.iter() .iter()
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text .map(|x| restore_function_names(x, &replacements))
.collect::<Vec<String>>() .collect::<Vec<String>>()
} }
@@ -232,4 +276,13 @@ fn split_function_test() {
&["2", "sin(π) + 2", "cos(tau)"], &["2", "sin(π) + 2", "cos(tau)"],
SplitType::Multiplication, SplitType::Multiplication,
); );
// Test that exp() function is properly handled (not split into e*xp)
assert_test("exp(x)", &["exp(x)"], SplitType::Multiplication);
assert_test("2exp(x)", &["2", "exp(x)"], SplitType::Multiplication);
assert_test(
"exp(x)sin(x)",
&["exp(x)", "sin(x)"],
SplitType::Multiplication,
);
} }