diff --git a/src/egui_app.rs b/src/egui_app.rs index 0458b41..1caef9d 100644 --- a/src/egui_app.rs +++ b/src/egui_app.rs @@ -1,8 +1,8 @@ +use crate::consts::*; use crate::function::{FunctionEntry, Riemann, DEFAULT_FUNCTION_ENTRY}; use crate::misc::{dyn_mut_iter, option_vec_printer, JsonFileOutput, SerdeValueHelper}; -use crate::parsing::{generate_hint, process_func_str, test_func}; - -use crate::consts::*; +use crate::parsing::{process_func_str, test_func}; +use crate::suggestions::generate_hint; use eframe::{egui, epi}; use egui::plot::Plot; use egui::{ diff --git a/src/lib.rs b/src/lib.rs index e19d942..de38456 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ mod function; mod function_output; mod misc; mod parsing; +mod suggestions; cfg_if::cfg_if! { if #[cfg(target_arch = "wasm32")] { diff --git a/src/main.rs b/src/main.rs index 0940271..6f6ff69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod function; mod function_output; mod misc; mod parsing; +mod suggestions; // For running the program natively! (Because why not?) #[cfg(not(target_arch = "wasm32"))] diff --git a/src/misc.rs b/src/misc.rs index 2b5ab36..319a1e5 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -313,6 +313,36 @@ pub fn step_helper(max_i: usize, min_x: f64, step: f64) -> Vec { .collect() } +pub fn common_substring<'a>(a: &'a str, b: &'a str) -> Option { + let a_chars: Vec = a.chars().collect(); + let b_chars: Vec = b.chars().collect(); + if a_chars[0] != b_chars[0] { + return None; + } + + let mut last_value: String = a_chars[0].to_string(); + let max_common_i = std::cmp::min(a.len(), b.len()) - 1; + for i in 1..=max_common_i { + let a_i = a_chars[i]; + let b_i = b_chars[i]; + if a_i == b_i { + last_value += &a_i.to_string() + } else { + break; + } + } + + Some(last_value) +} + +pub fn chars_take(chars: &Vec, i: usize) -> String { + if i > chars.len() { + panic!("chars_take: i is larget than chars.len()"); + } + + chars.iter().rev().take(i).rev().collect::() +} + #[cfg(test)] mod tests { use super::*; @@ -380,6 +410,7 @@ mod tests { assert_eq!(resolution_helper(3, -2.0, 1.0), vec![-2.0, -1.0, 0.0]); } + /// Tests [`option_vec_printer`] #[test] fn option_vec_printer_test() { let values_strings: HashMap>, &str> = HashMap::from([ @@ -404,4 +435,31 @@ mod tests { assert_eq!(option_vec_printer(key), value); } } + + /// Tests [`common_substring`] + #[test] + fn common_substring_test() { + let values = HashMap::from([ + (("test", "text"), Some("te")), + (("lol", "text"), None), + (("sin(", "sinh("), Some("sin")), + (("aaa", "bbb"), None), + ]); + + for ((key_a, key_b), value) in values { + assert_eq!(common_substring(key_a, key_b), value.map(|e| e.to_owned())); + } + } + + #[test] + fn chars_take_test() { + let values = HashMap::from([(("test", 2), "st"), (("cool text", 4), "text")]); + + for ((in_str, i), value) in values { + assert_eq!( + chars_take(&in_str.chars().collect::>(), i), + value.to_owned() + ); + } + } } diff --git a/src/parsing.rs b/src/parsing.rs index e76fe75..9ca6690 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -204,136 +204,6 @@ pub fn test_func(function_string: &str) -> Option { } } -pub fn generate_hint(input: &str) -> String { - if input.is_empty() { - return "x^2".to_owned(); - } - - let chars: Vec = input.chars().collect(); - - 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 ")".to_owned(); - } - - let len = chars.len(); - - if chars.len() >= 5 { - let result_two = match (chars[len - 5].to_string() - + &chars[len - 4].to_string() - + &chars[len - 3].to_string() - + &chars[len - 2].to_string() - + &chars[len - 1].to_string()) - .as_str() - { - "round" => Some("("), - "fract" => Some("("), - "trunc" => Some("("), - "floor" => Some("("), - - _ => None, - }; - - if let Some(output) = result_two { - return output.to_owned(); - } - } - - if chars.len() >= 4 { - let result_two = match (chars[len - 4].to_string() - + &chars[len - 3].to_string() - + &chars[len - 2].to_string() - + &chars[len - 1].to_string()) - .as_str() - { - "asin" => Some("("), - "acos" => Some("("), - "atan" => Some("("), - - "sinh" => Some("("), - "cosh" => Some("("), - "tanh" => Some("("), - - "ceil" => Some("("), - "roun" => Some("d("), - "sqrt" => Some("("), - "cbrt" => Some("("), - "floo" => Some("r("), - - "frac" => Some("t("), - - _ => None, - }; - - if let Some(output) = result_two { - return output.to_owned(); - } - } - - if chars.len() >= 3 { - let result_two = match (chars[len - 3].to_string() - + &chars[len - 2].to_string() - + &chars[len - 1].to_string()) - .as_str() - { - "flo" => Some("or("), - "log" => Some("("), - "abs" => Some("("), - - "sin" => Some("("), - "cos" => Some("("), - "tan" => Some("("), - - "asi" => Some("n("), - "aco" => Some("s("), - "ata" => Some("n("), - "exp" => Some("("), - "fra" => Some("ct("), - "cbr" => Some("t("), - "cei" => Some("l("), - - _ => None, - }; - - if let Some(output) = result_two { - return output.to_owned(); - } - } - - if chars.len() >= 2 { - let result_two = match (chars[len - 2].to_string() + &chars[len - 1].to_string()).as_str() { - "lo" => Some("g("), - "si" => Some("n("), - "ab" => Some("s("), - "co" => Some("s("), - "ta" => Some("n("), - "as" => Some("in("), - "ac" => Some("os("), - "at" => Some("an("), - "ln" => Some("("), - "fl" => Some("oor("), - "sq" => Some("rt("), - "ex" => Some("p("), - "ce" => Some("il("), - - _ => None, - }; - - if let Some(output) = result_two { - return output.to_owned(); - } - } - - String::new() -} - #[cfg(test)] mod tests { use super::*; @@ -442,31 +312,4 @@ mod tests { test_process_helper(key, value); } } - - /// Tests to make sure hints are properly outputed based on input - #[test] - fn hint_test() { - let values = HashMap::from([ - ("", "x^2"), - ("sin(x", ")"), - ("sin(x)", ""), - ("x^x", ""), - ("(x+1)(x-1", ")"), - ("lo", "g("), - ("log", "("), - ("asi", "n("), - ("si", "n("), - ("asin", "("), - ("fl", "oor("), - ("ata", "n("), - ("at", "an("), - ("roun", "d("), - ("floo", "r("), - ("flo", "or("), - ]); - - for (key, value) in values { - assert_eq!(generate_hint(key), value.to_owned()); - } - } } diff --git a/src/suggestions.rs b/src/suggestions.rs new file mode 100644 index 0000000..f98e668 --- /dev/null +++ b/src/suggestions.rs @@ -0,0 +1,237 @@ +use crate::misc::{chars_take, common_substring}; +use std::collections::{HashMap, HashSet}; + +pub fn generate_hint(input: &str) -> String { + if input.is_empty() { + return "x^2".to_owned(); + } + + let chars: Vec = input.chars().collect(); + + 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 ")".to_owned(); + } + + let len = chars.len(); + + if len >= 5 { + let result_five = get_completion(chars_take(&chars, 5)); + + if let Some(output) = result_five { + return output.to_owned(); + } + } + + if len >= 4 { + let result_four = get_completion(chars_take(&chars, 4)); + + if let Some(output) = result_four { + return output.to_owned(); + } + } + + if len >= 3 { + let result_three = get_completion(chars_take(&chars, 3)); + + if let Some(output) = result_three { + return output.to_owned(); + } + } + + if len >= 2 { + let result_two = get_completion(chars_take(&chars, 2)); + if let Some(output) = result_two { + return output.to_owned(); + } + } + + String::new() +} + +fn gen_completion_hashmap(input: Vec) -> HashMap { + let mut tuple_list: Vec<(String, String)> = Vec::new(); + + for entry in input { + for i in 1..=entry.len() { + let (first, last) = entry.split_at(i); + tuple_list.push((first.to_string(), last.to_string())); + } + } + + let mut output: HashMap = HashMap::new(); + + let key_list: Vec = tuple_list.iter().map(|(key, _)| key.clone()).collect(); + + let mut seen = HashSet::new(); + for (key, value) in tuple_list.clone() { + if seen.contains(&key) { + continue; + } + + seen.insert(key.clone()); + + let duplicate_num = key_list.iter().filter(|ele| **ele == key).count(); + if 1 == duplicate_num { + output.insert(key, value); + continue; + } + + let same_keys_merged: Vec = tuple_list + .iter() + .filter(|(a, _)| **a == key) + .map(|(a, b)| a.clone() + b) + .collect(); + + let merged_key_value = key.clone() + &value; + + let mut common_substr: Option = None; + for same_key in same_keys_merged { + if let Some(common_substr_unwrapped) = common_substr { + common_substr = common_substring(&common_substr_unwrapped, &same_key); + } else { + common_substr = common_substring(&same_key, &merged_key_value) + } + + if common_substr.is_none() { + break; + } + } + + if let Some(common_substr_unwrapped) = common_substr { + println!("{}", common_substr_unwrapped); + output.insert(key.clone(), common_substr_unwrapped.replace(&key, "")); + } + } + // println!("{:?}", output); + + output +} + +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", +]; + +lazy_static::lazy_static! { + static ref COMPLETION_HASHMAP: HashMap = gen_completion_hashmap(SUPPORTED_FUNCTIONS.to_vec().iter().map(|ele| ele.to_string() + "(").collect()); +} + +pub fn get_completion(key: String) -> Option { + if key.is_empty() { + return None; + } + + match COMPLETION_HASHMAP.get(&key) { + Some(data_x) => { + if data_x.is_empty() { + None + } else { + Some(data_x.clone()) + } + } + None => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests to make sure hints are properly outputed based on input + #[test] + fn hint_test() { + let values = HashMap::from([ + ("", "x^2"), + ("sin(x", ")"), + ("sin(x)", ""), + ("x^x", ""), + ("(x+1)(x-1", ")"), + // ("lo", "g("), + // ("log", "("), + ("asi", "n("), + ("asin", "("), + ("fl", "oor("), + ("ata", "n("), + ("at", "an("), + ("roun", "d("), + ("floo", "r("), + ("flo", "or("), + ]); + + for (key, value) in values { + println!("{} + {}", key, value); + assert_eq!(generate_hint(key), value.to_owned()); + } + } + + #[test] + fn completion_hashmap_test() { + let mut values: HashMap> = HashMap::new(); + + let processed_func: Vec = SUPPORTED_FUNCTIONS + .iter() + .map(|ele| ele.to_string() + "(") + .collect(); + + let mut data_tuple: Vec<(String, Option)> = Vec::new(); + for func in processed_func.iter() { + for i in 1..=func.len() { + let (first, last) = func.split_at(i); + let value = match last { + "" => None, + x => Some(x.to_string()), + }; + data_tuple.push((first.to_string(), value)); + } + } + + let key_list: Vec = data_tuple.iter().map(|(a, _)| a.clone()).collect(); + + for (key, value) in data_tuple { + if key_list.iter().filter(|a| **a == key).count() == 1 { + values.insert(key, value); + } + } + + let values_old = values.clone(); + values = values + .iter() + .filter(|(key, _)| values_old.iter().filter(|(a, _)| a == key).count() == 1) + .map(|(a, b)| (a.to_string(), b.clone())) + .collect(); + + let manual_values: Vec<(&str, Option<&str>)> = + vec![("sin", None), ("cos", None), ("tan", None)]; + + for (key, value) in manual_values { + values.insert( + key.to_string(), + match value { + Some(x) => Some(x.to_string()), + None => None, + }, + ); + } + + for (key, value) in values { + println!( + "{} + {}", + key, + match value.clone() { + Some(x) => x.clone(), + None => "(No completion)".to_string(), + } + ); + + assert_eq!(get_completion(key.to_string()), value); + } + } +}