// ============================================================================= // Hilbert.js | Functions // (c) Mathigon // ============================================================================= import {flatten, isOneOf, join, repeat, unique, words} from '@mathigon/core'; import {evaluate, interval, Interval} from './eval'; import {collapseTerm} from './parser'; import {BRACKETS, escape, isSpecialFunction, VOICE_STRINGS} from './symbols'; import {ExprElement, ExprMap, ExprNumber, MathMLMap, VarMap} from './elements'; import {ExprError} from './errors'; const PRECEDENCE = words('+ − * × · / ÷ // sup sub subsup'); const SUBSUP = words('sub sup subsup'); const COMMA = ','; function needsBrackets(expr: ExprElement, parentFn: string): boolean { if (!PRECEDENCE.includes(parentFn)) return false; if (expr instanceof ExprTerm) return true; if (!(expr instanceof ExprFunction)) return false; 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); } function addMFence(expr: ExprElement, fn: string, string: string) { return needsBrackets(expr, fn) ? `${string}` : string; } function addMRow(expr: ExprElement, string: string) { const needsRow = (expr instanceof ExprTerm) || (expr instanceof ExprFunction); return needsRow ? `${string}` : string; } function supVoice(a: string) { return a === '2' ? 'squared' : a === '3' ? 'cubed' : `to the power of ${a}`; } export class ExprFunction extends ExprElement { constructor(readonly fn: string, readonly args: ExprElement[] = []) { super(); } evaluate(vars: VarMap = {}) { const args = this.args.map(a => a.evaluate(vars)); if (this.fn in vars) { const fn = vars[this.fn]; if (typeof fn === 'function') return fn(...args); if (typeof fn === 'number' && args.length === 1) return evaluate.mul(fn, args[0]); 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]; throw ExprError.undefinedFunction(this.fn); } interval(vars: VarMap = {}): Interval { const args = this.args.map(a => a.interval(vars)); if (this.fn in vars) { const fn = vars[this.fn]; if (typeof fn === 'function') return repeat(fn(...args.map(a => a[0])), 2) as Interval; if (typeof fn === 'number' && args.length === 1) return interval.mul([fn, fn], args[0]); if (Array.isArray(fn) && args.length === 1) return interval.mul(fn, args[0]); 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]; throw ExprError.undefinedFunction(this.fn); } substitute(vars: ExprMap = {}) { return new ExprFunction(this.fn, this.args.map(a => a.substitute(vars))); } collapse() { if (this.fn === '(') return this.args[0].collapse(); return new ExprFunction(this.fn, this.args.map(a => a.collapse())); } get simplified() { // TODO Write CAS simplification algorithms return this; } get variables() { return unique(flatten(this.args.map(a => a.variables))); } get functions() { return unique([this.fn, ...flatten(this.args.map(a => a.functions))]); } toString() { const args = this.args.map(a => needsBrackets(a, this.fn) ? `(${a.toString()})` : a.toString()); if (this.fn === '−') { return args.length > 1 ? args.join(' − ') : `−${args[0]}`; } if (this.fn === 'sup') return args.join('^'); if (this.fn === 'sub') return args.join('_'); if (this.fn === 'subsup') return `${args[0]}_${args[1]}^${args[2]}`; if (words('+ * × · / = < > ≤ ≥ ≈ ≟ ≠').includes(this.fn)) { return args.join(` ${this.fn} `); } if (isOneOf(this.fn, '(', '[', '{')) { return this.fn + this.args.join(', ') + BRACKETS[this.fn]; } if (isOneOf(this.fn, '!', '%')) return args[0] + this.fn; // TODO Implement other functions return `${this.fn}(${args.join(', ')})`; } 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])); if (this.fn in custom) { const argsX = args.map((a, i) => ({ toString: () => a, val: this.args[i] })); return custom[this.fn](...argsX); } if (this.fn === '−') { return argsF.length > 1 ? argsF.join('') : `${argsF[0]}`; } if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥', '≈', '≟', '≠')) { const fn = escape(this.fn); return argsF.join(`${fn}`); } if (isOneOf(this.fn, '*', '×', '·')) { let str = argsF[0]; for (let i = 1; i < argsF.length - 1; ++i) { // We only show the × symbol between consecutive numbers. const showTimes = (this.args[0] instanceof ExprNumber && this.args[1] instanceof ExprNumber); str += (showTimes ? `×` : '') + argsF[1]; } return str; } if (this.fn === '//') return argsF.join(`/`); if (this.fn === 'sqrt') return `${argsF[0]}`; if (isOneOf(this.fn, '/', 'root')) { // Fractions or square 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('')}`; } if (isOneOf(this.fn, 'sup', 'sub')) { // Sup and sub only have brackets around their first argument. const args1 = [addMRow(this.args[0], argsF[0]), addMRow(this.args[1], args[1])]; return `${args1.join('')}`; } 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 `${args1.join('')}`; } if (isOneOf(this.fn, '(', '[', '{')) { return `${argsF.join(COMMA)}`; } if (isOneOf(this.fn, '!', '%')) { return `${argsF[0]}${this.fn}`; } if (this.fn === 'abs') { return `${argsF.join(COMMA)}`; } if (this.fn === 'bar') { return `${addMRow(this.args[0], argsF[0])}`; } if (this.fn === 'vec') { return `${addMRow(this.args[0], argsF[0])}`; } // TODO Implement other functions const variant = isSpecialFunction(this.fn) ? ' mathvariant="normal"' : ''; return `${this.fn}${argsF.join(COMMA)}`; } toVoice(custom: MathMLMap = {}) { const args = this.args.map(a => a.toVoice(custom)); const joined = args.join(' '); if (this.fn in custom) { const argsX = args.map((a, i) => ({ toString: () => a, val: this.args[i] })); return custom[this.fn](...argsX); } if (isOneOf(this.fn, '(', '[', '{')) return joined; // Maybe `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 === '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 (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}`; } } // ----------------------------------------------------------------------------- export class ExprTerm extends ExprElement { constructor(readonly items: ExprElement[]) { super(); } evaluate(vars: VarMap = {}) { return this.collapse().evaluate(vars); } interval(vars: VarMap = {}) { return this.collapse().interval(vars); } substitute(vars: ExprMap = {}) { return this.collapse().substitute(vars); } get simplified() { return this.collapse().simplified; } get variables() { return unique(join(...this.items.map(i => i.variables))); } get functions() { return this.collapse().functions; } toString() { return this.items.map(i => i.toString()).join(' '); } toMathML(custom: MathMLMap = {}) { return this.items.map(i => i.toMathML(custom)).join(''); } toVoice(custom: MathMLMap = {}) { return this.items.map(i => i.toVoice(custom)).join(' '); } collapse() { return collapseTerm(this.items).collapse(); } }