From 03cf9417317a6142b8eaf892e794af337df0671a Mon Sep 17 00:00:00 2001 From: Philipp Legner Date: Tue, 14 Jun 2022 22:32:43 +0100 Subject: [PATCH 01/16] Pair programming --- src/eval.ts | 14 +++++++++++- src/functions.ts | 56 ++++++++++++++++++++++++++++++++---------------- src/parser.ts | 3 ++- src/symbols.ts | 2 +- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/eval.ts b/src/eval.ts index e5f8472d..182f058e 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -5,7 +5,7 @@ import {total} from '@mathigon/core'; -import {gcd, isBetween, lcm} from '@mathigon/fermat'; +import {gcd, isBetween, lcm, nearlyEquals} from '@mathigon/fermat'; import {SpecialFunction} from './symbols'; const OPERATORS = ['add', 'sub', 'mul', 'div', 'sup'] as const; @@ -37,6 +37,12 @@ export const hasZero = (a: Interval) => contains(a, 0); // ----------------------------------------------------------------------------- // Standard Evaluation +export const evaluateRel: Record<'='|'<'|'>', (...args: number[]) => boolean> = { + '=': (a, b) => nearlyEquals(a, b), + '<': (a, b) => a < b, + '>': (a, b) => a > b +}; + export const evaluate: Record number> = { add: (...args) => args.reduce((a, b) => a + b, 0), sub: (...args) => (args.length > 1) ? args[0] - args[1] : -args[0], @@ -119,6 +125,12 @@ function intervalMod(a: Interval, m = TWO_PI): Interval { // ----------------------------------------------------------------------------- // Interval Evaluation +export const intervalRel: Record<'='|'<'|'>', (...args: Interval[]) => boolean> = { + '=': (a, b) => (contains(a, b[0]) && contains(a, b[1])) || (contains(b, a[0]) && contains(b, a[1])), + '<': (a, b) => a[1] < b[0], + '>': (a, b) => a[1] < b[0] +}; + export const interval: Record Interval> = { add: (...args) => int(total(args.map(a => a[0])), total(args.map(a => a[1]))), sub: (a, b) => b !== undefined ? int(a[0] - b[1], a[1] - b[0]) : int(-a[1], -a[0]), diff --git a/src/functions.ts b/src/functions.ts index 3d0651b9..25099d98 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -5,10 +5,10 @@ import {flatten, isOneOf, join, repeat, unique, words} from '@mathigon/core'; -import {evaluate, interval, Interval} from './eval'; +import {evaluate, evaluateRel, interval, Interval, intervalRel} from './eval'; import {collapseTerm} from './parser'; -import {BRACKETS, escape, isSpecialFunction, VOICE_STRINGS} from './symbols'; -import {CustomFunction, ExprElement, ExprMap, ExprNumber, MathMLMap, VarMap} from './elements'; +import {BRACKETS, escape, isSpecialFunction, SpecialFunction, VOICE_STRINGS} from './symbols'; +import {ExprElement, ExprMap, ExprNumber, MathMLMap, VarMap} from './elements'; import {ExprError} from './errors'; @@ -40,12 +40,29 @@ function supVoice(a: string) { export class ExprFunction extends ExprElement { + private operator?: 'add'|'sub'|'mul'|'div'|'sup'|SpecialFunction; constructor(readonly fn: string, readonly args: ExprElement[] = []) { super(); + this.operator = fn === '+' ? 'add' : fn === '−' ? 'sub' : + '*·×'.includes(fn) ? 'mul' : fn === '/' ? 'div' : fn === 'sup' ? 'sup' : + isSpecialFunction(fn) ? fn : undefined; } evaluate(vars: VarMap = {}) { + if (this.fn === '{') { + // Piecewise functions + for (let i = 0; i < this.args.length; i += 1) { + if (this.args[i + 1].evaluate(vars)) return this.args[i].evaluate(); + } + return NaN; + } else if (this.fn === '(') { + return this.args[0].evaluate(vars); + } else if (this.fn === '[') { + // TODO Evaluate matrices + return NaN; + } + const args = this.args.map(a => a.evaluate(vars)); if (this.fn in vars) { @@ -55,17 +72,24 @@ export class ExprFunction extends ExprElement { throw ExprError.uncallableExpression(this.fn); } - if (this.fn === '+') return evaluate.add(...args); - if (this.fn === '−') return evaluate.sub(...args); - if (['*', '·', '×'].includes(this.fn)) return evaluate.mul(...args); - if (this.fn === '/') return evaluate.div(...args); - if (this.fn === 'sup') return evaluate.sup(...args); - if (isSpecialFunction(this.fn)) return evaluate[this.fn](...args); - if (this.fn === '(') return args[0]; + if ('=<>'.includes(this.fn)) return evaluateRel[this.fn as '='|'<'|'>'](...args) ? 1 : 0; + if (this.operator) return evaluate[this.operator](...args); throw ExprError.undefinedFunction(this.fn); } interval(vars: VarMap = {}): Interval { + if (this.fn === '{') { + for (let i = 0; i < this.args.length; i += 1) { + if (this.args[i + 1].interval(vars)[0]) return this.args[i].interval(vars); + } + return [NaN, NaN]; + } else if (this.fn === '(') { + return this.args[0].interval(vars); + } else if (this.fn === '[') { + // TODO Evaluate matrices + return [NaN, NaN]; + } + const args = this.args.map(a => a.interval(vars)); if (this.fn in vars) { @@ -76,13 +100,8 @@ export class ExprFunction extends ExprElement { throw ExprError.uncallableExpression(this.fn); } - if (this.fn === '+') return interval.add(...args); - if (this.fn === '−') return interval.sub(...args); - if (['*', '·', '×'].includes(this.fn)) return interval.mul(...args); - if (this.fn === '/') return interval.div(...args); - if (this.fn === 'sup') return interval.sup(...args); - if (isSpecialFunction(this.fn)) return interval[this.fn](...args); - if (this.fn === '(') return args[0]; + if ('=<>'.includes(this.fn)) return intervalRel[this.fn as '='|'<'|'>'](...args) ? [1, 1] : [0, 0]; + if (this.operator) return interval[this.operator](...args); throw ExprError.undefinedFunction(this.fn); } @@ -189,7 +208,8 @@ export class ExprFunction extends ExprElement { } if (isOneOf(this.fn, '(', '[', '{')) { - return `${argsF.join(COMMA)}`; + const join = this.fn === '(' ? COMMA : ''; + return `${argsF.join(join)}`; } if (isOneOf(this.fn, '!', '%')) { diff --git a/src/parser.ts b/src/parser.ts index e2c65fe3..87615d77 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -200,8 +200,9 @@ export function matchBrackets(tokens: ExprElement[], context?: {variables?: stri const fnName = isFn ? (term.pop() as ExprIdentifier).i : (closed![0] as ExprOperator).o; // Support multiple arguments for function calls. - const args = splitArray(closed!.slice(1), a => isOperator(a, ',')); + const args = splitArray(closed!.slice(1), a => isOperator(a, ', ;')); term.push(new ExprFunction(fnName, args.map(prepareTerm))); + // TODO Tell ExprFunction how many rows/columns there are in the matrix } else if (isOperator(t, '( [ {')) { stack.push([t]); diff --git a/src/symbols.ts b/src/symbols.ts index 7c5f5523..3a23d6a6 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -112,7 +112,7 @@ const UPPERCASE = ALPHABET.toUpperCase().split(''); const GREEK = Object.values(SPECIAL_IDENTIFIERS); export const IDENTIFIER_SYMBOLS = [...LOWERCASE, ...UPPERCASE, ...GREEK, '$']; -const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…°•∥⊥\'∠:%∼△'; +const SIMPLE_SYMBOLS = '|()[]{}÷,;!<>=*/+-–−~^_…°•∥⊥\'∠:%∼△'; const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS); export const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS]; From 3cd8a521e7f684c9fae9b7e776f071515fa29ceb Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 19 Jul 2022 00:03:13 +0200 Subject: [PATCH 02/16] Parse and tokenize underover correctly --- src/parser.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 87615d77..5ca25327 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -130,6 +130,8 @@ function findBinaryFunction(tokens: ExprElement[], fn: string) { if (isOperator(tokens[0], fn)) throw ExprError.startOperator(tokens[0]); if (isOperator(last(tokens), fn)) throw ExprError.endOperator(last(tokens)); + const mUnderOver = ['∑', '∏']; + for (let i = 1; i < tokens.length - 1; ++i) { if (!isOperator(tokens[i], fn)) continue; const token = tokens[i] as ExprOperator; @@ -137,7 +139,8 @@ function findBinaryFunction(tokens: ExprElement[], fn: string) { const a = tokens[i - 1]; const b = tokens[i + 1]; - if (a instanceof ExprOperator) { + // Sigma is ExprOperator, next to '_' also an operator + if (a instanceof ExprOperator && !mUnderOver.includes(a.o)) { throw ExprError.consecutiveOperators(a.o, token.o); } if (b instanceof ExprOperator) { @@ -151,7 +154,8 @@ function findBinaryFunction(tokens: ExprElement[], fn: string) { 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)); + const mathMLFn = mUnderOver.includes(a.toString()) ? 'underover' : 'subsup'; + tokens.splice(i - 1, 5, new ExprFunction(mathMLFn, args)); i -= 4; } else { From dabc8726a463edf26b2216435e10e5de290ce49b Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 19 Jul 2022 00:04:34 +0200 Subject: [PATCH 03/16] Blocker for `closed` being undefined --- src/parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 5ca25327..da286363 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -195,13 +195,14 @@ export function matchBrackets(tokens: ExprElement[], context?: {variables?: stri } const closed = stack.pop(); + if (closed === undefined) continue; const term = last(stack); const lastTerm = last(term); const isFn = isOperator(t, ')') && lastTerm instanceof ExprIdentifier && !safeVariables.includes(lastTerm.i); - const fnName = isFn ? (term.pop() as ExprIdentifier).i : (closed![0] as ExprOperator).o; + const fnName = isFn ? (term.pop() as ExprIdentifier).i : (closed[0] as ExprOperator).o; // Support multiple arguments for function calls. const args = splitArray(closed!.slice(1), a => isOperator(a, ', ;')); From a9b60762349adbfacb5477abfc8d4440949dcf58 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 19 Jul 2022 00:05:25 +0200 Subject: [PATCH 04/16] Filter and re-add semicolons We do not want to create separate nested args. --- src/parser.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index da286363..1bc44cf9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -205,9 +205,17 @@ export function matchBrackets(tokens: ExprElement[], context?: {variables?: stri const fnName = isFn ? (term.pop() as ExprIdentifier).i : (closed[0] as ExprOperator).o; // Support multiple arguments for function calls. - const args = splitArray(closed!.slice(1), a => isOperator(a, ', ;')); + const withinBrackets = closed.slice(1); + const args = splitArray(withinBrackets, a => isOperator(a, ', ;')); + + // Conditionally re-add semicolon row break markers for matrices + for (let i = 0; i < withinBrackets.length; i++) { + if (withinBrackets[i].toString() === ';' && isOperator(t, '] }')) { + args.splice(i - 1, 0, [withinBrackets[i]]); + } + } + term.push(new ExprFunction(fnName, args.map(prepareTerm))); - // TODO Tell ExprFunction how many rows/columns there are in the matrix } else if (isOperator(t, '( [ {')) { stack.push([t]); From f67ad302d43c120d7f244c8a030adf88cf292587 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 19 Jul 2022 00:06:36 +0200 Subject: [PATCH 05/16] Ignore semi-colons when converting to MathML Since these have been re-added during parsing, we now ignore them. --- src/functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions.ts b/src/functions.ts index 25099d98..e1a85c12 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -153,8 +153,8 @@ export class ExprFunction extends ExprElement { } toMathML(custom: MathMLMap = {}) { - const args = this.args.map(a => a.toMathML(custom)); - const argsF = this.args.map((a, i) => addMFence(a, this.fn, args[i])); + const args = this.args.filter(a => a.toString() !== ';').map(a => a.toMathML(custom)); + const argsF = this.args.filter(a => a.toString() !== ';').map((a, i) => addMFence(a, this.fn, args[i])); if (this.fn in custom) { const argsX = args.map((a, i) => ({ From 6f6434e1caafc126088dd9c023781eda8ce3f493 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 19 Jul 2022 00:07:28 +0200 Subject: [PATCH 06/16] Count and pass `rows` attribute to `mfenced` --- src/functions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/functions.ts b/src/functions.ts index e1a85c12..a6cff230 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -209,7 +209,8 @@ export class ExprFunction extends ExprElement { if (isOneOf(this.fn, '(', '[', '{')) { const join = this.fn === '(' ? COMMA : ''; - return `${argsF.join(join)}`; + const rows = this.args.filter(r => r.toString() === ';').length + 1; + return `${argsF.join(join)}`; } if (isOneOf(this.fn, '!', '%')) { From 7f024ebcec85f3d29035d3d619bea2396219788f Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 19 Jul 2022 00:07:53 +0200 Subject: [PATCH 07/16] `underover` MathML tag --- src/functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions.ts b/src/functions.ts index a6cff230..0600643c 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -201,10 +201,10 @@ export class ExprFunction extends ExprElement { return `${args1.join('')}`; } - if (this.fn === 'subsup') { + if (this.fn === 'subsup' || this.fn === 'underover') { const args1 = [addMRow(this.args[0], argsF[0]), addMRow(this.args[1], args[1]), addMRow(this.args[2], args[2])]; - return `${args1.join('')}`; + return `${args1.join('')}`; } if (isOneOf(this.fn, '(', '[', '{')) { From bf8ffd905f819da3b07d07e3d03cf9f39d9608be Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Jul 2022 12:23:54 +0200 Subject: [PATCH 08/16] `toVoice` for nth roots --- src/functions.ts | 2 ++ test/voice-test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/functions.ts b/src/functions.ts index 0600643c..da768cfd 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -250,6 +250,8 @@ export class ExprFunction extends ExprElement { // Maybe `open bracket ${joined} close bracket` ? if (this.fn === 'sqrt') return `square root of ${joined}`; + if (this.fn === 'root' && args[1] === '3') return `cubic root of ${args[0]}`; + if (this.fn === 'root' && args[1] !== '3') return `${args[1]}th root of ${[args[0]]}`; if (this.fn === '%') return `${joined} percent`; if (this.fn === '!') return `${joined} factorial`; if (this.fn === '/') return `${args[0]} over ${args[1]}`; diff --git a/test/voice-test.ts b/test/voice-test.ts index e204281c..2d01fe1c 100644 --- a/test/voice-test.ts +++ b/test/voice-test.ts @@ -13,6 +13,8 @@ const voice = (src: string) => Expression.parse(src).toVoice(); tape('Basic Voice', (test) => { test.equal(voice('sqrt(5)'), 'square root of 5'); + test.equal(voice('root(27, 3)'), 'cubic root of 27'); + test.equal(voice('root(256, 4)'), '4th root of 256'); test.equal(voice('a * b'), '_a_ times _b_'); test.equal(voice('(a * b)'), '_a_ times _b_'); test.equal(voice('a^b'), '_a_ to the power of _b_'); From e981e77ffe3217d114bc2fb2489035a54a917de0 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 22 Jul 2022 14:46:27 +0200 Subject: [PATCH 09/16] Split brackets and matrix to MathML, include rows Only pass row attribute to matrices --- src/functions.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/functions.ts b/src/functions.ts index da768cfd..8da34bdb 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -207,10 +207,13 @@ export class ExprFunction extends ExprElement { return `${args1.join('')}`; } - if (isOneOf(this.fn, '(', '[', '{')) { - const join = this.fn === '(' ? COMMA : ''; + if (this.fn === '(') { + return `${argsF.join(COMMA)}`; + } + + if (isOneOf(this.fn, '[', '{')) { const rows = this.args.filter(r => r.toString() === ';').length + 1; - return `${argsF.join(join)}`; + return `${argsF.join('')}`; } if (isOneOf(this.fn, '!', '%')) { From cf5bf67ee0fc50536443502eadb72deb258607db Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 25 Jul 2022 14:34:37 +0200 Subject: [PATCH 10/16] Clarify root comment --- src/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.ts b/src/functions.ts index 8da34bdb..16b8291b 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -188,7 +188,7 @@ export class ExprFunction extends ExprElement { if (this.fn === 'sqrt') return `${argsF[0]}`; if (isOneOf(this.fn, '/', 'root')) { - // Fractions or square roots don't have brackets around their arguments + // Fractions or roots don't have brackets around their arguments const el = (this.fn === '/' ? 'mfrac' : 'mroot'); const args1 = this.args.map((a, i) => addMRow(a, args[i])); return `<${el}>${args1.join('')}`; From 79bfcefaabba7c6360bd3959afdf8eba0a78f91b Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 25 Jul 2022 14:34:50 +0200 Subject: [PATCH 11/16] Consider integrals as under over elements --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 1bc44cf9..b90ed6c4 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -130,7 +130,7 @@ function findBinaryFunction(tokens: ExprElement[], fn: string) { if (isOperator(tokens[0], fn)) throw ExprError.startOperator(tokens[0]); if (isOperator(last(tokens), fn)) throw ExprError.endOperator(last(tokens)); - const mUnderOver = ['∑', '∏']; + const mUnderOver = ['∑', '∏', '∫']; for (let i = 1; i < tokens.length - 1; ++i) { if (!isOperator(tokens[i], fn)) continue; From 0918879e0a7a7ee5f92c30f294462d3362b13f01 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 25 Jul 2022 14:35:30 +0200 Subject: [PATCH 12/16] Update MathML tests Matrices, piecewise, under over tests --- test/mathml-test.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/test/mathml-test.ts b/test/mathml-test.ts index 0c5f6e37..d81e16df 100644 --- a/test/mathml-test.ts +++ b/test/mathml-test.ts @@ -128,8 +128,6 @@ tape('Roots', (test) => { tape('Groupings', (test) => { test.equal(mathML('(a+b)'), 'a+b'); - test.equal(mathML('{a+b}'), - 'a+b'); test.equal(mathML('abs(a+b)'), 'a+b'); test.equal(mathML('a,b,c'), @@ -144,3 +142,31 @@ tape('Groupings', (test) => { 'eiτ=1'); test.end(); }); + +tape('Matrices and Piecewise', (test) => { + test.equal(mathML('[a, b, c]'), + 'abc'); + test.equal(mathML('[a, b; c, d]'), + 'abcd'); + test.equal(mathML('[a; b; c]'), + 'abc'); + test.equal(mathML('{a, b, c}'), + 'abc'); + test.equal(mathML('{a, b; c, d}'), + 'abcd'); + test.equal(mathML('{a; b; c}'), + 'abc'); + test.equal(mathML('{a+b}'), + 'a+b'); + test.end(); +}); + +tape('Under Over', (test) => { + test.equal(mathML('∑_(i = 0)^2 i'), + 'i=02i'); + test.equal(mathML('∫_a^b c'), + 'abc'); + test.equal(mathML('∫_0^1 xdx'), + '01xdx'); + test.end(); +}); From e254d220ac6f24aae273574298becdd9277db58c Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 25 Jul 2022 15:27:23 +0200 Subject: [PATCH 13/16] Nth root tests --- test/evaluate-test.ts | 2 ++ test/mathml-test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/evaluate-test.ts b/test/evaluate-test.ts index a0ed0a15..ee463613 100644 --- a/test/evaluate-test.ts +++ b/test/evaluate-test.ts @@ -27,6 +27,8 @@ tape('Functions', (test) => { test.equal(value('2 ^ 3'), 8); test.equal(value('4 / 2'), 2); test.equal(value('sqrt(81)'), 9); + test.equal(value('root(27, 3)'), 3); + test.equal(value('root(81, 4)'), 3); test.equal(Math.round(value('sin(pi)')), 0); test.end(); }); diff --git a/test/mathml-test.ts b/test/mathml-test.ts index d81e16df..3e9f492d 100644 --- a/test/mathml-test.ts +++ b/test/mathml-test.ts @@ -163,7 +163,7 @@ tape('Matrices and Piecewise', (test) => { tape('Under Over', (test) => { test.equal(mathML('∑_(i = 0)^2 i'), - 'i=02i'); + 'i=02i'); test.equal(mathML('∫_a^b c'), 'abc'); test.equal(mathML('∫_0^1 xdx'), From 5e6d12c2cb42f647e844b9b8f8b797859fcd8e49 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 5 Aug 2022 17:05:38 +0200 Subject: [PATCH 14/16] Voice accessibility for under over elements --- src/functions.ts | 12 +++++++++++- test/voice-test.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/functions.ts b/src/functions.ts index 16b8291b..45a9974f 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -263,7 +263,17 @@ export class ExprFunction extends ExprElement { if (this.fn === 'sub') return joined; if (this.fn === 'subsup') return `${args[0]} ${args[1]} ${supVoice(args[2])}`; if (this.fn === 'sup') return `${args[0]} ${supVoice(args[1])}`; - + if (this.fn === 'underover') { + let symbol = '?'; + if (args[0] === '∑') { + symbol = 'sum'; + } else if (args[0] === '∫') { + symbol = 'integral'; + } else if (args[0] === '∏') { + symbol = 'product'; + } + return `${symbol} from ${args[1]} to ${args[2]} of`; + } if (VOICE_STRINGS[this.fn]) return args.join(` ${VOICE_STRINGS[this.fn]} `); // TODO Implement other cases diff --git a/test/voice-test.ts b/test/voice-test.ts index 2d01fe1c..c6aa539b 100644 --- a/test/voice-test.ts +++ b/test/voice-test.ts @@ -25,5 +25,14 @@ tape('Basic Voice', (test) => { test.equal(voice('a/b'), '_a_ over _b_'); test.equal(voice('a//b'), '_a_ divided by _b_'); test.equal(voice('(a + b)/cc'), '_a_ plus _b_ over cc'); + test.equal(voice('∑_(i=1)^(10)'), 'sum from _i_ equals 1 to 10 of'); + test.equal(voice('∑_(i=1)^(10)i'), 'sum from _i_ equals 1 to 10 of _i_'); + test.equal(voice('∑_(i=1)^(10)i + 1'), 'sum from _i_ equals 1 to 10 of _i_ plus 1'); + test.equal(voice('∏_(i=1)^(10)'), 'product from _i_ equals 1 to 10 of'); + test.equal(voice('∏_(i=1)^(10)i'), 'product from _i_ equals 1 to 10 of _i_'); + test.equal(voice('∏_(i=1)^(10)i + 1'), 'product from _i_ equals 1 to 10 of _i_ plus 1'); + test.equal(voice('∫_(i=1)^(10)'), 'integral from _i_ equals 1 to 10 of'); + test.equal(voice('∫_(i=1)^(10)i'), 'integral from _i_ equals 1 to 10 of _i_'); + test.equal(voice('∫_(i=1)^(10)i + 1'), 'integral from _i_ equals 1 to 10 of _i_ plus 1'); test.end(); }); From 93674032133b4c82977dcc0c0b1e143df607102a Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 5 Aug 2022 17:08:36 +0200 Subject: [PATCH 15/16] TODO: evaluate underover functions --- src/functions.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/functions.ts b/src/functions.ts index 45a9974f..2390ac42 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -73,6 +73,24 @@ export class ExprFunction extends ExprElement { } if ('=<>'.includes(this.fn)) return evaluateRel[this.fn as '='|'<'|'>'](...args) ? 1 : 0; + // TODO: evaluate underover functions + // if (this.fn === 'underover') { + // if (this.args[0].toString() === '∑') { + // let sum = 0; + // for (let i = args[1]; i < args[2]; i++) { + // sum += i; + // } + // return sum; + // } + // if (this.args[0].toString() === '∏') { + // let prod = 1; + // for (let i = args[1]; i < args[2]; i++) { + // prod *= i; + // } + // return prod; + // } + // } + // if (this.operator) return evaluate[this.operator](...args); throw ExprError.undefinedFunction(this.fn); } From 5e07209194998162a333382ad3984a4c2aabf669 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 5 Aug 2022 17:13:23 +0200 Subject: [PATCH 16/16] Clarifying comment --- src/functions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/functions.ts b/src/functions.ts index 2390ac42..b44024fd 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -171,6 +171,7 @@ export class ExprFunction extends ExprElement { } toMathML(custom: MathMLMap = {}) { + // Remove matrix/piecewise row breaks by filtering semi-colons. const args = this.args.filter(a => a.toString() !== ';').map(a => a.toMathML(custom)); const argsF = this.args.filter(a => a.toString() !== ';').map((a, i) => addMFence(a, this.fn, args[i]));