refactoring
This commit is contained in:
2
parsing/.gitignore
vendored
Normal file
2
parsing/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
20
parsing/Cargo.toml
Normal file
20
parsing/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "parsing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/Titaniumtown/YTBN-Graphing-Software/tree/main/parsing"
|
||||
description = "Parsing library for YTBN-Graphing-Software"
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
phf = "0.10.1"
|
||||
exmex = { git = "https://github.com/bertiqwerty/exmex.git", branch = "main", features = [
|
||||
"partial",
|
||||
] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[build-dependencies]
|
||||
phf_codegen = "0.10.0"
|
||||
55
parsing/build.rs
Normal file
55
parsing/build.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
|
||||
const SUPPORTED_FUNCTIONS: [&str; 22] = [
|
||||
"abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor",
|
||||
"round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=.git/logs/HEAD");
|
||||
println!("cargo:rerun-if-changed=src/*");
|
||||
|
||||
generate_hashmap();
|
||||
}
|
||||
|
||||
fn generate_hashmap() {
|
||||
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs");
|
||||
let mut file = BufWriter::new(File::create(&path).expect("Could not create file"));
|
||||
|
||||
let string_hashmap = compile_hashmap(
|
||||
SUPPORTED_FUNCTIONS
|
||||
.to_vec()
|
||||
.iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let mut hashmap = phf_codegen::Map::new();
|
||||
|
||||
for (key, value) in string_hashmap.iter() {
|
||||
hashmap.entry(key, value);
|
||||
}
|
||||
|
||||
write!(
|
||||
&mut file,
|
||||
"static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};",
|
||||
hashmap.build()
|
||||
)
|
||||
.expect("Could not write to file");
|
||||
|
||||
write!(
|
||||
&mut file,
|
||||
"#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};",
|
||||
SUPPORTED_FUNCTIONS.len(),
|
||||
SUPPORTED_FUNCTIONS.to_vec()
|
||||
)
|
||||
.expect("Could not write to file");
|
||||
}
|
||||
|
||||
include!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/src/autocomplete_helper.rs"
|
||||
));
|
||||
97
parsing/src/autocomplete_helper.rs
Normal file
97
parsing/src/autocomplete_helper.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use core::cmp::Ordering;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// https://www.dotnetperls.com/sort-rust
|
||||
fn compare_len_reverse_alpha(a: &String, b: &String) -> Ordering {
|
||||
match a.len().cmp(&b.len()) {
|
||||
Ordering::Equal => b.cmp(a),
|
||||
order => order,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates hashmap (well really a vector of tuple of strings that are then turned into a hashmap by phf)
|
||||
#[allow(dead_code)]
|
||||
pub fn compile_hashmap(data: Vec<String>) -> Vec<(String, String)> {
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let tuple_list_1: Vec<(String, String)> = data
|
||||
.iter()
|
||||
.map(|e| e.to_string() + "(")
|
||||
.flat_map(|func| all_possible_splits(func, &mut seen))
|
||||
.collect();
|
||||
|
||||
let keys: Vec<&String> = tuple_list_1.iter().map(|(a, _)| a).collect();
|
||||
let mut output: Vec<(String, String)> = Vec::new();
|
||||
let mut seen_3: HashSet<String> = HashSet::new();
|
||||
|
||||
for (key, value) in tuple_list_1.iter() {
|
||||
if seen_3.contains(&*key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen_3.insert(key.clone());
|
||||
if keys.iter().filter(|a| a == &&key).count() == 1 {
|
||||
output.push((key.clone(), format!(r#"Hint::Single("{}")"#, value)));
|
||||
} else {
|
||||
let mut multi_data = tuple_list_1
|
||||
.iter()
|
||||
.filter(|(a, _)| a == key)
|
||||
.map(|(_, b)| b)
|
||||
.collect::<Vec<&String>>();
|
||||
multi_data.sort_unstable_by(|a, b| compare_len_reverse_alpha(a, b));
|
||||
output.push((key.clone(), format!("Hint::Many(&{:?})", multi_data)));
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// Returns a vector of all possible splitting combinations of a strings
|
||||
#[allow(dead_code)]
|
||||
fn all_possible_splits(
|
||||
func: String, seen: &mut HashSet<(String, String)>,
|
||||
) -> Vec<(String, String)> {
|
||||
(1..func.len())
|
||||
.map(|i| {
|
||||
let (first, last) = func.split_at(i);
|
||||
(first.to_string(), last.to_string())
|
||||
})
|
||||
.flat_map(|(first, last)| {
|
||||
if seen.contains(&(first.clone(), last.clone())) {
|
||||
return None;
|
||||
}
|
||||
seen.insert((first.to_string(), last.to_string()));
|
||||
|
||||
Some((first, last))
|
||||
})
|
||||
.collect::<Vec<(String, String)>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Tests to make sure hashmap generation works as expected
|
||||
#[test]
|
||||
fn hashmap_gen_test() {
|
||||
let data = vec!["time", "text", "test"];
|
||||
let expect = vec![
|
||||
("t", r#"Hint::Many(&["ime(", "ext(", "est("])"#),
|
||||
("ti", r#"Hint::Single("me(")"#),
|
||||
("tim", r#"Hint::Single("e(")"#),
|
||||
("time", r#"Hint::Single("(")"#),
|
||||
("te", r#"Hint::Many(&["xt(", "st("])"#),
|
||||
("tex", r#"Hint::Single("t(")"#),
|
||||
("text", r#"Hint::Single("(")"#),
|
||||
("tes", r#"Hint::Single("t(")"#),
|
||||
("test", r#"Hint::Single("(")"#),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
compile_hashmap(data.iter().map(|e| e.to_string()).collect()),
|
||||
expect
|
||||
.iter()
|
||||
.map(|(a, b)| (a.to_string(), b.to_string()))
|
||||
.collect::<Vec<(String, String)>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
2
parsing/src/lib.rs
Normal file
2
parsing/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod parsing;
|
||||
pub mod suggestions;
|
||||
346
parsing/src/parsing.rs
Normal file
346
parsing/src/parsing.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use exmex::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Function returns `f64::NaN` at every x value, which is not displayed.
|
||||
static ref EMPTY_FUNCTION: FlatEx<f64> = exmex::parse::<f64>("0/0").unwrap();
|
||||
}
|
||||
/// Function that includes f(x), f'(x), f'(x)'s string representation, and
|
||||
/// f''(x)
|
||||
#[derive(Clone)]
|
||||
pub struct BackingFunction {
|
||||
/// f(x)
|
||||
function: FlatEx<f64>,
|
||||
/// f'(x)
|
||||
derivative_1: FlatEx<f64>,
|
||||
/// Mathematical representation of f'(x)
|
||||
derivative_1_str: String,
|
||||
/// f''(x)
|
||||
derivative_2: FlatEx<f64>,
|
||||
|
||||
nth_derivative: Option<(usize, FlatEx<f64>, String)>,
|
||||
}
|
||||
|
||||
impl BackingFunction {
|
||||
/// Create new [`BackingFunction`] instance
|
||||
pub fn new(func_str: &str) -> Result<Self, String> {
|
||||
let function = match func_str {
|
||||
"" => EMPTY_FUNCTION.clone(),
|
||||
_ => {
|
||||
let parse_result = exmex::parse::<f64>(func_str);
|
||||
|
||||
match &parse_result {
|
||||
Err(e) => return Err(e.to_string()),
|
||||
Ok(_) => {
|
||||
let var_names = parse_result.as_ref().unwrap().var_names().to_vec();
|
||||
|
||||
if var_names != ["x"] {
|
||||
let var_names_not_x: Vec<&String> = var_names
|
||||
.iter()
|
||||
.filter(|ele| ele != &"x")
|
||||
.collect::<Vec<&String>>();
|
||||
|
||||
return Err(match var_names_not_x.len() {
|
||||
1 => {
|
||||
format!("Error: invalid variable: {}", var_names_not_x[0])
|
||||
}
|
||||
_ => {
|
||||
format!("Error: invalid variables: {:?}", var_names_not_x)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
parse_result.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let derivative_1 = function
|
||||
.partial(0)
|
||||
.unwrap_or_else(|_| EMPTY_FUNCTION.clone());
|
||||
|
||||
let derivative_1_str = prettyify_function_str(derivative_1.unparse());
|
||||
|
||||
let derivative_2 = function
|
||||
.partial_iter([0, 0].iter())
|
||||
.unwrap_or_else(|_| EMPTY_FUNCTION.clone());
|
||||
|
||||
Ok(Self {
|
||||
function,
|
||||
derivative_1,
|
||||
derivative_1_str,
|
||||
derivative_2,
|
||||
nth_derivative: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns Mathematical representation of the function's derivative
|
||||
pub fn get_derivative_str(&self) -> &str { &self.derivative_1_str }
|
||||
|
||||
/// Calculate f(x)
|
||||
pub fn get(&self, x: f64) -> f64 { self.function.eval(&[x]).unwrap_or(f64::NAN) }
|
||||
|
||||
/// Calculate f'(x)
|
||||
pub fn get_derivative_1(&self, x: f64) -> f64 {
|
||||
self.derivative_1.eval(&[x]).unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
/// Calculate f''(x)
|
||||
pub fn get_derivative_2(&self, x: f64) -> f64 {
|
||||
self.derivative_2.eval(&[x]).unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
pub fn get_nth_derivative_str(&self) -> &str { &self.nth_derivative.as_ref().unwrap().2 }
|
||||
|
||||
pub fn get_nth_derivative(&mut self, n: usize, x: f64) -> f64 {
|
||||
match n {
|
||||
0 => self.get(x),
|
||||
1 => self.get_derivative_1(x),
|
||||
2 => self.get_derivative_2(x),
|
||||
_ => {
|
||||
if let Some((curr_n, curr_n_func, _)) = &self.nth_derivative {
|
||||
if curr_n == &n {
|
||||
return curr_n_func.eval(&[x]).unwrap_or(f64::NAN);
|
||||
}
|
||||
}
|
||||
let new_func = self
|
||||
.function
|
||||
.partial_iter((1..=n).map(|_| 0).collect::<Vec<usize>>().iter())
|
||||
.unwrap_or_else(|_| EMPTY_FUNCTION.clone());
|
||||
|
||||
self.nth_derivative = Some((
|
||||
n,
|
||||
new_func.clone(),
|
||||
prettyify_function_str(new_func.unparse()),
|
||||
));
|
||||
new_func.eval(&[x]).unwrap_or(f64::NAN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prettyify_function_str(func: &str) -> String {
|
||||
let new_str = func.to_owned().replace("{x}", "x");
|
||||
|
||||
if &new_str == "0/0" {
|
||||
"Undefined".to_owned()
|
||||
} else {
|
||||
new_str
|
||||
}
|
||||
}
|
||||
|
||||
pub const VALID_VARIABLES: [char; 5] = ['x', 'X', 'e', 'E', 'π'];
|
||||
const LETTERS: [char; 52] = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
|
||||
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
const NUMBERS: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
#[inline]
|
||||
pub fn is_variable(c: &char) -> bool { VALID_VARIABLES.contains(&c) }
|
||||
|
||||
#[inline]
|
||||
pub fn is_letter(c: &char) -> bool { LETTERS.contains(&c) }
|
||||
|
||||
#[inline]
|
||||
pub fn is_number(c: &char) -> bool { NUMBERS.contains(&c) }
|
||||
|
||||
/*
|
||||
EXTREMELY Janky function that tries to put asterisks in the proper places to be parsed. This is so cursed. But it works, and I hopefully won't ever have to touch it again.
|
||||
One limitation though, variables with multiple characters like `pi` cannot be multiplied (like `pipipipi` won't result in `pi*pi*pi*pi`). But that's such a niche use case (and that same thing could be done by using exponents) that it doesn't really matter.
|
||||
In the future I may want to completely rewrite this or implement this natively in exmex.
|
||||
*/
|
||||
pub fn process_func_str(function_in: &str) -> String {
|
||||
let function = function_in
|
||||
.replace("log10(", "log(") // log10 -> log
|
||||
.replace("pi", "π") // pi -> π
|
||||
.replace("exp", "\u{1fc93}"); // replace 'exp' with this random unicode character because it can't be parsed correctly
|
||||
let function_chars: Vec<char> = function.chars().collect();
|
||||
let mut output_string: String = String::new();
|
||||
for (i, c) in function_chars.iter().enumerate() {
|
||||
let mut add_asterisk: bool = false;
|
||||
|
||||
let prev_prev_prev_char = if i > 2 {
|
||||
*function_chars.get(i - 3).unwrap()
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
|
||||
let prev_prev_char = if i > 1 {
|
||||
*function_chars.get(i - 2).unwrap()
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
|
||||
let prev_char = if i > 0 {
|
||||
*function_chars.get(i - 1).unwrap()
|
||||
} else {
|
||||
' '
|
||||
};
|
||||
|
||||
let c_is_number = is_number(c);
|
||||
let c_is_letter = is_letter(c);
|
||||
let c_is_variable = is_variable(c);
|
||||
let prev_char_is_variable = is_variable(&prev_char);
|
||||
let prev_char_is_number = is_number(&prev_char);
|
||||
|
||||
// makes special case for log with base of a 1-2 digit number
|
||||
if ((prev_prev_prev_char == 'l')
|
||||
&& (prev_prev_char == 'o')
|
||||
&& (prev_char == 'g')
|
||||
&& c_is_number)
|
||||
| ((prev_prev_char == 'c') && (prev_char == 'e') && (*c == 'i'))
|
||||
{
|
||||
output_string += &c.to_string();
|
||||
continue;
|
||||
}
|
||||
|
||||
let c_letters_var = c_is_letter | c_is_variable;
|
||||
let prev_letters_var = prev_char_is_variable | is_letter(&prev_char);
|
||||
|
||||
if prev_char == ')' {
|
||||
// cases like `)x`, `)2`, and `)(`
|
||||
if c_letters_var | c_is_number | (*c == '(') {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if *c == '(' {
|
||||
// cases like `x(` and `2(`
|
||||
if (prev_char_is_variable | prev_char_is_number) && !is_letter(&prev_prev_char) {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if prev_char_is_number {
|
||||
// cases like `2x` and `2sin(x)`
|
||||
if c_letters_var {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if c_is_letter {
|
||||
// cases like `e2` and `xx`
|
||||
if prev_char_is_number
|
||||
| (prev_char_is_variable && c_is_variable)
|
||||
| prev_char_is_variable
|
||||
| (prev_char == 'π')
|
||||
{
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if (c_is_number | c_letters_var) && prev_letters_var {
|
||||
// cases like `x2` and `xx`
|
||||
add_asterisk = true;
|
||||
}
|
||||
|
||||
// if add_asterisk is true, add the asterisk
|
||||
if add_asterisk {
|
||||
output_string += "*";
|
||||
}
|
||||
|
||||
// push current char to `output_string` (which is eventually returned)
|
||||
output_string += &c.to_string();
|
||||
}
|
||||
|
||||
output_string
|
||||
.replace("log(", "log10(")
|
||||
.replace('\u{1fc93}', "exp")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::suggestions::SUPPORTED_FUNCTIONS;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// returns if function with string `func_str` is valid after processing through [`process_func_str`]
|
||||
fn func_is_valid(func_str: &str) -> bool {
|
||||
BackingFunction::new(&process_func_str(func_str)).is_ok()
|
||||
}
|
||||
|
||||
/// Used for testing: passes function to [`process_func_str`] before running [`test_func`]. if `expect_valid` == `true`, it expects no errors to be created.
|
||||
fn test_func_helper(func_str: &str, expect_valid: bool) {
|
||||
let is_valid = func_is_valid(func_str);
|
||||
println!(
|
||||
"function: {} (expected: {}, got: {})",
|
||||
func_str, expect_valid, is_valid
|
||||
);
|
||||
|
||||
assert!(is_valid == expect_valid);
|
||||
}
|
||||
|
||||
/// Tests to make sure functions that are expected to succeed, succeed.
|
||||
#[test]
|
||||
fn test_expected_func_successes() {
|
||||
let functions = vec![
|
||||
"x^2",
|
||||
"2x",
|
||||
"E^x",
|
||||
"log10(x)",
|
||||
"xxxxx", // test variables side-by-side
|
||||
"sin(x)",
|
||||
"xsin(x)", // Tests `x{letter}` pattern
|
||||
"sin(x)cos(x)", // Tests `){letter}` pattern
|
||||
"x/0", // always returns NaN
|
||||
"(x+1)(x-3)", // tests 2 parentheses in `)(` pattern
|
||||
"(2x+1)x",
|
||||
"(2x+1)pi",
|
||||
"pi(2x+1)",
|
||||
"pipipipipipix",
|
||||
"e^sin(x)",
|
||||
"E^sin(x)",
|
||||
"e^x",
|
||||
];
|
||||
|
||||
for func_str in functions.iter().cloned() {
|
||||
test_func_helper(func_str, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests to make sure functions that are expected to fail, fail.
|
||||
#[test]
|
||||
fn test_expected_func_failures() {
|
||||
let functions = vec![
|
||||
"a", // Invalid variable
|
||||
"l^2", // Invalid variable
|
||||
"log222(x)", // Invalid function
|
||||
"abcdef", // Invalid variables
|
||||
"log10(x", // unclosed bracket
|
||||
"x^a", // Invalid variable
|
||||
"sin(cos(x)))", // extra bracket
|
||||
"((())", // extra opening bracket
|
||||
"0/0",
|
||||
];
|
||||
|
||||
for func_str in functions.iter().cloned() {
|
||||
test_func_helper(func_str, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helps with tests of [`process_func_str`]
|
||||
#[cfg(test)]
|
||||
fn test_process_helper(input: &str, expected: &str) {
|
||||
assert_eq!(&process_func_str(input), expected);
|
||||
}
|
||||
|
||||
/// Tests to make sure my cursed function works as intended
|
||||
#[test]
|
||||
fn func_process_test() {
|
||||
let values = HashMap::from([
|
||||
("2x", "2*x"),
|
||||
(")(", ")*("),
|
||||
("(2", "(2"),
|
||||
("log10(x)", "log10(x)"),
|
||||
("log2(x)", "log2(x)"),
|
||||
("pipipipipipi", "π*π*π*π*π*π"),
|
||||
("10pi", "10*π"),
|
||||
("pi10", "π*10"),
|
||||
("emax(x)", "e*max(x)"),
|
||||
("pisin(x)", "π*sin(x)"),
|
||||
("e^sin(x)", "e^sin(x)"),
|
||||
]);
|
||||
|
||||
for (key, value) in values {
|
||||
test_process_helper(key, value);
|
||||
}
|
||||
|
||||
for func in SUPPORTED_FUNCTIONS.iter() {
|
||||
let func_new = format!("{}(x)", func);
|
||||
test_process_helper(&func_new, &func_new);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
parsing/src/suggestions.rs
Normal file
172
parsing/src/suggestions.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use crate::parsing::is_number;
|
||||
|
||||
pub const HINT_EMPTY: Hint = Hint::Single("x^2");
|
||||
const HINT_CLOSED_PARENS: Hint = Hint::Single(")");
|
||||
|
||||
/// Only enacts println if cfg(test) is enabled
|
||||
macro_rules! test_print {
|
||||
($($arg:tt)*) => {
|
||||
#[cfg(test)]
|
||||
println!($($arg)*)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Generate a hint based on the input `input`, returns an `Option<String>`
|
||||
pub fn generate_hint<'a>(input: &str) -> &'a Hint<'a> {
|
||||
if input.is_empty() {
|
||||
return &HINT_EMPTY;
|
||||
}
|
||||
|
||||
let chars: Vec<char> = input.chars().collect::<Vec<char>>();
|
||||
|
||||
let mut open_parens: usize = 0;
|
||||
let mut closed_parens: usize = 0;
|
||||
chars.iter().for_each(|chr| match *chr {
|
||||
'(' => open_parens += 1,
|
||||
')' => closed_parens += 1,
|
||||
_ => {}
|
||||
});
|
||||
|
||||
if open_parens > closed_parens {
|
||||
return &HINT_CLOSED_PARENS;
|
||||
}
|
||||
|
||||
// let len = chars.len();
|
||||
|
||||
|
||||
let mut split: Vec<String> = Vec::new();
|
||||
|
||||
let mut buffer: Vec<char> = Vec::new();
|
||||
for c in chars {
|
||||
buffer.push(c);
|
||||
if c == ')' {
|
||||
split.push(buffer.iter().collect::<String>());
|
||||
buffer.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
let buffer_string = buffer.iter().collect::<String>();
|
||||
|
||||
if ((&buffer_string == "log") | (&buffer_string == "log1")) && is_number(&c) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !buffer.is_empty() {
|
||||
split.push(buffer.iter().collect::<String>());
|
||||
}
|
||||
|
||||
test_print!("split: {:?}", split);
|
||||
|
||||
if split.is_empty() {
|
||||
return COMPLETION_HASHMAP.get(input).unwrap_or(&Hint::None);
|
||||
}
|
||||
|
||||
COMPLETION_HASHMAP.get(& unsafe {split.last().unwrap_unchecked()}.as_str()).unwrap_or(&Hint::None)
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Hint<'a> {
|
||||
Single(&'a str),
|
||||
Many(&'a [&'a str]),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for Hint<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) }
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Display for Hint<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Hint::Single(single_data) => {
|
||||
return write!(f, "{}", single_data);
|
||||
}
|
||||
Hint::Many(multi_data) => {
|
||||
return write!(f, "{:?}", multi_data);
|
||||
}
|
||||
Hint::None => {
|
||||
return write!(f, "None");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Hint<'a> {
|
||||
pub fn is_none(&self) -> bool { matches!(self, Hint::None) }
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_some(&self) -> bool { !self.is_none() }
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_single(&self) -> bool { matches!(self, Hint::Single(_)) }
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Tests to make sure hints are properly outputed based on input
|
||||
#[test]
|
||||
fn hint_test() {
|
||||
let values = HashMap::from([
|
||||
("", Hint::Single("x^2")),
|
||||
("si", Hint::Many(&["n(", "nh(", "gnum("])),
|
||||
("log", Hint::Many(&["2(", "10("])),
|
||||
("cos", Hint::Many(&["(", "h("])),
|
||||
("sin(", Hint::Single(")")),
|
||||
("sqrt", Hint::Single("(")),
|
||||
("ln(x)", Hint::None),
|
||||
("ln(x)cos", Hint::Many(&["(", "h("])),
|
||||
]);
|
||||
|
||||
for (key, value) in values {
|
||||
println!("{} + {:?}", key, value);
|
||||
assert_eq!(generate_hint(key), &value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_to_string_test() {
|
||||
let values = HashMap::from([
|
||||
("x^2", Hint::Single("x^2")),
|
||||
(
|
||||
r#"["n(", "nh(", "gnum("]"#,
|
||||
Hint::Many(&["n(", "nh(", "gnum("]),
|
||||
),
|
||||
(r#"["n("]"#, Hint::Many(&["n("])),
|
||||
("None", Hint::None),
|
||||
]);
|
||||
|
||||
for (key, value) in values {
|
||||
assert_eq!(value.to_string(), key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_function_test() {
|
||||
SUPPORTED_FUNCTIONS
|
||||
.iter()
|
||||
.map(|func1| {
|
||||
SUPPORTED_FUNCTIONS
|
||||
.iter()
|
||||
.map(|func2| func1.to_string() + func2)
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.flatten()
|
||||
.filter(|func| !SUPPORTED_FUNCTIONS.contains(&func.as_str()))
|
||||
.for_each(|key| {
|
||||
println!("{}", key);
|
||||
if generate_hint(&key).is_none() {
|
||||
println!("success: {}", key);
|
||||
} else {
|
||||
panic!("failed: {}", key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user