Special symbols for *, /, arc functions, and subscript

This commit is contained in:
PaddiM8 2021-12-29 18:32:11 +01:00
parent 09fafcb573
commit 75460502ef
6 changed files with 311 additions and 22 deletions

View File

@ -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::<RLHelper>::new();
@ -96,8 +96,8 @@ impl Highlighter for LineHighlighter {
let reg = Regex::new(
r"(?x)
(?P<op>([+\-/*%^!]|if|otherwise)) |
(?P<identifier>[^!-@\s_|^\[\]\{\}]+(_\d+)?)",
(?P<op>([+\-/*%^!×÷]|if|otherwise)) |
(?P<identifier>[^!-@\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<Self::Candidate>), 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()
});
}
}

View File

@ -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

View File

@ -8,3 +8,4 @@ pub mod parser;
mod prelude;
mod symbol_table;
mod test_helpers;
pub mod text_utils;

View File

@ -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<Expr, CalcError> {
fn parse_identifier(context: &mut Context) -> Result<Expr, CalcError> {
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<Expr, CalcError> {
} 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<Expr, CalcError> {
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<Expr, CalcError> {
}
}
fn parse_log_base(identifier: &Identifier) -> Result<Expr, CalcError> {
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<Expr, CalcError> {
let mut chars: Vec<char> = identifier.pure_name.chars().collect();
let mut left = Expr::Var(Identifier::from_full_name(&chars[0].to_string()));

77
kalk/src/text_utils.rs Normal file
View File

@ -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<Item = char>) -> Option<f64> {
if let Ok(result) = subscript_to_digits(chars).parse::<f64>() {
Some(result)
} else {
None
}
}
pub fn subscript_to_digits(chars: impl Iterator<Item = char>) -> 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<Item = char>) -> 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;
}

View File

@ -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 = `<a style="color: ${linkcolor}"
href="https://kalker.strct.net/#usage"
href="https://kalker.xyz/#usage"
target="blank">Link to usage guide</a>`;
} else if (input.trim() == "clear") {
outputLines = [];
@ -133,6 +131,10 @@
: `<span style="color: ${errorcolor}">${result}</span>`;
}
// 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(
/(?<comparison>(!=|[<>]=?))|(?<html>[<>&]|(\n\s*\}?|\s+))|(?<op>([+\-/*%^!≈]|if|otherwise)|(?<identifier>[^!-@\s_|^⌊⌋⌈⌉≈\[\]\{\}≠≥≤]+(_\d+)?)\(?)/g,
/(?<comparison>(!=|[<>]=?))|(?<html>[<>&]|(\n\s*\}?|\s+))|(?<op>([+\-/*%^!≈×÷]|if|otherwise)|(?<identifier>[^!-@\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;
}
</script>
<div class="calculator" bind:this={calculatorElement}>