280 lines
7.0 KiB
Rust
280 lines
7.0 KiB
Rust
use std::intrinsics::assume;
|
|
|
|
use crate::parsing::is_variable;
|
|
|
|
pub const HINT_EMPTY: Hint = Hint::Single("x^2");
|
|
const HINT_CLOSED_PARENS: Hint = Hint::Single(")");
|
|
|
|
/// Only enacts println if cfg(test) is enabled
|
|
#[allow(unused_macros)]
|
|
macro_rules! test_print {
|
|
($($arg:tt)*) => {
|
|
#[cfg(test)]
|
|
println!($($arg)*)
|
|
};
|
|
}
|
|
|
|
pub fn split_function(input: &str, split: SplitType) -> Vec<String> {
|
|
split_function_chars(
|
|
&input
|
|
.replace("pi", "π") // replace "pi" text with pi symbol
|
|
.replace("**", "^") // support alternate manner of expressing exponents
|
|
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function
|
|
.chars()
|
|
.collect::<Vec<char>>(),
|
|
split,
|
|
)
|
|
.iter()
|
|
.map(|x| x.replace("\u{1fc93}", "exp")) // Convert back to `exp` text
|
|
.collect::<Vec<String>>()
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
pub enum SplitType {
|
|
Multiplication,
|
|
Term,
|
|
}
|
|
|
|
pub fn split_function_chars(chars: &[char], split: SplitType) -> Vec<String> {
|
|
// Catch some basic cases
|
|
match chars.len() {
|
|
0 => return Vec::new(),
|
|
1 => return vec![chars[0].to_string()],
|
|
_ => {}
|
|
}
|
|
|
|
unsafe {
|
|
assume(chars.len() > 1);
|
|
assume(!chars.is_empty());
|
|
}
|
|
|
|
// Resulting split-up data
|
|
let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1);
|
|
|
|
/// Used to store info about a character
|
|
struct BoolSlice {
|
|
closing_parens: bool,
|
|
open_parens: bool,
|
|
number: bool,
|
|
letter: bool,
|
|
variable: bool,
|
|
masked_num: bool,
|
|
masked_var: bool,
|
|
}
|
|
|
|
impl BoolSlice {
|
|
const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self {
|
|
let isnumber = c.is_ascii_digit();
|
|
let isvariable = is_variable(c);
|
|
Self {
|
|
closing_parens: *c == ')',
|
|
open_parens: *c == '(',
|
|
number: isnumber,
|
|
letter: c.is_ascii_alphabetic(),
|
|
variable: isvariable,
|
|
masked_num: match isnumber {
|
|
true => prev_masked_num,
|
|
false => false,
|
|
},
|
|
masked_var: match isvariable {
|
|
true => prev_masked_var,
|
|
false => false,
|
|
},
|
|
}
|
|
}
|
|
|
|
const fn is_unmasked_variable(&self) -> bool { self.variable && !self.masked_var }
|
|
|
|
const fn is_unmasked_number(&self) -> bool { self.number && !self.masked_num }
|
|
|
|
const fn calculate_mask(&mut self, other: &BoolSlice) {
|
|
if other.masked_num && self.number {
|
|
// If previous char was a masked number, and current char is a number, mask current char's variable status
|
|
self.masked_num = true;
|
|
} else if other.masked_var && self.variable {
|
|
// If previous char was a masked variable, and current char is a variable, mask current char's variable status
|
|
self.masked_var = true;
|
|
} else if other.letter && !other.is_unmasked_variable() {
|
|
// If letter and not a variable (or a masked variable)
|
|
if self.number {
|
|
// Mask number status if current char is number
|
|
self.masked_num = true;
|
|
} else if self.variable {
|
|
// Mask variable status if current char is a variable
|
|
self.masked_var = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool {
|
|
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) {
|
|
return true;
|
|
} else if other.closing_parens {
|
|
// Cases like `)x`, `)2`, and `)(`
|
|
return (*c == '(')
|
|
| (self.letter && !self.is_unmasked_variable())
|
|
| self.is_unmasked_variable()
|
|
| self.is_unmasked_number();
|
|
} else if *c == '(' {
|
|
// Cases like `x(` and `2(`
|
|
return (other.is_unmasked_variable() | other.is_unmasked_number())
|
|
&& !other.letter;
|
|
} else if other.is_unmasked_number() {
|
|
// Cases like `2x` and `2sin(x)`
|
|
return self.is_unmasked_variable() | self.letter;
|
|
} else if self.is_unmasked_variable() | self.letter {
|
|
// Cases like `e2` and `xx`
|
|
return other.is_unmasked_number()
|
|
| (other.is_unmasked_variable() && self.is_unmasked_variable())
|
|
| other.is_unmasked_variable();
|
|
} else if (self.is_unmasked_number() | self.letter | self.is_unmasked_variable())
|
|
&& (other.is_unmasked_number() | other.letter)
|
|
{
|
|
return true;
|
|
} else if self.is_unmasked_number() && other.is_unmasked_variable() {
|
|
// Cases like `x2`
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup first char here
|
|
let mut prev_char: BoolSlice = BoolSlice::from_char(&chars[0], false, false);
|
|
|
|
let mut last = unsafe { data.last_mut().unwrap_unchecked() };
|
|
|
|
// Iterate through all chars excluding the first one
|
|
for c in chars.iter().skip(1) {
|
|
// Set data about current character
|
|
let mut curr_c = BoolSlice::from_char(c, prev_char.masked_num, prev_char.masked_var);
|
|
|
|
curr_c.calculate_mask(&prev_char);
|
|
|
|
// Append split
|
|
if curr_c.splitable(c, &prev_char, &split) {
|
|
data.push(String::new());
|
|
last = unsafe { data.last_mut().unwrap_unchecked() };
|
|
}
|
|
|
|
// Exclude asterisks
|
|
if c != &'*' {
|
|
last.push(*c);
|
|
}
|
|
|
|
// Move current character data to `prev_char`
|
|
prev_char = curr_c;
|
|
}
|
|
|
|
data
|
|
}
|
|
|
|
/// 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;
|
|
} else {
|
|
let chars: Vec<char> = input.chars().collect::<Vec<char>>();
|
|
|
|
unsafe {
|
|
assume(!chars.is_empty());
|
|
}
|
|
|
|
if let Some(hint) = COMPLETION_HASHMAP.get(get_last_term(&chars).as_str()) {
|
|
return hint;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
return &Hint::None;
|
|
}
|
|
}
|
|
|
|
pub fn get_last_term(chars: &[char]) -> String {
|
|
assert!(!chars.is_empty());
|
|
|
|
let mut result = split_function_chars(chars, SplitType::Term);
|
|
unsafe {
|
|
assume(!result.is_empty());
|
|
assume(result.len() > 0);
|
|
let output = result.pop();
|
|
assume(output.is_some());
|
|
output.unwrap_unchecked()
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Copy)]
|
|
pub enum Hint<'a> {
|
|
Single(&'a str),
|
|
Many(&'a [&'a str]),
|
|
None,
|
|
}
|
|
|
|
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> std::fmt::Debug for Hint<'a> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
std::fmt::Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl<'a> Hint<'a> {
|
|
#[inline]
|
|
pub const fn is_none(&self) -> bool { matches!(&self, &Hint::None) }
|
|
|
|
#[inline]
|
|
#[allow(dead_code)]
|
|
pub const fn is_some(&self) -> bool { !self.is_none() }
|
|
|
|
#[inline]
|
|
#[allow(dead_code)]
|
|
pub const fn is_single(&self) -> bool { matches!(&self, &Hint::Single(_)) }
|
|
|
|
#[inline]
|
|
#[allow(dead_code)]
|
|
pub const fn single(&self) -> Option<&&str> {
|
|
if let Hint::Single(data) = self {
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[allow(dead_code)]
|
|
pub const fn many(&self) -> Option<&&[&str]> {
|
|
if let Hint::Many(data) = self {
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
|