mirror of
https://github.com/PaddiM8/kalker.git
synced 2025-02-07 20:19:18 +01:00
Special symbols for *, /, arc functions, and subscript
This commit is contained in:
parent
09fafcb573
commit
75460502ef
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -8,3 +8,4 @@ pub mod parser;
|
||||
mod prelude;
|
||||
mod symbol_table;
|
||||
mod test_helpers;
|
||||
pub mod text_utils;
|
||||
|
@ -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
77
kalk/src/text_utils.rs
Normal 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;
|
||||
}
|
@ -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}>
|
||||
|
Loading…
Reference in New Issue
Block a user