From 75460502efc1bd253c427dfcc41d8372d656b65e Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Wed, 29 Dec 2021 18:32:11 +0100 Subject: [PATCH] Special symbols for *, /, arc functions, and subscript --- cli/src/repl.rs | 49 +++++++++--- kalk/src/lexer.rs | 34 +++++++-- kalk/src/lib.rs | 1 + kalk/src/parser.rs | 37 +++++++++- kalk/src/text_utils.rs | 77 +++++++++++++++++++ web/src/KalkCalculator.svelte | 135 +++++++++++++++++++++++++++++++++- 6 files changed, 311 insertions(+), 22 deletions(-) create mode 100644 kalk/src/text_utils.rs diff --git a/cli/src/repl.rs b/cli/src/repl.rs index 1d6cad6..6d3c9db 100644 --- a/cli/src/repl.rs +++ b/cli/src/repl.rs @@ -4,8 +4,8 @@ use kalk::parser; use lazy_static::lazy_static; use regex::Captures; use regex::Regex; -use rustyline::config::Configurer; use rustyline::completion::Completer; +use rustyline::config::Configurer; use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; use rustyline::hint::Hinter; @@ -17,8 +17,8 @@ use rustyline::{Editor, Helper}; use std::borrow::Cow; use std::borrow::Cow::Owned; use std::collections::HashMap; -use std::process; use std::fs; +use std::process; pub fn start(mut parser: &mut parser::Context, precision: u32) { let mut editor = Editor::::new(); @@ -96,8 +96,8 @@ impl Highlighter for LineHighlighter { let reg = Regex::new( r"(?x) - (?P([+\-/*%^!]|if|otherwise)) | - (?P[^!-@\s_|^⌊⌋⌈⌉\[\]\{\}≠≥≤]+(_\d+)?)", + (?P([+\-/*%^!×÷]|if|otherwise)) | + (?P[^!-@\s_|^⌊⌋⌈⌉\[\]\{\}≠≥≤⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎]+(_\d+)?)", ) .unwrap(); @@ -146,6 +146,21 @@ lazy_static! { m.insert("!=", "≠"); m.insert(">=", "≥"); m.insert("<=", "≤"); + m.insert("*", "×"); + m.insert("/", "÷"); + m.insert("asin", "sin⁻¹()"); + m.insert("acos", "cos⁻¹()"); + m.insert("atan", "tan⁻¹()"); + m.insert("acot", "cot⁻¹()"); + m.insert("acosec", "cosec⁻¹()"); + m.insert("asec", "sec⁻¹()"); + m.insert("asinh", "sinh⁻¹()"); + m.insert("acosh", "cosh⁻¹()"); + m.insert("atanh", "tanh⁻¹()"); + m.insert("acoth", "coth⁻¹()"); + m.insert("acosech", "cosech⁻¹()"); + m.insert("asech", "sech⁻¹()"); + m.insert("cbrt", "∛"); m }; } @@ -159,10 +174,25 @@ impl Completer for RLHelper { _ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec), ReadlineError> { for key in COMPLETION_FUNCS.keys() { - if line[..pos].ends_with(key) { + let slice = &line[..pos]; + if slice.ends_with(key) { let value = *COMPLETION_FUNCS.get(key).unwrap(); return Ok((pos - key.len(), vec![value.to_string()])); } + + let mut subscript_digits = String::new(); + for c in slice.chars().rev() { + if c.is_digit(10) { + subscript_digits.insert(0, c); + } else { + break; + } + } + + if subscript_digits.len() > 0 { + let value = kalk::text_utils::digits_to_subscript(subscript_digits.chars()); + return Ok((pos - subscript_digits.chars().count() - 1, vec![value])); + } } Ok((0, vec![line.to_string()])) @@ -171,11 +201,10 @@ impl Completer for RLHelper { fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str) { line.backspace(line.pos() - start); line.insert_str(line.pos(), elected); - line.move_forward(match elected { - "Σ()" => 2, - "∏()" => 2, - "∫()" => 2, - _ => 1, + line.move_forward(if elected.ends_with(")") { + elected.chars().count() - 1 + } else { + elected.chars().count() }); } } diff --git a/kalk/src/lexer.rs b/kalk/src/lexer.rs index fa0f90c..d4d1c8f 100644 --- a/kalk/src/lexer.rs +++ b/kalk/src/lexer.rs @@ -1,3 +1,4 @@ +use crate::text_utils::{is_subscript, is_superscript}; use std::iter::Peekable; use std::str; use std::str::Chars; @@ -111,8 +112,8 @@ impl<'a> Lexer<'a> { let token = match c { '+' => build(TokenKind::Plus, "", span), '-' => build(TokenKind::Minus, "", span), - '*' => build(TokenKind::Star, "", span), - '/' => build(TokenKind::Slash, "", span), + '*' | '×' => build(TokenKind::Star, "", span), + '/' | '÷' => build(TokenKind::Slash, "", span), '^' => build(TokenKind::Power, "", span), '|' => build(TokenKind::Pipe, "", span), '⌈' => build(TokenKind::OpenCeil, "", span), @@ -217,7 +218,13 @@ impl<'a> Lexer<'a> { // Only allow identifiers with a special character to have *one* character. No more. // Break the loop if it isn't the first run and the current character is a special character. - if end - start > 0 && !(c.is_ascii_alphabetic() || c == '\'' || c == '_') { + if end - start > 0 + && !(c.is_ascii_alphabetic() + || c == '\'' + || c == '_' + || is_superscript(&c) + || is_subscript(&c)) + { break; } @@ -237,9 +244,22 @@ impl<'a> Lexer<'a> { let value = match value.as_ref() { "Σ" | "∑" => String::from("sum"), "∏" => String::from("prod"), - "∫" => String::from("integrate"), + "∫" | "integral" => String::from("integrate"), + "sin⁻¹" => String::from("asin"), + "cos⁻¹" => String::from("acos"), + "tan⁻¹" => String::from("atan"), + "cot⁻¹" => String::from("acot"), + "cosec⁻¹" => String::from("acosec"), + "sec⁻¹" => String::from("asec"), + "sinh⁻¹" => String::from("asinh"), + "cosh⁻¹" => String::from("acosh"), + "tanh⁻¹" => String::from("atanh"), + "coth⁻¹" => String::from("acoth"), + "cosech⁻¹" => String::from("acosech"), + "sech⁻¹" => String::from("asech"), + "∛" => String::from("cbrt"), "°" => String::from("deg"), - _ => value, + _ => value, // things like log₂ are handled in the parser }; build(kind, &value, (start, end)) @@ -268,8 +288,8 @@ fn is_valid_identifier(c: Option<&char>) -> bool { match c { '+' | '-' | '/' | '*' | '%' | '^' | '!' | '(' | ')' | '=' | '.' | ',' | ';' | '|' | '⌊' | '⌋' | '⌈' | '⌉' | '[' | ']' | '{' | '}' | 'π' | '√' | 'τ' | 'ϕ' | 'Γ' | '<' - | '>' | '≠' | '≥' | '≤' => false, - _ => !c.is_digit(10), + | '>' | '≠' | '≥' | '≤' | '×' | '÷' => false, + _ => !c.is_digit(10) || is_superscript(c) || is_subscript(c), } } else { false diff --git a/kalk/src/lib.rs b/kalk/src/lib.rs index 7f30996..70d4c1b 100644 --- a/kalk/src/lib.rs +++ b/kalk/src/lib.rs @@ -8,3 +8,4 @@ pub mod parser; mod prelude; mod symbol_table; mod test_helpers; +pub mod text_utils; diff --git a/kalk/src/parser.rs b/kalk/src/parser.rs index 26c9303..93d10e9 100644 --- a/kalk/src/parser.rs +++ b/kalk/src/parser.rs @@ -114,6 +114,7 @@ pub enum CalcError { UnableToSolveEquation, UnableToOverrideConstant(String), UnableToParseExpression, + UnrecognizedLogBase, Unknown, } @@ -142,6 +143,7 @@ impl ToString for CalcError { CalcError::UnableToParseExpression => format!("Unable to parse expression."), CalcError::UnableToSolveEquation => format!("Unable to solve equation."), CalcError::UnableToOverrideConstant(name) => format!("Unable to override constant: '{}'.", name), + CalcError::UnrecognizedLogBase => format!("Unrecognized log base."), CalcError::Unknown => format!("Unknown error."), } } @@ -589,8 +591,19 @@ fn parse_group_fn(context: &mut Context) -> Result { fn parse_identifier(context: &mut Context) -> Result { let identifier = Identifier::from_full_name(&advance(context).value); + + let mut log_base = None; + if identifier.full_name.starts_with("log") { + if let Some(c) = identifier.full_name.chars().nth(3) { + if crate::text_utils::is_subscript(&c) { + log_base = Some(parse_log_base(&identifier)?); + } + } + } + let exists_as_fn = context.symbol_table.contains_fn(&identifier.pure_name) - || context.current_function.as_ref() == Some(&identifier.pure_name); + || context.current_function.as_ref() == Some(&identifier.pure_name) + || log_base.is_some(); // Eg. sqrt64 if exists_as_fn @@ -602,6 +615,14 @@ fn parse_identifier(context: &mut Context) -> Result { } else { parse_factor(context)? }; + + if let Some(log_base) = log_base { + return Ok(Expr::FnCall( + Identifier::from_full_name("log"), + vec![parameter, log_base], + )); + } + return Ok(Expr::FnCall(identifier, vec![parameter])); } @@ -637,6 +658,11 @@ fn parse_identifier(context: &mut Context) -> Result { context.is_in_integral = false; } + if let Some(log_base) = log_base { + parameters.push(log_base); + return Ok(Expr::FnCall(Identifier::from_full_name("log"), parameters)); + } + return Ok(Expr::FnCall(identifier, parameters)); } @@ -713,6 +739,15 @@ fn parse_identifier(context: &mut Context) -> Result { } } +fn parse_log_base(identifier: &Identifier) -> Result { + let subscript = identifier.full_name.chars().skip(3); + if let Some(base) = crate::text_utils::parse_subscript(subscript) { + Ok(Expr::Literal(base)) + } else { + Err(CalcError::UnrecognizedLogBase) + } +} + fn split_into_variables(context: &mut Context, identifier: &Identifier) -> Result { let mut chars: Vec = identifier.pure_name.chars().collect(); let mut left = Expr::Var(Identifier::from_full_name(&chars[0].to_string())); diff --git a/kalk/src/text_utils.rs b/kalk/src/text_utils.rs new file mode 100644 index 0000000..6cd282a --- /dev/null +++ b/kalk/src/text_utils.rs @@ -0,0 +1,77 @@ +pub fn is_superscript(c: &char) -> bool { + match c { + '⁰' | '¹' | '²' | '³' | '⁴' | '⁵' | '⁶' | '⁷' | '⁸' | '⁹' | '⁺' | '⁻' | '⁼' | '⁽' | '⁾' => { + true + } + _ => false, + } +} + +pub fn is_subscript(c: &char) -> bool { + match c { + '₀' | '₁' | '₂' | '₃' | '₄' | '₅' | '₆' | '₇' | '₈' | '₉' | '₊' | '₋' | '₌' | '₍' | '₎' => { + true + } + _ => false, + } +} + +pub fn parse_subscript(chars: impl Iterator) -> Option { + if let Ok(result) = subscript_to_digits(chars).parse::() { + Some(result) + } else { + None + } +} + +pub fn subscript_to_digits(chars: impl Iterator) -> String { + let mut regular = String::new(); + for c in chars { + regular.push(match c { + '₀' => '0', + '₁' => '1', + '₂' => '2', + '₃' => '3', + '₄' => '4', + '₅' => '5', + '₆' => '6', + '₇' => '7', + '₈' => '8', + '₉' => '9', + '₊' => '+', + '₋' => '-', + '₌' => '=', + '₍' => '(', + '₎' => ')', + _ => c, + }); + } + + return regular; +} + +pub fn digits_to_subscript(chars: impl Iterator) -> String { + let mut subscript = String::new(); + for c in chars { + subscript.push(match c { + '0' => '₀', + '1' => '₁', + '2' => '₂', + '3' => '₃', + '4' => '₄', + '5' => '₅', + '6' => '₆', + '7' => '₇', + '8' => '₈', + '9' => '₉', + '+' => '₊', + '-' => '₋', + '=' => '₌', + '(' => '₍', + ')' => '₎', + _ => c, + }); + } + + return subscript; +} diff --git a/web/src/KalkCalculator.svelte b/web/src/KalkCalculator.svelte index 6bdc41b..41e2e42 100644 --- a/web/src/KalkCalculator.svelte +++ b/web/src/KalkCalculator.svelte @@ -45,7 +45,6 @@ let calculatorElement: HTMLElement; let inputElement: HTMLTextAreaElement; let highlightedTextElement: HTMLElement; - let hasBeenInteractedWith = false; let ignoreNextInput = false; function setText(text: string) { @@ -103,7 +102,6 @@ } function handleKeyDown(event: KeyboardEvent, kalk: Kalk) { - hasBeenInteractedWith = true; if (event.key == "Enter") { if ( hasUnevenAmountOfBraces( @@ -119,7 +117,7 @@ if (input.trim() == "help") { output = `Link to usage guide`; } else if (input.trim() == "clear") { outputLines = []; @@ -133,6 +131,10 @@ : `${result}`; } + // Highlight + const target = event.target as HTMLInputElement; + setText(target.value); + outputLines = output ? [...outputLines, [getHtml(), true], [output, false]] : [...outputLines, [getHtml(), true]]; @@ -277,7 +279,7 @@ let result = input; let offset = 0; result = result.replace( - /(?(!=|[<>]=?))|(?[<>&]|(\n\s*\}?|\s+))|(?([+\-/*%^!≈]|if|otherwise)|(?[^!-@\s_|^⌊⌋⌈⌉≈\[\]\{\}≠≥≤]+(_\d+)?)\(?)/g, + /(?(!=|[<>]=?))|(?[<>&]|(\n\s*\}?|\s+))|(?([+\-/*%^!≈×÷]|if|otherwise)|(?[^!-@\s_|^⌊⌋⌈⌉≈\[\]\{\}≠≥≤⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎]+(_\d+)?)\(?)/g, (substring, _, comparison, _2, html, _3, op, identifier) => { if (comparison) { if (substring == "<=") return "≤"; @@ -302,6 +304,11 @@ } } + if (op) { + if (substring == "*") return "×"; + if (substring == "/") return "÷"; + } + if (identifier) { let substringWithoutParen = substring.endsWith("(") ? substring.slice(0, -1) @@ -353,6 +360,80 @@ newSubstring = "⌈⌉"; break; } + case "asin": { + newSubstring = "sin⁻¹"; + addParen = true; + break; + } + case "acos": { + newSubstring = "cos⁻¹"; + addParen = true; + break; + } + case "atan": { + newSubstring = "tan⁻¹"; + addParen = true; + break; + } + case "acot": { + newSubstring = "cot⁻¹"; + addParen = true; + break; + } + case "acosec": { + newSubstring = "cosec⁻¹"; + addParen = true; + break; + } + case "asec": { + newSubstring = "sec⁻¹"; + addParen = true; + break; + } + case "asinh": { + newSubstring = "sinh⁻¹"; + addParen = true; + break; + } + case "acosh": { + newSubstring = "cosh⁻¹"; + addParen = true; + break; + } + case "acoth": { + newSubstring = "coth⁻¹"; + addParen = true; + break; + } + case "acosech": { + newSubstring = "cosech⁻¹"; + addParen = true; + break; + } + case "asech": { + newSubstring = "sech⁻¹"; + addParen = true; + break; + } + case "cbrt": { + newSubstring = "∛"; + break; + } + } + + let underscoreIndex = newSubstring.lastIndexOf("_"); + if (underscoreIndex != -1) { + let subscript = ""; + for ( + let i = underscoreIndex + 1; + i < newSubstring.length; + i++ + ) { + subscript += digitToSubscript(newSubstring[i]); + } + + newSubstring = + newSubstring.slice(0, underscoreIndex) + subscript; } offset -= substring.length - newSubstring.length; @@ -376,8 +457,54 @@ } ); + result = result.replace(/([_₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎]\d+)/g, (substring) => { + let newSubstring = substring + .replace("_", "") + .replace(/\d/, digitToSubscript); + offset -= substring.length - newSubstring.length; + + return newSubstring; + }); + return [result, offset]; } + + function digitToSubscript(input: string): string { + switch (input) { + case "0": + return "₀"; + case "1": + return "₁"; + case "2": + return "₂"; + case "3": + return "₃"; + case "4": + return "₄"; + case "5": + return "₅"; + case "6": + return "₆"; + case "7": + return "₇"; + case "8": + return "₈"; + case "9": + return "₉"; + case "+": + return "₊"; + case "-": + return "₋"; + case "=": + return "₌"; + case "(": + return "₍"; + case ")": + return "₎"; + } + + return input; + }