@mathigon/hilbert
Advanced tools
Comparing version 0.3.5 to 0.3.6
@@ -702,7 +702,2 @@ 'use strict'; | ||
lArr: '⇐', | ||
CC: 'ℂ', | ||
NN: 'ℕ', | ||
QQ: 'ℚ', | ||
RR: 'ℝ', | ||
ZZ: 'ℤ' | ||
}; | ||
@@ -742,3 +737,8 @@ const SPECIAL_IDENTIFIERS = { | ||
psi: 'ψ', | ||
omega: 'ω' | ||
omega: 'ω', | ||
CC: 'ℂ', | ||
NN: 'ℕ', | ||
QQ: 'ℚ', | ||
RR: 'ℝ', | ||
ZZ: 'ℤ' | ||
}; | ||
@@ -753,2 +753,8 @@ const ALPHABET = 'abcdefghijklmnopqrstuvwxyz'; | ||
const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS]; | ||
const FUNCTION_NAMES = { | ||
'_': 'sub', | ||
'^': 'sup', | ||
'//': '/', | ||
'÷': '/' | ||
}; | ||
const ESCAPES = { | ||
@@ -767,2 +773,20 @@ '<': '<', | ||
} | ||
const VOICE_STRINGS = { | ||
'+': 'plus', | ||
'−': 'minus', | ||
'·': 'times', | ||
'×': 'times', | ||
'/': 'over', | ||
'//': 'divided by', | ||
'%': 'percent', | ||
'!': 'factorial', | ||
'±': 'plus-minus', | ||
'=': 'equals', | ||
'≠': 'does not equal', | ||
'<': 'is less than', | ||
'>': 'is greater than', | ||
'≤': 'is less than or equal to', | ||
'≥': 'is greater than or equal to', | ||
'π': 'pi' | ||
}; | ||
@@ -789,2 +813,4 @@ // ============================================================================= | ||
/** Converts the expression to a MathML string. */ | ||
toVoice(custom = {}) { return ''; } | ||
/** Converts the expression to a MathML string. */ | ||
toMathML(custom = {}) { return ''; } | ||
@@ -800,2 +826,3 @@ } | ||
toString() { return '' + this.n; } | ||
toVoice() { return '' + this.n; } | ||
toMathML() { return `<mn>${this.n}</mn>`; } | ||
@@ -822,2 +849,5 @@ } | ||
toString() { return this.i; } | ||
toVoice(custom = {}) { | ||
return (this.i in custom) ? custom[this.i] : VOICE_STRINGS[this.i] || this.i; | ||
} | ||
} | ||
@@ -835,2 +865,3 @@ class ExprString extends ExprElement { | ||
toString() { return '"' + this.s + '"'; } | ||
toVoice() { return this.s; } | ||
toMathML() { return `<mtext>${this.s}</mtext>`; } | ||
@@ -848,2 +879,5 @@ } | ||
toString() { return this.o.replace('//', '/'); } | ||
toVoice(custom = {}) { | ||
return (this.o in custom) ? custom[this.o] : VOICE_STRINGS[this.o] || this.o; | ||
} | ||
get functions() { return [this.o]; } | ||
@@ -857,3 +891,4 @@ toMathML() { | ||
// ============================================================================= | ||
const PRECEDENCE = words('+ − * × · / ÷ // sup sub'); | ||
const PRECEDENCE = words('+ − * × · / ÷ // sup sub subsup'); | ||
const SUBSUP = words('sub sup subsup'); | ||
const COMMA = '<mo value="," lspace="0">,</mo>'; | ||
@@ -869,2 +904,4 @@ function needsBrackets(expr, parentFn) { | ||
return false; | ||
if (SUBSUP.includes(expr.fn) && SUBSUP.includes(parentFn)) | ||
return true; | ||
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr.fn); | ||
@@ -947,2 +984,4 @@ } | ||
return args.join('_'); | ||
if (this.fn === 'subsup') | ||
return `${args[0]}_${args[1]}^${args[2]}`; | ||
if (words('+ * × · / = < > ≤ ≥ ≈').includes(this.fn)) | ||
@@ -1001,2 +1040,7 @@ return args.join(' ' + this.fn + ' '); | ||
} | ||
if (this.fn === 'subsup') { | ||
const args1 = [addMRow(this.args[0], argsF[0]), | ||
addMRow(this.args[1], args[1]), addMRow(this.args[2], args[2])]; | ||
return `<msubsup>${args1.join('')}</msubsup>`; | ||
} | ||
if (isOneOf(this.fn, '(', '[', '{')) | ||
@@ -1016,2 +1060,32 @@ return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${argsF.join(COMMA)}</mfenced>`; | ||
} | ||
toVoice(custom = {}) { | ||
const args = this.args.map(a => a.toVoice(custom)); | ||
const joined = args.join(' '); | ||
if (this.fn in custom && !custom[this.fn]) | ||
return joined; | ||
if (isOneOf(this.fn, '(', '[', '{')) | ||
return `open bracket ${joined} close bracket`; | ||
if (this.fn === 'sqrt') | ||
return `square root of ${joined}`; | ||
if (this.fn === '%') | ||
return `${joined} percent`; | ||
if (this.fn === '!') | ||
return `${joined} factorial`; | ||
if (this.fn === '/') | ||
return `${args[0]} over ${args[1]}`; | ||
if (this.fn === '//') | ||
return `${args[0]} divided by ${args[1]}`; | ||
if (this.fn === 'sup') | ||
return `${args[0]} to the power of ${args[1]}`; | ||
if (this.fn === 'sub') | ||
return joined; | ||
if (this.fn === 'subsup') | ||
return `${args[0]} ${args[1]} to the power of ${args[2]}`; | ||
if (VOICE_STRINGS[this.fn]) | ||
return args.join(` ${VOICE_STRINGS[this.fn]} `); | ||
// TODO Implement other cases | ||
if (isSpecialFunction(this.fn)) | ||
return `${this.fn} ${joined}`; | ||
return `${this.fn} of ${joined}`; | ||
} | ||
} | ||
@@ -1033,2 +1107,5 @@ // ----------------------------------------------------------------------------- | ||
} | ||
toVoice(custom = {}) { | ||
return this.items.map(i => i.toVoice(custom)).join(' '); | ||
} | ||
collapse() { return collapseTerm(this.items).collapse(); } | ||
@@ -1050,2 +1127,5 @@ } | ||
function createToken(buffer, type) { | ||
if (type === TokenType.STR) | ||
return new ExprString(buffer); | ||
// Strings can be empty, but other types cannot. | ||
if (!buffer || !type) | ||
@@ -1055,4 +1135,2 @@ return undefined; | ||
return new ExprSpace(); | ||
if (type === TokenType.STR) | ||
return new ExprString(buffer); | ||
if (type === TokenType.NUM) { | ||
@@ -1156,3 +1234,3 @@ // This can happen if users simply type ".", which get parsed as number. | ||
} | ||
function findBinaryFunction(tokens, fn, toFn) { | ||
function findBinaryFunction(tokens, fn) { | ||
if (isOperator(tokens[0], fn)) | ||
@@ -1172,5 +1250,20 @@ throw ExprError.startOperator(tokens[0]); | ||
throw ExprError.consecutiveOperators(token.o, b.o); | ||
const args = [removeBrackets(a), removeBrackets(b)]; | ||
tokens.splice(i - 1, 3, new ExprFunction(toFn || token.o, args)); | ||
i -= 2; | ||
const token2 = tokens[i + 2]; | ||
if (fn === '^ _' && isOperator(token, '_ ^') && isOperator(token2, '_ ^') && token.o !== token2.o) { | ||
// Special handling for subsup operator. | ||
const c = tokens[i + 3]; | ||
if (c instanceof ExprOperator) | ||
throw ExprError.consecutiveOperators(token2.o, c.o); | ||
const args = [removeBrackets(a), removeBrackets(b), removeBrackets(c)]; | ||
if (token.o === '^') | ||
[args[1], args[2]] = [args[2], args[1]]; | ||
tokens.splice(i - 1, 5, new ExprFunction('subsup', args)); | ||
i -= 4; | ||
} | ||
else { | ||
const fn = FUNCTION_NAMES[token.o] || token.o; | ||
const args = [removeBrackets(a), removeBrackets(b)]; | ||
tokens.splice(i - 1, 3, new ExprFunction(fn, args)); | ||
i -= 2; | ||
} | ||
} | ||
@@ -1181,5 +1274,3 @@ } | ||
function prepareTerm(tokens) { | ||
// TODO Combine sup and sub calls into a single supsub function. | ||
findBinaryFunction(tokens, '^', 'sup'); | ||
findBinaryFunction(tokens, '_', 'sub'); | ||
findBinaryFunction(tokens, '^ _'); | ||
findBinaryFunction(tokens, '/'); | ||
@@ -1276,3 +1367,3 @@ return makeTerm(tokens); | ||
findBinaryFunction(tokens, '= < > ≤ ≥'); | ||
findBinaryFunction(tokens, '// ÷', '/'); | ||
findBinaryFunction(tokens, '// ÷'); | ||
// Match multiplication operators. | ||
@@ -1279,0 +1370,0 @@ tokens = findAssociativeFunction(tokens, '× * ·', true); |
{ | ||
"name": "@mathigon/hilbert", | ||
"version": "0.3.5", | ||
"version": "0.3.6", | ||
"description": "JavaScript expression parsing, MathML rendering and CAS.", | ||
@@ -27,4 +27,4 @@ "keywords": [ | ||
"dependencies": { | ||
"@mathigon/fermat": "^0.3.5", | ||
"@mathigon/core": "^0.3.5" | ||
"@mathigon/fermat": "^0.3.6", | ||
"@mathigon/core": "^0.3.6" | ||
}, | ||
@@ -31,0 +31,0 @@ "devDependencies": { |
@@ -14,5 +14,4 @@ # Hilbert.ts | ||
* [ ] Support for functions with subscripts (e.g. `log_a(b)`). | ||
* [ ] Support for super+subscripts (e.g. `a_n^2` or `a^2_n`). | ||
* [ ] Support for large operators (sum, product and integral). | ||
* [ ] Parse ^ operator from right to left (e.g. `2^2^2 == 2^(2^2)`). | ||
* [ ] Parse ^ and _ operator from right to left (e.g. `2^2^2 == 2^(2^2)`). | ||
* [ ] Add `evaluate()`, `toString()` and `toMathML()` methods for many more | ||
@@ -19,0 +18,0 @@ special functions. |
@@ -8,3 +8,3 @@ // ============================================================================= | ||
import {Obj} from '@mathigon/core'; | ||
import {CONSTANTS, escape, isSpecialFunction} from './symbols'; | ||
import {CONSTANTS, escape, isSpecialFunction, VOICE_STRINGS} from './symbols'; | ||
import {ExprError} from './errors'; | ||
@@ -51,2 +51,5 @@ | ||
/** Converts the expression to a MathML string. */ | ||
toVoice(custom: Obj<string> = {}) { return ''; } | ||
/** Converts the expression to a MathML string. */ | ||
toMathML(custom: MathMLMap = {}) { return ''; } | ||
@@ -67,2 +70,4 @@ } | ||
toVoice() { return '' + this.n; } | ||
toMathML() { return `<mn>${this.n}</mn>`; } | ||
@@ -93,2 +98,6 @@ } | ||
toString() { return this.i; } | ||
toVoice(custom: Obj<string> = {}) { | ||
return (this.i in custom) ? custom[this.i] : VOICE_STRINGS[this.i] || this.i; | ||
} | ||
} | ||
@@ -109,2 +118,4 @@ | ||
toVoice() { return this.s; } | ||
toMathML() { return `<mtext>${this.s}</mtext>`; } | ||
@@ -128,2 +139,6 @@ } | ||
toVoice(custom: Obj<string> = {}) { | ||
return (this.o in custom) ? custom[this.o] : VOICE_STRINGS[this.o] || this.o; | ||
} | ||
get functions() { return [this.o]; } | ||
@@ -130,0 +145,0 @@ |
@@ -7,5 +7,5 @@ // ============================================================================= | ||
import {unique, flatten, words, isOneOf, join} from '@mathigon/core'; | ||
import {unique, flatten, words, isOneOf, join, Obj} from '@mathigon/core'; | ||
import {collapseTerm} from './parser'; | ||
import {BRACKETS, escape, isSpecialFunction} from './symbols'; | ||
import {BRACKETS, escape, isSpecialFunction, VOICE_STRINGS} from './symbols'; | ||
import {ExprElement, ExprNumber, CustomFunction, MathMLMap, VarMap, ExprMap} from './elements'; | ||
@@ -15,3 +15,4 @@ import {ExprError} from './errors'; | ||
const PRECEDENCE = words('+ − * × · / ÷ // sup sub'); | ||
const PRECEDENCE = words('+ − * × · / ÷ // sup sub subsup'); | ||
const SUBSUP = words('sub sup subsup'); | ||
const COMMA = '<mo value="," lspace="0">,</mo>'; | ||
@@ -24,2 +25,3 @@ | ||
if (!PRECEDENCE.includes(expr.fn)) return false; | ||
if (SUBSUP.includes(expr.fn) && SUBSUP.includes(parentFn)) return true; | ||
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr.fn); | ||
@@ -112,2 +114,3 @@ } | ||
if (this.fn === 'sub') return args.join('_'); | ||
if (this.fn === 'subsup') return `${args[0]}_${args[1]}^${args[2]}`; | ||
@@ -175,2 +178,8 @@ if (words('+ * × · / = < > ≤ ≥ ≈').includes(this.fn)) | ||
if (this.fn === 'subsup') { | ||
const args1 = [addMRow(this.args[0], argsF[0]), | ||
addMRow(this.args[1], args[1]), addMRow(this.args[2], args[2])]; | ||
return `<msubsup>${args1.join('')}</msubsup>`; | ||
} | ||
if (isOneOf(this.fn, '(', '[', '{')) | ||
@@ -199,2 +208,26 @@ return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${argsF.join( | ||
} | ||
toVoice(custom: Obj<string> = {}) { | ||
const args = this.args.map(a => a.toVoice(custom)); | ||
const joined = args.join(' '); | ||
if (this.fn in custom && !custom[this.fn]) return joined; | ||
if (isOneOf(this.fn, '(', '[', '{')) return `open bracket ${joined} close bracket`; | ||
if (this.fn === 'sqrt') return `square root of ${joined}`; | ||
if (this.fn === '%') return `${joined} percent`; | ||
if (this.fn === '!') return `${joined} factorial`; | ||
if (this.fn === '/') return `${args[0]} over ${args[1]}`; | ||
if (this.fn === '//') return `${args[0]} divided by ${args[1]}`; | ||
if (this.fn === 'sup') return `${args[0]} to the power of ${args[1]}`; | ||
if (this.fn === 'sub') return joined; | ||
if (this.fn === 'subsup') return `${args[0]} ${args[1]} to the power of ${args[2]}`; | ||
if (VOICE_STRINGS[this.fn]) return args.join(` ${VOICE_STRINGS[this.fn]} `); | ||
// TODO Implement other cases | ||
if (isSpecialFunction(this.fn)) return `${this.fn} ${joined}`; | ||
return `${this.fn} of ${joined}`; | ||
} | ||
} | ||
@@ -226,3 +259,7 @@ | ||
toVoice(custom: Obj<string> = {}) { | ||
return this.items.map(i => i.toVoice(custom)).join(' '); | ||
} | ||
collapse() { return collapseTerm(this.items).collapse(); } | ||
} |
@@ -8,3 +8,3 @@ // ============================================================================= | ||
import {last, words} from '@mathigon/core'; | ||
import {SPECIAL_OPERATORS, SPECIAL_IDENTIFIERS, IDENTIFIER_SYMBOLS, OPERATOR_SYMBOLS, BRACKETS} from './symbols'; | ||
import {SPECIAL_OPERATORS, SPECIAL_IDENTIFIERS, IDENTIFIER_SYMBOLS, OPERATOR_SYMBOLS, BRACKETS, FUNCTION_NAMES} from './symbols'; | ||
import {ExprNumber, ExprIdentifier, ExprOperator, ExprSpace, ExprString, ExprElement} from './elements'; | ||
@@ -22,6 +22,8 @@ import {ExprFunction, ExprTerm} from './functions'; | ||
function createToken(buffer: string, type: TokenType) { | ||
if (type === TokenType.STR) return new ExprString(buffer); | ||
// Strings can be empty, but other types cannot. | ||
if (!buffer || !type) return undefined; | ||
if (type === TokenType.SPACE && buffer.length > 1) return new ExprSpace(); | ||
if (type === TokenType.STR) return new ExprString(buffer); | ||
@@ -122,3 +124,3 @@ if (type === TokenType.NUM) { | ||
function isOperator(expr: ExprElement, fns: string) { | ||
function isOperator(expr: ExprElement, fns: string): expr is ExprOperator { | ||
return expr instanceof ExprOperator && words(fns).includes(expr.o); | ||
@@ -132,3 +134,3 @@ } | ||
function findBinaryFunction(tokens: ExprElement[], fn: string, toFn?: string) { | ||
function findBinaryFunction(tokens: ExprElement[], fn: string) { | ||
if (isOperator(tokens[0], fn)) throw ExprError.startOperator(tokens[0]); | ||
@@ -148,5 +150,18 @@ if (isOperator(last(tokens), fn)) throw ExprError.endOperator(last(tokens)); | ||
const args = [removeBrackets(a), removeBrackets(b)]; | ||
tokens.splice(i - 1, 3, new ExprFunction(toFn || token.o, args)); | ||
i -= 2; | ||
const token2 = tokens[i + 2]; | ||
if (fn === '^ _' && isOperator(token, '_ ^') && isOperator(token2, '_ ^') && token.o !== token2.o) { | ||
// Special handling for subsup operator. | ||
const c = tokens[i + 3]; | ||
if (c instanceof ExprOperator) throw ExprError.consecutiveOperators(token2.o, c.o); | ||
const args = [removeBrackets(a), removeBrackets(b), removeBrackets(c)]; | ||
if (token.o === '^') [args[1], args[2]] = [args[2], args[1]]; | ||
tokens.splice(i - 1, 5, new ExprFunction('subsup', args)); | ||
i -= 4; | ||
} else { | ||
const fn = FUNCTION_NAMES[token.o] || token.o; | ||
const args = [removeBrackets(a), removeBrackets(b)]; | ||
tokens.splice(i - 1, 3, new ExprFunction(fn, args)); | ||
i -= 2; | ||
} | ||
} | ||
@@ -160,5 +175,3 @@ } | ||
function prepareTerm(tokens: ExprElement[]) { | ||
// TODO Combine sup and sub calls into a single supsub function. | ||
findBinaryFunction(tokens, '^', 'sup'); | ||
findBinaryFunction(tokens, '_', 'sub'); | ||
findBinaryFunction(tokens, '^ _'); | ||
findBinaryFunction(tokens, '/'); | ||
@@ -266,3 +279,3 @@ return makeTerm(tokens); | ||
findBinaryFunction(tokens, '= < > ≤ ≥'); | ||
findBinaryFunction(tokens, '// ÷', '/'); | ||
findBinaryFunction(tokens, '// ÷'); | ||
@@ -269,0 +282,0 @@ // Match multiplication operators. |
@@ -63,8 +63,2 @@ // ============================================================================= | ||
lArr: '⇐', | ||
CC: 'ℂ', | ||
NN: 'ℕ', | ||
QQ: 'ℚ', | ||
RR: 'ℝ', | ||
ZZ: 'ℤ' | ||
}; | ||
@@ -83,2 +77,3 @@ | ||
Omega: 'Ω', | ||
alpha: 'α', | ||
@@ -106,3 +101,9 @@ beta: 'β', | ||
psi: 'ψ', | ||
omega: 'ω' | ||
omega: 'ω', | ||
CC: 'ℂ', | ||
NN: 'ℕ', | ||
QQ: 'ℚ', | ||
RR: 'ℝ', | ||
ZZ: 'ℤ' | ||
}; | ||
@@ -120,2 +121,9 @@ | ||
export const FUNCTION_NAMES: Obj<string> = { | ||
'_': 'sub', | ||
'^': 'sup', | ||
'//': '/', | ||
'÷': '/' | ||
}; | ||
const ESCAPES: Obj<string> = { | ||
@@ -138,1 +146,20 @@ '<': '<', | ||
} | ||
export const VOICE_STRINGS: Obj<string> = { | ||
'+': 'plus', | ||
'−': 'minus', | ||
'·': 'times', | ||
'×': 'times', | ||
'/': 'over', | ||
'//': 'divided by', | ||
'%': 'percent', | ||
'!': 'factorial', | ||
'±': 'plus-minus', | ||
'=': 'equals', | ||
'≠': 'does not equal', | ||
'<': 'is less than', | ||
'>': 'is greater than', | ||
'≤': 'is less than or equal to', | ||
'≥': 'is greater than or equal to', | ||
'π': 'pi' | ||
}; |
@@ -39,2 +39,18 @@ // ============================================================================= | ||
tape('SupSub', (test) => { | ||
test.equal(mathML('a^2+1'), '<msup><mi>a</mi><mn>2</mn></msup><mo value="+">+</mo><mn>1</mn>'); | ||
test.equal(mathML('a^(2+1)'), '<msup><mi>a</mi><mrow><mn>2</mn><mo value="+">+</mo><mn>1</mn></mrow></msup>'); | ||
test.equal(mathML('a_2+1'), '<msub><mi>a</mi><mn>2</mn></msub><mo value="+">+</mo><mn>1</mn>'); | ||
test.equal(mathML('a_(2+1)'), '<msub><mi>a</mi><mrow><mn>2</mn><mo value="+">+</mo><mn>1</mn></mrow></msub>'); | ||
test.equal(mathML('a^2_1'), '<msubsup><mi>a</mi><mn>1</mn><mn>2</mn></msubsup>'); | ||
test.equal(mathML('a_1^2'), '<msubsup><mi>a</mi><mn>1</mn><mn>2</mn></msubsup>'); | ||
test.equal(mathML('(a_1)^2'), '<msup><mrow><mfenced><msub><mi>a</mi><mn>1</mn></msub></mfenced></mrow><mn>2</mn></msup>'); | ||
test.equal(mathML('(a^2)_1'), '<msub><mrow><mfenced><msup><mi>a</mi><mn>2</mn></msup></mfenced></mrow><mn>1</mn></msub>'); | ||
test.end(); | ||
}); | ||
tape('Custom Functions', (test) => { | ||
@@ -54,3 +70,2 @@ const options = { | ||
tape('Whitespace', (test) => { | ||
@@ -57,0 +72,0 @@ test.equal(mathML('a b'), '<mi>a</mi><mspace/><mi>b</mi>'); |
@@ -46,7 +46,8 @@ // ============================================================================= | ||
tape('super and subscripts', (test) => { | ||
test.equal(str('x^2_n'), '(x^2)_n'); | ||
test.equal(str('x^(2_n)'), 'x^2_n'); | ||
test.equal(str('x_n^2'), 'x_(n^2)'); | ||
test.equal(str('x_1^2 + 2^3_(n^2)'), 'x_(1^2) + (2^3)_(n^2)'); | ||
test.equal(str('x^(2_n)'), 'x^(2_n)'); | ||
test.equal(str('(x^2)_n'), '(x^2)_n'); | ||
test.equal(str('x_1^2 + 2^3_(n^2)'), 'x_1^2 + 2_(n^2)^3'); | ||
test.equal(str('x_(n+1) = x_n / 2'), 'x_(n + 1) = x_n / 2'); | ||
test.equal(str('x_n^2'), 'x_n^2'); | ||
test.equal(str('x^2_n'), 'x_n^2'); | ||
test.end(); | ||
@@ -53,0 +54,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
96480
19
2464
54
Updated@mathigon/core@^0.3.6
Updated@mathigon/fermat@^0.3.6