From f8cb7ffdabd3bef6ae5d85a80c326e152396e985 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 11 Apr 2026 08:12:30 +0300 Subject: [PATCH] remove mathjs --- app/javascript/submission_form/calculator.js | 874 ++++++++++++++++++ .../submission_form/formula_areas.vue | 29 +- .../submission_form/payment_step.vue | 29 +- package.json | 1 - yarn.lock | 67 -- 5 files changed, 878 insertions(+), 122 deletions(-) create mode 100644 app/javascript/submission_form/calculator.js diff --git a/app/javascript/submission_form/calculator.js b/app/javascript/submission_form/calculator.js new file mode 100644 index 00000000..55cc185f --- /dev/null +++ b/app/javascript/submission_form/calculator.js @@ -0,0 +1,874 @@ +/* + * Portions of this file are derived from the Dentaku gem + * https://github.com/rubysolo/dentaku + * + * MIT License + * + * Copyright (c) 2012 Solomon White + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Modifications and JavaScript port + * Copyright (c) 2026 DocuSeal, LLC + */ + +class CalculatorError extends Error { constructor (msg) { super(msg); this.name = 'CalculatorError' } } +class ParseError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ParseError' } } +class TokenizerError extends CalculatorError { constructor (msg) { super(msg); this.name = 'TokenizerError' } } +class ArgumentError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ArgumentError' } } +class ZeroDivisionError extends CalculatorError { constructor () { super('divided by 0'); this.name = 'ZeroDivisionError' } } +class UnboundVariableError extends CalculatorError { + constructor (vars) { + super('no value provided for variables: ' + vars.join(', ')) + this.name = 'UnboundVariableError' + this.unbound = vars + } +} + +class Token { + constructor (category, value, raw) { + this.category = category + this.value = value + this.raw = raw == null ? String(value) : String(raw) + } + + is (cat) { return this.category === cat } +} + +const OPERATOR_NAMES = { + '^': 'pow', + '+': 'add', + '-': 'subtract', + '*': 'multiply', + '/': 'divide', + '%': 'mod', + '|': 'bitor', + '&': 'bitand', + '<<': 'bitshiftleft', + '>>': 'bitshiftright' +} +const COMPARATOR_NAMES = { + '<=': 'le', + '>=': 'ge', + '!=': 'ne', + '<>': 'ne', + '<': 'lt', + '>': 'gt', + '==': 'eq', + '=': 'eq' +} +const GROUPING_NAMES = { '(': 'open', ')': 'close', ',': 'comma' } + +function buildScanners (caseSensitive) { + return [ + (s) => { + const m = /^\s+/.exec(s) + return m ? { tokens: [], length: m[0].length } : null + }, + (s) => { + const m = /^null\b/i.exec(s) + return m ? { tokens: [new Token('null', null, m[0])], length: m[0].length } : null + }, + (s) => { + const m = /^((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)\b/.exec(s) + if (!m) return null + return { tokens: [new Token('numeric', Number(m[0]), m[0])], length: m[0].length } + }, + (s) => { + const m = /^0x[0-9a-f]+\b/i.exec(s) + if (!m) return null + return { tokens: [new Token('numeric', parseInt(m[0].slice(2), 16), m[0])], length: m[0].length } + }, + (s) => { + const m = /^"([^"]*)"/.exec(s) + if (!m) return null + return { tokens: [new Token('string', m[1], m[0])], length: m[0].length } + }, + (s) => { + const m = /^'([^']*)'/.exec(s) + if (!m) return null + return { tokens: [new Token('string', m[1], m[0])], length: m[0].length } + }, + (s, last) => { + if (s[0] !== '-') return null + const ok = last == null || + last.is('operator') || + last.is('comparator') || + last.is('combinator') || + last.value === 'open' || + last.value === 'comma' || + last.value === 'array_start' + if (!ok) return null + return { tokens: [new Token('operator', 'negate', '-')], length: 1 } + }, + (s) => { + const m = /^(and|or|&&|\|\|)\s/i.exec(s) + if (!m) return null + const raw = m[1].toLowerCase() + const name = raw === '&&' ? 'and' : raw === '||' ? 'or' : raw + return { tokens: [new Token('combinator', name, m[0])], length: m[0].length } + }, + (s) => { + const m = /^(<<|>>|\^|\+|-|\*|\/|%|\||&)/.exec(s) + if (!m) return null + return { tokens: [new Token('operator', OPERATOR_NAMES[m[0]], m[0])], length: m[0].length } + }, + (s) => { + const m = /^(\(|\)|,)/.exec(s) + if (!m) return null + return { tokens: [new Token('grouping', GROUPING_NAMES[m[0]], m[0])], length: m[0].length } + }, + (s) => { + if (s[0] === '{') return { tokens: [new Token('array', 'array_start', '{')], length: 1 } + if (s[0] === '}') return { tokens: [new Token('array', 'array_end', '}')], length: 1 } + return null + }, + (s) => { + const m = /^(<=|>=|!=|<>|==|=|<|>)/.exec(s) + if (!m) return null + return { tokens: [new Token('comparator', COMPARATOR_NAMES[m[0]], m[0])], length: m[0].length } + }, + (s) => { + const m = /^(true|false)\b/i.exec(s) + if (!m) return null + return { tokens: [new Token('logical', m[0].toLowerCase() === 'true', m[0])], length: m[0].length } + }, + (s) => { + const m = /^(\w+!?)\s*\(/.exec(s) + if (!m) return null + const name = m[1].toLowerCase() + return { + tokens: [ + new Token('function', name, m[1]), + new Token('grouping', 'open', '(') + ], + length: m[0].length + } + }, + (s) => { + const m = /^[A-Za-z_][\w.]*\b/.exec(s) + if (!m) return null + const value = caseSensitive ? m[0] : m[0].toLowerCase() + return { tokens: [new Token('identifier', value, m[0])], length: m[0].length } + }, + (s) => { + const m = /^`([^`]*)`/.exec(s) + if (!m) return null + return { tokens: [new Token('identifier', m[1], m[0])], length: m[0].length } + } + ] +} + +function tokenize (input, options) { + options = options || {} + let remaining = String(input).replace(/\/\*[\s\S]*?\*\//g, '') + const scanners = buildScanners(options.caseSensitive === true) + const tokens = [] + let nesting = 0 + + while (remaining.length > 0) { + let matched = false + for (let i = 0; i < scanners.length; i++) { + const result = scanners[i](remaining, tokens.length ? tokens[tokens.length - 1] : null) + if (result) { + for (const tok of result.tokens) { + if (tok.category === 'grouping' && tok.value === 'open') nesting++ + if (tok.category === 'grouping' && tok.value === 'close') { + nesting-- + if (nesting < 0) throw new TokenizerError('too many closing parentheses') + } + tokens.push(tok) + } + remaining = remaining.slice(result.length) + matched = true + break + } + } + if (!matched) throw new TokenizerError("parse error at: '" + remaining + "'") + } + if (nesting > 0) throw new TokenizerError('too many opening parentheses') + return tokens +} + +function uniq (arr) { + const seen = new Set() + const out = [] + for (const x of arr) { if (!seen.has(x)) { seen.add(x); out.push(x) } } + return out +} + +function toNumber (val) { + if (typeof val === 'number') return val + if (typeof val === 'boolean') return val ? 1 : 0 + if (val == null) throw new ArgumentError("'" + val + "' is not coercible to numeric") + if (typeof val === 'string') { + if (val.length === 0) throw new ArgumentError("'' is not coercible to numeric") + const n = Number(val) + if (isNaN(n)) throw new ArgumentError("'" + val + "' is not coercible to numeric") + return n + } + throw new ArgumentError("'" + val + "' is not coercible to numeric") +} + +class Node { + static precedence = 0 + static arity = null + static rightAssociative = false + static resolveClass () { return this } + static minParamCount () { return this.arity } + static maxParamCount () { return this.arity } + value () { return null } + dependencies () { return [] } + get type () { return null } +} + +class NilNode extends Node { value () { return null } } + +class Numeric extends Node { + constructor (token) { super(); this.v = token.value } + value () { return this.v } + get type () { return 'numeric' } +} + +class Logical extends Node { + constructor (token) { super(); this.v = token.value } + value () { return this.v } + get type () { return 'logical' } +} + +class StringNode extends Node { + constructor (token) { super(); this.v = token.value } + value () { return this.v } + get type () { return 'string' } +} + +class Identifier extends Node { + constructor (token) { super(); this.ident = token.value } + value (ctx) { + if (ctx && (this.ident in ctx)) { + const v = ctx[this.ident] + if (v instanceof Node) return v.value(ctx) + if (typeof v === 'function') return v() + return v + } + throw new UnboundVariableError([this.ident]) + } + + dependencies (ctx) { + if (ctx && (this.ident in ctx)) { + const v = ctx[this.ident] + return v instanceof Node ? v.dependencies(ctx) : [] + } + return [this.ident] + } +} + +class Grouping extends Node { + constructor (node) { super(); this.node = node } + value (ctx) { return this.node.value(ctx) } + get type () { return this.node.type } + dependencies (ctx) { return this.node.dependencies(ctx) } +} + +class Operation extends Node { + constructor (left, right) { super(); this.left = left; this.right = right } + dependencies (ctx) { + const l = this.left ? this.left.dependencies(ctx) : [] + const r = this.right ? this.right.dependencies(ctx) : [] + return uniq(l.concat(r)) + } +} + +class Arithmetic extends Operation { get type () { return 'numeric' } } + +class Addition extends Arithmetic { + static precedence = 10 + value (ctx) { + const l = this.left.value(ctx) + const r = this.right.value(ctx) + if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r) + return toNumber(l) + toNumber(r) + } +} + +class Subtraction extends Arithmetic { + static precedence = 10 + value (ctx) { return toNumber(this.left.value(ctx)) - toNumber(this.right.value(ctx)) } +} + +class Multiplication extends Arithmetic { + static precedence = 20 + value (ctx) { return toNumber(this.left.value(ctx)) * toNumber(this.right.value(ctx)) } +} + +class Division extends Arithmetic { + static precedence = 20 + value (ctx) { + const r = toNumber(this.right.value(ctx)) + if (r === 0) throw new ZeroDivisionError() + return toNumber(this.left.value(ctx)) / r + } +} + +class Modulo extends Arithmetic { + static arity = 2 + static precedence = 20 + static resolveClass (nextTok) { + if (!nextTok) return Percentage + if (nextTok.category === 'operator') return Percentage + if (nextTok.category === 'comparator') return Percentage + if (nextTok.category === 'combinator') return Percentage + if (nextTok.category === 'grouping' && (nextTok.value === 'close' || nextTok.value === 'comma')) return Percentage + if (nextTok.category === 'array' && nextTok.value === 'array_end') return Percentage + return Modulo + } + + value (ctx) { + const r = toNumber(this.right.value(ctx)) + if (r === 0) throw new ZeroDivisionError() + return toNumber(this.left.value(ctx)) % r + } +} + +class Percentage extends Arithmetic { + static arity = 1 + static precedence = 30 + constructor (child) { super(child, null) } + value (ctx) { return toNumber(this.left.value(ctx)) * 0.01 } + dependencies (ctx) { return this.left.dependencies(ctx) } +} + +class Exponentiation extends Arithmetic { + static precedence = 30 + static rightAssociative = true + value (ctx) { return Math.pow(toNumber(this.left.value(ctx)), toNumber(this.right.value(ctx))) } +} + +class Negation extends Arithmetic { + static arity = 1 + static precedence = 40 + static rightAssociative = true + constructor (node) { super(node, null) } + value (ctx) { return -toNumber(this.left.value(ctx)) } + dependencies (ctx) { return this.left.dependencies(ctx) } +} + +class BitwiseOr extends Operation { + static precedence = 10 + value (ctx) { return toNumber(this.left.value(ctx)) | toNumber(this.right.value(ctx)) } + get type () { return 'numeric' } +} +class BitwiseAnd extends Operation { + static precedence = 10 + value (ctx) { return toNumber(this.left.value(ctx)) & toNumber(this.right.value(ctx)) } + get type () { return 'numeric' } +} +class BitwiseShiftLeft extends Operation { + static precedence = 10 + value (ctx) { return toNumber(this.left.value(ctx)) << toNumber(this.right.value(ctx)) } + get type () { return 'numeric' } +} +class BitwiseShiftRight extends Operation { + static precedence = 10 + value (ctx) { return toNumber(this.left.value(ctx)) >> toNumber(this.right.value(ctx)) } + get type () { return 'numeric' } +} + +function cmpCast (v) { + if (typeof v === 'string' && /^-?\d*\.?\d+$/.test(v)) return Number(v) + return v +} +class Comparator extends Operation { + static precedence = 5 + get type () { return 'logical' } +} +class LessThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) < cmpCast(this.right.value(ctx)) } } +class LessThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) <= cmpCast(this.right.value(ctx)) } } +class GreaterThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) > cmpCast(this.right.value(ctx)) } } +class GreaterThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) >= cmpCast(this.right.value(ctx)) } } +class NotEqual extends Comparator { + value (ctx) { + const l = cmpCast(this.left.value(ctx)) + const r = cmpCast(this.right.value(ctx)) + return l !== r + } +} +class Equal extends Comparator { + value (ctx) { + const l = cmpCast(this.left.value(ctx)) + const r = cmpCast(this.right.value(ctx)) + return l === r + } +} + +class Combinator extends Operation { + static precedence = 0 + get type () { return 'logical' } +} +class And extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) && Boolean(this.right.value(ctx)) } } +class Or extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) || Boolean(this.right.value(ctx)) } } + +class ArrayNode extends Node { + static arity = null + static minParamCount () { return 0 } + static maxParamCount () { return Infinity } + constructor (...items) { + super() + this.items = items + } + + value (ctx) { return this.items.map((i) => i.value(ctx)) } + dependencies (ctx) { + return uniq([].concat.apply([], this.items.map((i) => i.dependencies(ctx)))) + } + + get type () { return 'array' } +} + +class FunctionNode extends Node { + static arity = null + static minParamCount () { return 0 } + static maxParamCount () { return Infinity } + constructor (...args) { + super() + this.args = args + } + + dependencies (ctx) { + return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx)))) + } +} + +const FUNCTION_REGISTRY = Object.create(null) +function registerFunctionClass (name, cls) { FUNCTION_REGISTRY[name.toLowerCase()] = cls } +function getFunctionClass (name) { return FUNCTION_REGISTRY[name.toLowerCase()] } + +function defineFunction (name, spec) { + const min = spec.min == null ? 0 : spec.min + const max = spec.max == null ? Infinity : spec.max + const call = spec.call + const type = spec.type || 'numeric' + class F extends FunctionNode { + static arity = (min === max) ? min : null + static minParamCount () { return min } + static maxParamCount () { return max } + value (ctx) { + const vals = this.args.map((a) => a.value(ctx)) + return call(vals, ctx) + } + + get type () { return type } + } + registerFunctionClass(name, F) + return F +} + +function flattenNumeric (vals) { + const out = []; + (function walk (v) { + if (Array.isArray(v)) v.forEach(walk) + else out.push(toNumber(v)) + })(vals) + return out +} + +defineFunction('abs', { min: 1, max: 1, call: (v) => Math.abs(toNumber(v[0])) }) +defineFunction('min', { min: 1, call: (v) => Math.min.apply(null, flattenNumeric(v)) }) +defineFunction('max', { min: 1, call: (v) => Math.max.apply(null, flattenNumeric(v)) }) +defineFunction('sum', { min: 1, call: (v) => flattenNumeric(v).reduce((a, b) => a + b, 0) }) +defineFunction('avg', { + min: 1, + call: (v) => { + const nums = flattenNumeric(v) + if (nums.length === 0) return 0 + return nums.reduce((a, b) => a + b, 0) / nums.length + } +}) +defineFunction('count', { + min: 1, + max: 1, + call: (v) => Array.isArray(v[0]) ? v[0].length : (v[0] == null ? 0 : 1) +}) +defineFunction('round', { + min: 1, + max: 2, + call: (v) => { + const n = toNumber(v[0]) + const p = v.length > 1 ? toNumber(v[1]) : 0 + const f = Math.pow(10, p) + return Math.round(n * f) / f + } +}) +defineFunction('roundup', { + min: 1, + max: 2, + call: (v) => { + const n = toNumber(v[0]) + const p = v.length > 1 ? toNumber(v[1]) : 0 + const f = Math.pow(10, p) + return (n < 0 ? -Math.floor(-n * f) : Math.ceil(n * f)) / f + } +}) +defineFunction('rounddown', { + min: 1, + max: 2, + call: (v) => { + const n = toNumber(v[0]) + const p = v.length > 1 ? toNumber(v[1]) : 0 + const f = Math.pow(10, p) + return (n < 0 ? -Math.ceil(-n * f) : Math.floor(n * f)) / f + } +}) + +const MATH_FNS = { + sqrt: Math.sqrt, + cbrt: Math.cbrt, + sin: Math.sin, + cos: Math.cos, + tan: Math.tan, + asin: Math.asin, + acos: Math.acos, + atan: Math.atan, + sinh: Math.sinh, + cosh: Math.cosh, + tanh: Math.tanh, + exp: Math.exp, + log: Math.log, + log2: Math.log2, + log10: Math.log10, + floor: Math.floor, + ceil: Math.ceil +} +Object.keys(MATH_FNS).forEach((name) => { + const fn = MATH_FNS[name] + defineFunction(name, { min: 1, max: 1, call: (v) => fn(toNumber(v[0])) }) +}) +defineFunction('atan2', { min: 2, max: 2, call: (v) => Math.atan2(toNumber(v[0]), toNumber(v[1])) }) +defineFunction('hypot', { min: 1, call: (v) => Math.hypot.apply(null, flattenNumeric(v)) }) +defineFunction('pow', { min: 2, max: 2, call: (v) => Math.pow(toNumber(v[0]), toNumber(v[1])) }) +defineFunction('pi', { min: 0, max: 0, call: () => Math.PI }) +defineFunction('e', { min: 0, max: 0, call: () => Math.E }) + +defineFunction('not', { min: 1, max: 1, type: 'logical', call: (v) => !v[0] }) +defineFunction('and', { min: 1, type: 'logical', call: (v) => v.every((x) => Boolean(x)) }) +defineFunction('or', { min: 1, type: 'logical', call: (v) => v.some((x) => Boolean(x)) }) +defineFunction('xor', { + min: 2, + type: 'logical', + call: (v) => v.reduce((acc, x) => acc !== Boolean(x), false) +}) + +class IfFunction extends FunctionNode { + static arity = 3 + static minParamCount () { return 3 } + static maxParamCount () { return 3 } + value (ctx) { + return this.args[0].value(ctx) ? this.args[1].value(ctx) : this.args[2].value(ctx) + } + + get type () { return this.args[1].type } + dependencies (ctx) { + try { + return this.args[0].value(ctx) ? this.args[1].dependencies(ctx) : this.args[2].dependencies(ctx) + } catch (e) { + return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx)))) + } + } +} +registerFunctionClass('if', IfFunction) + +defineFunction('length', { min: 1, max: 1, call: (v) => v[0] == null ? 0 : String(v[0]).length }) +defineFunction('upcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toUpperCase() }) +defineFunction('downcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toLowerCase() }) +defineFunction('concat', { min: 1, type: 'string', call: (v) => v.map((x) => String(x)).join('') }) +defineFunction('left', { + min: 2, + max: 2, + type: 'string', + call: (v) => String(v[0]).slice(0, toNumber(v[1])) +}) +defineFunction('right', { + min: 2, + max: 2, + type: 'string', + call: (v) => { + const n = toNumber(v[1]) + return n === 0 ? '' : String(v[0]).slice(-n) + } +}) +defineFunction('mid', { + min: 3, + max: 3, + type: 'string', + call: (v) => { + const s = String(v[0]) + const start = toNumber(v[1]) - 1 + const len = toNumber(v[2]) + return s.slice(start, start + len) + } +}) +defineFunction('trim', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).trim() }) +defineFunction('contains', { + min: 2, + max: 2, + type: 'logical', + call: (v) => { + if (Array.isArray(v[0])) return v[0].indexOf(v[1]) !== -1 + return String(v[0]).indexOf(String(v[1])) !== -1 + } +}) + +const OP_CLASSES = { + add: Addition, + subtract: Subtraction, + multiply: Multiplication, + divide: Division, + pow: Exponentiation, + negate: Negation, + mod: Modulo, + bitor: BitwiseOr, + bitand: BitwiseAnd, + bitshiftleft: BitwiseShiftLeft, + bitshiftright: BitwiseShiftRight, + lt: LessThan, + gt: GreaterThan, + le: LessThanOrEqual, + ge: GreaterThanOrEqual, + ne: NotEqual, + eq: Equal, + and: And, + or: Or +} + +function isOperationClass (cls) { + return cls && (cls.prototype instanceof Operation) +} + +function parse (tokens) { + if (!tokens || tokens.length === 0) return new NilNode() + + const output = [] + const operations = [] + const arities = [] + const skip = new Set() + + function consume (count) { + if (count == null) count = 2 + const op = operations.pop() + if (!op) throw new ParseError('invalid statement') + + const outputSize = output.length + const opMin = (typeof op.minParamCount === 'function') ? op.minParamCount() : null + const opMax = (typeof op.maxParamCount === 'function') ? op.maxParamCount() : null + + const actualArgs = (op.arity != null) ? op.arity : count + const minSize = (op.arity != null) ? op.arity : (opMin != null ? opMin : count) + const maxSize = (op.arity != null) ? op.arity : (opMax != null ? opMax : count) + + if (outputSize < minSize || actualArgs < minSize) { + throw new ParseError((op.name || 'operator') + ' has too few operands (given ' + outputSize + ', expected ' + minSize + ')') + } + if ((outputSize > maxSize && operations.length === 0) || actualArgs > maxSize) { + throw new ParseError((op.name || 'operator') + ' has too many operands (given ' + outputSize + ', expected ' + maxSize + ')') + } + + if (op === ArrayNode && output.length === 0) { + output.push(new ArrayNode()) + return + } + + if (outputSize < actualArgs) throw new ParseError('invalid statement') + const args = [] + for (let i = 0; i < actualArgs; i++) args.unshift(output.pop()) + + // eslint-disable-next-line new-cap + output.push(new op(...args)) + } + + function handleOperator (token, lookahead) { + let cls = OP_CLASSES[token.value] + if (!cls) throw new ParseError('Unknown operator ' + token.value) + if (typeof cls.resolveClass === 'function') cls = cls.resolveClass(lookahead) + const prec = cls.precedence + if (cls.rightAssociative) { + while (operations.length > 0) { + const top = operations[operations.length - 1] + if (isOperationClass(top) && prec < top.precedence) consume() + else break + } + } else { + while (operations.length > 0) { + const top = operations[operations.length - 1] + if (isOperationClass(top) && prec <= top.precedence) consume() + else break + } + } + operations.push(cls) + } + + function handleFunction (token) { + const fn = getFunctionClass(token.value) + if (!fn) throw new ParseError('Undefined function ' + token.value) + arities.push(0) + operations.push(fn) + } + + function handleArray (token) { + if (token.value === 'array_start') { + operations.push(ArrayNode) + arities.push(0) + } else { + while (operations.length > 0 && operations[operations.length - 1] !== ArrayNode) consume() + if (operations[operations.length - 1] !== ArrayNode) throw new ParseError('Unbalanced bracket') + consume(arities.pop() + 1) + } + } + + function handleGrouping (token, lookahead, index) { + if (token.value === 'open') { + if (lookahead && lookahead.value === 'close') { + skip.add(index + 1) + arities.pop() + consume(0) + } else { + operations.push(Grouping) + } + } else if (token.value === 'close') { + while (operations.length > 0 && operations[operations.length - 1] !== Grouping) consume() + const lparen = operations.pop() + if (lparen !== Grouping) throw new ParseError('Unbalanced parenthesis') + const top = operations[operations.length - 1] + if (top && (top.prototype instanceof FunctionNode)) consume(arities.pop() + 1) + } else if (token.value === 'comma') { + if (arities.length === 0) throw new ParseError('invalid statement') + arities[arities.length - 1] += 1 + while (operations.length > 0 && + operations[operations.length - 1] !== Grouping && + operations[operations.length - 1] !== ArrayNode) consume() + } else { + throw new ParseError('Unknown grouping token ' + token.value) + } + } + + for (let i = 0; i < tokens.length; i++) { + if (skip.has(i)) continue + const token = tokens[i] + const lookahead = tokens[i + 1] + + switch (token.category) { + case 'numeric': output.push(new Numeric(token)); break + case 'logical': output.push(new Logical(token)); break + case 'string': output.push(new StringNode(token)); break + case 'identifier': output.push(new Identifier(token)); break + case 'null': output.push(new NilNode()); break + case 'operator': + case 'comparator': + case 'combinator': + handleOperator(token, lookahead); break + case 'function': handleFunction(token); break + case 'array': handleArray(token); break + case 'grouping': handleGrouping(token, lookahead, i); break + default: + throw new ParseError('Not implemented for tokens of category ' + token.category) + } + } + + while (operations.length > 0) consume() + + if (output.length !== 1) throw new ParseError('Invalid statement') + return output[0] +} + +class Calculator { + constructor (options) { + options = options || {} + this.caseSensitive = options.caseSensitive === true + this.memory = Object.create(null) + this._astCache = Object.create(null) + } + + key (k) { return this.caseSensitive ? k : String(k).toLowerCase() } + + normalize (data) { + const out = Object.create(null) + if (!data) return out + for (const k of Object.keys(data)) out[this.key(k)] = data[k] + return out + } + + store (data) { Object.assign(this.memory, this.normalize(data)); return this } + bind (data) { return this.store(data) } + clear () { this.memory = Object.create(null); return this } + + ast (expression) { + const cached = this._astCache[expression] + if (cached) return cached + const node = parse(tokenize(expression, { caseSensitive: this.caseSensitive })) + this._astCache[expression] = node + return node + } + + tokenize (expression) { + return tokenize(expression, { caseSensitive: this.caseSensitive }) + } + + dependencies (expression) { + return this.ast(expression).dependencies(this.memory) + } + + evaluate (expression, data, onError) { + try { + return this.evaluateStrict(expression, data) + } catch (e) { + if (typeof data === 'function') return data(expression, e) + if (typeof onError === 'function') return onError(expression, e) + return undefined + } + } + + evaluateStrict (expression, data) { + const ctx = Object.assign(Object.create(null), this.memory, this.normalize(data)) + const node = this.ast(expression) + return node.value(ctx) + } + + addFunction (name, spec) { + defineFunction(name, { + min: spec.minArgs == null ? (spec.min == null ? 0 : spec.min) : spec.minArgs, + max: spec.maxArgs == null ? (spec.max == null ? Infinity : spec.max) : spec.maxArgs, + type: spec.type || 'numeric', + call: spec.call + }) + return this + } +} + +export { + Calculator, + tokenize, + parse, + CalculatorError, + ParseError, + TokenizerError, + UnboundVariableError, + ZeroDivisionError, + ArgumentError +} + +export function addFunction (name, spec) { defineFunction(name, spec) } diff --git a/app/javascript/submission_form/formula_areas.vue b/app/javascript/submission_form/formula_areas.vue index d5f80507..b0b1698c 100644 --- a/app/javascript/submission_form/formula_areas.vue +++ b/app/javascript/submission_form/formula_areas.vue @@ -68,34 +68,9 @@ export default { } }, async mounted () { - const { - create, - evaluateDependencies, - addDependencies, - subtractDependencies, - divideDependencies, - multiplyDependencies, - powDependencies, - roundDependencies, - absDependencies, - sinDependencies, - tanDependencies, - cosDependencies - } = await import('mathjs') + const { Calculator } = await import('./calculator') - this.math = create({ - evaluateDependencies, - addDependencies, - subtractDependencies, - divideDependencies, - multiplyDependencies, - powDependencies, - roundDependencies, - absDependencies, - sinDependencies, - tanDependencies, - cosDependencies - }) + this.math = new Calculator() this.isMathLoaded = true }, diff --git a/app/javascript/submission_form/payment_step.vue b/app/javascript/submission_form/payment_step.vue index adf4e19c..bcf32892 100644 --- a/app/javascript/submission_form/payment_step.vue +++ b/app/javascript/submission_form/payment_step.vue @@ -165,34 +165,9 @@ export default { } if (this.field.preferences?.formula) { - const { - create, - evaluateDependencies, - addDependencies, - subtractDependencies, - divideDependencies, - multiplyDependencies, - powDependencies, - roundDependencies, - absDependencies, - sinDependencies, - tanDependencies, - cosDependencies - } = await import('mathjs') + const { Calculator } = await import('./calculator') - this.math = create({ - evaluateDependencies, - addDependencies, - subtractDependencies, - divideDependencies, - multiplyDependencies, - powDependencies, - roundDependencies, - absDependencies, - sinDependencies, - tanDependencies, - cosDependencies - }) + this.math = new Calculator() this.isMathLoaded = true } diff --git a/package.json b/package.json index be3c55d9..24f3130e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "css-minimizer-webpack-plugin": "^5.0.0", "daisyui": "^3.9.4", "driver.js": "^1.3.5", - "mathjs": "^12.4.0", "mini-css-extract-plugin": "^2.10.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", diff --git a/yarn.lock b/yarn.lock index 1069bd9a..71b9ddda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,13 +966,6 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" - integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.18.10", "@babel/template@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" @@ -2994,11 +2987,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== -complex.js@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31" - integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg== - compressible@~2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -3317,11 +3305,6 @@ decache@^3.0.5: dependencies: find "^0.2.4" -decimal.js@^10.4.3: - version "10.4.3" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" - integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -3632,11 +3615,6 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-latex@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" - integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -4091,11 +4069,6 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.4.tgz#b2bac8249a610c3396106da97c5a71da75b94b1c" - integrity sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q== - fraction.js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" @@ -4745,11 +4718,6 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -javascript-natural-sort@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" - integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== - jest-util@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" @@ -5022,21 +4990,6 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -mathjs@^12.4.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-12.4.0.tgz#875c2ec19e5be69885b29769f78bbb37220322b6" - integrity sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw== - dependencies: - "@babel/runtime" "^7.23.9" - complex.js "^2.1.1" - decimal.js "^10.4.3" - escape-latex "^1.2.0" - fraction.js "4.3.4" - javascript-natural-sort "^0.7.1" - seedrandom "^3.0.5" - tiny-emitter "^2.1.0" - typed-function "^4.1.1" - mdn-data@2.0.28: version "2.0.28" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" @@ -6192,11 +6145,6 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" @@ -6406,11 +6354,6 @@ schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -seedrandom@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" - integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -7003,11 +6946,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tiny-emitter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -7111,11 +7049,6 @@ typed-assert@^1.0.8: resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" integrity sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg== -typed-function@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.1.1.tgz#38ce3cae31f4f513bcb263563fdad27b2afa73e8" - integrity sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ== - uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"