@mathigon/hilbert
Advanced tools
Comparing version 0.2.1 to 0.2.2
@@ -334,3 +334,3 @@ 'use strict'; | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…'; | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…°'; | ||
const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS); | ||
@@ -449,3 +449,3 @@ const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS]; | ||
get variables() { return unique(join(...this.items.map(i => i.variables))); } | ||
get functions() { return unique(join(...this.items.map(i => i.functions))); } | ||
get functions() { return this.collapse().functions; } | ||
toString() { return this.items.map(i => i.toString()).join(' '); } | ||
@@ -459,3 +459,3 @@ toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); } | ||
const PRECEDENCE = words('+ − * × · // ^'); | ||
const PRECEDENCE = words('+ − * × · / ÷ // sup sub'); | ||
const COMMA = '<mo value="," lspace="0">,</mo>'; | ||
@@ -468,6 +468,10 @@ | ||
if (!PRECEDENCE.includes(expr.fn)) return false; | ||
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr); | ||
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr.fn); | ||
} | ||
function addRow(expr, string) { | ||
function addMFence(expr, fn, string) { | ||
return needsBrackets(expr, fn) ? `<mfenced>${string}</mfenced>` : string; | ||
} | ||
function addMRow(expr, string) { | ||
const needsRow = (expr instanceof ExprTerm) || (expr instanceof ExprFunction); | ||
@@ -516,2 +520,3 @@ return needsRow ? `<mrow>${string}</mrow>` : string; | ||
collapse() { | ||
if (this.fn === '(') return this.args[0].collapse(); | ||
return new ExprFunction(this.fn, this.args.map(a => a.collapse())); | ||
@@ -540,5 +545,6 @@ } | ||
if (this.fn === '^') return args.join('^'); | ||
if (this.fn === 'sup') return args.join('^'); | ||
if (this.fn === 'sub') return args.join('_'); | ||
if (words('+ * × · / sup = < > ≤ ≥').includes(this.fn)) | ||
if (words('+ * × · / = < > ≤ ≥').includes(this.fn)) | ||
return args.join(' ' + this.fn + ' '); | ||
@@ -556,19 +562,22 @@ | ||
toMathML(custom={}) { | ||
const args = this.args.map(a => needsBrackets(a, this.fn) ? | ||
'<mfenced>' + a.toMathML() + '</mfenced>' : a.toMathML()); | ||
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) return custom[this.fn](...args); | ||
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 args.length > 1 ? | ||
args.join('<mo value="−">−</mo>') : '<mo rspace="0" value="−">−</mo>' + args[0]; | ||
if (this.fn === '−') return argsF.length > 1 ? | ||
argsF.join('<mo value="−">−</mo>') : '<mo rspace="0" value="−">−</mo>' + argsF[0]; | ||
if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥')) | ||
return args.join(`<mo value="${this.fn}">${this.fn}</mo>`); | ||
return argsF.join(`<mo value="${this.fn}">${this.fn}</mo>`); | ||
if (isOneOf(this.fn, '*', '×', '·')) { | ||
let str = args[0]; | ||
for (let i = 1; i < args.length - 1; ++i) { | ||
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 ? `<mo value="×">×</mo>` : '') + args[1]; | ||
str += (showTimes ? `<mo value="×">×</mo>` : '') + argsF[1]; | ||
} | ||
@@ -578,18 +587,26 @@ return str; | ||
if (this.fn === 'sqrt') return `<msqrt>${args[0]}</msqrt>`; | ||
if (this.fn === '//') return argsF.join(`<mo value="/">/</mo>`); | ||
if (this.fn === 'sqrt') return `<msqrt>${argsF[0]}</msqrt>`; | ||
if (isOneOf(this.fn, '/', 'sup', 'sub', 'root')) { | ||
const el = {'/': 'mfrac', 'sup': 'msup', 'sub': 'msub', 'root': 'mroot'}[this.fn]; | ||
const args1 = args.map((a, i) => addRow(this.args[i], a)); | ||
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('')}</${el}>`; | ||
} | ||
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 `<m${this.fn}>${args1.join('')}</m${this.fn}>`; | ||
} | ||
if (isOneOf(this.fn, '(', '[', '{')) | ||
return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${args.join(COMMA)}</mfenced>`; | ||
return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${argsF.join(COMMA)}</mfenced>`; | ||
if (isOneOf(this.fn, '!', '%')) | ||
return args[0] + `<mo value="${this.fn}" lspace="0">${this.fn}</mo>`; | ||
return argsF[0] + `<mo value="${this.fn}" lspace="0">${this.fn}</mo>`; | ||
// TODO Implement other functions | ||
return `<mi>${this.fn}</mi><mfenced>${args.join(COMMA)}</mfenced>`; | ||
return `<mi>${this.fn}</mi><mfenced>${argsF.join(COMMA)}</mfenced>`; | ||
} | ||
@@ -610,3 +627,3 @@ } | ||
if (type === 'SPACE' && buffer.length > 1) return new ExprSpace(); | ||
if (type === 'STRING') return new ExprString(buffer); | ||
if (type === 'STR') return new ExprString(buffer); | ||
@@ -680,3 +697,5 @@ if (type === 'VAR') { | ||
function makeTerm(items) { | ||
return (items.length === 1) ? items[0] : new ExprTerm(items); | ||
if (items.length > 1) return new ExprTerm(items); | ||
if (items[0] instanceof ExprOperator) return new ExprTerm(items); | ||
return items[0]; | ||
} | ||
@@ -749,3 +768,5 @@ | ||
// Check if this is a normal bracket, or a function call. | ||
const isFn = (isOperator(t, ')') && last(term) instanceof ExprIdentifier); | ||
// Terms like x(y) are treated as functions, rather than implicit | ||
// multiplication, except for π(y). | ||
const isFn = (isOperator(t, ')') && last(term) instanceof ExprIdentifier && last(term).i !== 'π'); | ||
const fnName = isFn ? term.pop().i : isOperator(t, '|') ? 'abs' : closed[0].o; | ||
@@ -779,2 +800,3 @@ | ||
function clearBuffer() { | ||
if (lastWasSymbol) throw ExprError.invalidExpression(); | ||
if (!buffer.length) return; | ||
@@ -803,3 +825,2 @@ result.push(buffer.length > 1 ? new ExprFunction(symbol[0], buffer) : buffer[0]); | ||
if (lastWasSymbol) throw ExprError.invalidExpression(); | ||
clearBuffer(); | ||
@@ -824,6 +845,6 @@ return result; | ||
findBinaryFunction(tokens, '= < > ≤ ≥'); | ||
findBinaryFunction(tokens, '//', '/'); | ||
findBinaryFunction(tokens, '// ÷', '/'); | ||
// Match multiplication operators. | ||
tokens = findAssociativeFunction(tokens, '* × ·', true); | ||
tokens = findAssociativeFunction(tokens, '× * ·', true); | ||
@@ -867,16 +888,21 @@ // Match - and ± operators. | ||
function numEquals(expr1, expr2) { | ||
const vars = unique([...expr1.variables, ...expr2.variables]); | ||
const fn1 = expr1.collapse(); | ||
const fn2 = expr2.collapse(); | ||
try { | ||
const vars = unique([...expr1.variables, ...expr2.variables]); | ||
const fn1 = expr1.collapse(); | ||
const fn2 = expr2.collapse(); | ||
// We only test positive random numbers, because negative numbers raised | ||
// to non-integer powers return NaN. | ||
for (let i = 0; i < 5; ++i) { | ||
const substitution = {}; | ||
for (let v of vars) substitution[v] = CONSTANTS[v] || Math.random() * 5; | ||
const a = fn1.evaluate(substitution); | ||
const b = fn2.evaluate(substitution); | ||
if (!nearlyEquals(a, b)) return false; | ||
// We only test positive random numbers, because negative numbers raised | ||
// to non-integer powers return NaN. | ||
for (let i = 0; i < 5; ++i) { | ||
const substitution = {}; | ||
for (let v of vars) substitution[v] = CONSTANTS[v] || Math.random() * 5; | ||
const a = fn1.evaluate(substitution); | ||
const b = fn2.evaluate(substitution); | ||
if (isNaN(a) || isNaN(b)) continue; // This might happen in square roots. | ||
if (!nearlyEquals(a, b)) return false; | ||
} | ||
return true; | ||
} catch(e) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
@@ -883,0 +909,0 @@ |
{ | ||
"name": "@mathigon/hilbert", | ||
"version": "0.2.1", | ||
"version": "0.2.2", | ||
"description": "JavaScript expression parsing, MathML rendering and CAS.", | ||
@@ -28,3 +28,3 @@ "keywords": [ | ||
"dependencies": { | ||
"@mathigon/fermat": "^0.2.6", | ||
"@mathigon/fermat": "^0.2.7", | ||
"@mathigon/core": "^0.2.5" | ||
@@ -31,0 +31,0 @@ }, |
@@ -15,3 +15,2 @@ # Hilbert.js | ||
* [ ] _Remove expressions code from `fermat.js`. Update x-equation._ | ||
* [ ] Support for functions with subscripts (e.g. `log_a(b)`). | ||
@@ -18,0 +17,0 @@ * [ ] Support for super+subscripts (e.g. `a_n^2` or `a^2_n`). |
@@ -121,3 +121,3 @@ // ============================================================================= | ||
get variables() { return unique(join(...this.items.map(i => i.variables))); } | ||
get functions() { return unique(join(...this.items.map(i => i.functions))); } | ||
get functions() { return this.collapse().functions; } | ||
toString() { return this.items.map(i => i.toString()).join(' '); } | ||
@@ -124,0 +124,0 @@ toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); } |
@@ -34,16 +34,21 @@ // ============================================================================= | ||
function numEquals(expr1, expr2) { | ||
const vars = unique([...expr1.variables, ...expr2.variables]); | ||
const fn1 = expr1.collapse(); | ||
const fn2 = expr2.collapse(); | ||
try { | ||
const vars = unique([...expr1.variables, ...expr2.variables]); | ||
const fn1 = expr1.collapse(); | ||
const fn2 = expr2.collapse(); | ||
// We only test positive random numbers, because negative numbers raised | ||
// to non-integer powers return NaN. | ||
for (let i = 0; i < 5; ++i) { | ||
const substitution = {}; | ||
for (let v of vars) substitution[v] = CONSTANTS[v] || Math.random() * 5; | ||
const a = fn1.evaluate(substitution); | ||
const b = fn2.evaluate(substitution); | ||
if (!nearlyEquals(a, b)) return false; | ||
// We only test positive random numbers, because negative numbers raised | ||
// to non-integer powers return NaN. | ||
for (let i = 0; i < 5; ++i) { | ||
const substitution = {}; | ||
for (let v of vars) substitution[v] = CONSTANTS[v] || Math.random() * 5; | ||
const a = fn1.evaluate(substitution); | ||
const b = fn2.evaluate(substitution); | ||
if (isNaN(a) || isNaN(b)) continue; // This might happen in square roots. | ||
if (!nearlyEquals(a, b)) return false; | ||
} | ||
return true; | ||
} catch(e) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
@@ -50,0 +55,0 @@ |
@@ -14,3 +14,3 @@ // ============================================================================= | ||
const PRECEDENCE = words('+ − * × · // ^'); | ||
const PRECEDENCE = words('+ − * × · / ÷ // sup sub'); | ||
const COMMA = '<mo value="," lspace="0">,</mo>'; | ||
@@ -23,6 +23,10 @@ | ||
if (!PRECEDENCE.includes(expr.fn)) return false; | ||
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr); | ||
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr.fn); | ||
} | ||
function addRow(expr, string) { | ||
function addMFence(expr, fn, string) { | ||
return needsBrackets(expr, fn) ? `<mfenced>${string}</mfenced>` : string; | ||
} | ||
function addMRow(expr, string) { | ||
const needsRow = (expr instanceof ExprTerm) || (expr instanceof ExprFunction); | ||
@@ -71,2 +75,3 @@ return needsRow ? `<mrow>${string}</mrow>` : string; | ||
collapse() { | ||
if (this.fn === '(') return this.args[0].collapse(); | ||
return new ExprFunction(this.fn, this.args.map(a => a.collapse())); | ||
@@ -95,5 +100,6 @@ } | ||
if (this.fn === '^') return args.join('^'); | ||
if (this.fn === 'sup') return args.join('^'); | ||
if (this.fn === 'sub') return args.join('_'); | ||
if (words('+ * × · / sup = < > ≤ ≥').includes(this.fn)) | ||
if (words('+ * × · / = < > ≤ ≥').includes(this.fn)) | ||
return args.join(' ' + this.fn + ' '); | ||
@@ -111,19 +117,22 @@ | ||
toMathML(custom={}) { | ||
const args = this.args.map(a => needsBrackets(a, this.fn) ? | ||
'<mfenced>' + a.toMathML() + '</mfenced>' : a.toMathML()); | ||
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) return custom[this.fn](...args); | ||
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 args.length > 1 ? | ||
args.join('<mo value="−">−</mo>') : '<mo rspace="0" value="−">−</mo>' + args[0]; | ||
if (this.fn === '−') return argsF.length > 1 ? | ||
argsF.join('<mo value="−">−</mo>') : '<mo rspace="0" value="−">−</mo>' + argsF[0]; | ||
if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥')) | ||
return args.join(`<mo value="${this.fn}">${this.fn}</mo>`); | ||
return argsF.join(`<mo value="${this.fn}">${this.fn}</mo>`); | ||
if (isOneOf(this.fn, '*', '×', '·')) { | ||
let str = args[0]; | ||
for (let i = 1; i < args.length - 1; ++i) { | ||
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 ? `<mo value="×">×</mo>` : '') + args[1]; | ||
str += (showTimes ? `<mo value="×">×</mo>` : '') + argsF[1]; | ||
} | ||
@@ -133,19 +142,27 @@ return str; | ||
if (this.fn === 'sqrt') return `<msqrt>${args[0]}</msqrt>`; | ||
if (this.fn === '//') return argsF.join(`<mo value="/">/</mo>`); | ||
if (this.fn === 'sqrt') return `<msqrt>${argsF[0]}</msqrt>`; | ||
if (isOneOf(this.fn, '/', 'sup', 'sub', 'root')) { | ||
const el = {'/': 'mfrac', 'sup': 'msup', 'sub': 'msub', 'root': 'mroot'}[this.fn]; | ||
const args1 = args.map((a, i) => addRow(this.args[i], a)); | ||
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('')}</${el}>`; | ||
} | ||
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 `<m${this.fn}>${args1.join('')}</m${this.fn}>`; | ||
} | ||
if (isOneOf(this.fn, '(', '[', '{')) | ||
return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${args.join(COMMA)}</mfenced>`; | ||
return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${argsF.join(COMMA)}</mfenced>`; | ||
if (isOneOf(this.fn, '!', '%')) | ||
return args[0] + `<mo value="${this.fn}" lspace="0">${this.fn}</mo>`; | ||
return argsF[0] + `<mo value="${this.fn}" lspace="0">${this.fn}</mo>`; | ||
// TODO Implement other functions | ||
return `<mi>${this.fn}</mi><mfenced>${args.join(COMMA)}</mfenced>`; | ||
return `<mi>${this.fn}</mi><mfenced>${argsF.join(COMMA)}</mfenced>`; | ||
} | ||
} |
@@ -24,3 +24,3 @@ // ============================================================================= | ||
if (type === 'SPACE' && buffer.length > 1) return new ExprSpace(); | ||
if (type === 'STRING') return new ExprString(buffer); | ||
if (type === 'STR') return new ExprString(buffer); | ||
@@ -94,3 +94,5 @@ if (type === 'VAR') { | ||
function makeTerm(items) { | ||
return (items.length === 1) ? items[0] : new ExprTerm(items); | ||
if (items.length > 1) return new ExprTerm(items); | ||
if (items[0] instanceof ExprOperator) return new ExprTerm(items); | ||
return items[0]; | ||
} | ||
@@ -163,3 +165,5 @@ | ||
// Check if this is a normal bracket, or a function call. | ||
const isFn = (isOperator(t, ')') && last(term) instanceof ExprIdentifier); | ||
// Terms like x(y) are treated as functions, rather than implicit | ||
// multiplication, except for π(y). | ||
const isFn = (isOperator(t, ')') && last(term) instanceof ExprIdentifier && last(term).i !== 'π'); | ||
const fnName = isFn ? term.pop().i : isOperator(t, '|') ? 'abs' : closed[0].o; | ||
@@ -193,2 +197,3 @@ | ||
function clearBuffer() { | ||
if (lastWasSymbol) throw ExprError.invalidExpression(); | ||
if (!buffer.length) return; | ||
@@ -217,3 +222,2 @@ result.push(buffer.length > 1 ? new ExprFunction(symbol[0], buffer) : buffer[0]); | ||
if (lastWasSymbol) throw ExprError.invalidExpression(); | ||
clearBuffer(); | ||
@@ -238,6 +242,6 @@ return result; | ||
findBinaryFunction(tokens, '= < > ≤ ≥'); | ||
findBinaryFunction(tokens, '//', '/'); | ||
findBinaryFunction(tokens, '// ÷', '/'); | ||
// Match multiplication operators. | ||
tokens = findAssociativeFunction(tokens, '* × ·', true); | ||
tokens = findAssociativeFunction(tokens, '× * ·', true); | ||
@@ -244,0 +248,0 @@ // Match - and ± operators. |
@@ -104,4 +104,4 @@ // ============================================================================= | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…'; | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…°'; | ||
const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS); | ||
export const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS]; |
@@ -12,3 +12,3 @@ // ============================================================================= | ||
const expr = (src) => hilbert.Expression.parse(src); | ||
const mathML = (src) => expr(src).toMathML(); | ||
const mathML = (src, replace = {}) => expr(src).toMathML(replace); | ||
@@ -29,2 +29,17 @@ | ||
tape('Custom Functions', function(test) { | ||
const options = { | ||
a: (a) => `<a>${a}</a>`, | ||
b: (...b) => `<b>${b.join(',')}</b>`, | ||
c: (c) => `<c attr="${c.val.s}">${c}</c>` | ||
}; | ||
test.equal(mathML('a(1)', options), '<a><mn>1</mn></a>'); | ||
test.equal(mathML('b(1,2)', options), '<b><mn>1</mn>,<mn>2</mn></b>'); | ||
test.equal(mathML('c("text")', options), '<c attr="text"><mtext>text</mtext></c>'); | ||
test.end(); | ||
}); | ||
tape('Whitespace', function(test) { | ||
@@ -31,0 +46,0 @@ test.equal(mathML('a b'), '<mi>a</mi><mspace/><mi>b</mi>'); |
@@ -18,6 +18,6 @@ // ============================================================================= | ||
test.equal(str('-1'), '− 1'); | ||
test.equal(expr('x + y').toString(), 'x + y'); | ||
test.equal(expr('aa + bb + cc').toString(), 'aa + bb + cc'); | ||
test.equal(expr('5x + 2').toString(), '5 x + 2'); | ||
test.equal(expr('|a|').toString(), 'abs(a)'); | ||
test.equal(str('x + y'), 'x + y'); | ||
test.equal(str('aa + bb + cc'), 'aa + bb + cc'); | ||
test.equal(str('5x + 2'), '5 x + 2'); | ||
test.equal(str('|a|'), 'abs(a)'); | ||
test.end(); | ||
@@ -27,6 +27,28 @@ }); | ||
tape('special operators', function(test) { | ||
test.equal(expr('x - y').toString(), 'x − y'); | ||
test.equal(expr('x – y').toString(), 'x − y'); | ||
test.equal(expr('x − y').toString(), 'x − y'); | ||
test.equal(str('x - y'), 'x − y'); | ||
test.equal(str('x – y'), 'x − y'); | ||
test.equal(str('x − y'), 'x − y'); | ||
test.end(); | ||
}); | ||
tape('functions', function(test) { | ||
test.equal(str('A_B'), 'A_B'); | ||
test.equal(str('fn(A, B)'), 'fn(A, B)'); | ||
test.end(); | ||
}); | ||
tape('strings', function(test) { | ||
test.equal(str('"A" + "B"'), '"A" + "B"'); | ||
test.equal(str('"A"_"B"'), '"A"_"B"'); | ||
test.equal(str('fn(A_"B",C)'), 'fn(A_"B", C)'); | ||
test.end(); | ||
}); | ||
tape('errors', function(test) { | ||
test.throws(() => expr('a + + b').collapse()); | ||
test.throws(() => expr('a * - b').collapse()); | ||
test.throws(() => expr('a + (a +)').collapse()); | ||
test.throws(() => expr('a + (*)').collapse()); | ||
test.throws(() => expr('(+) - a').collapse()); | ||
test.end(); | ||
}); |
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
63072
18
1541
61
Updated@mathigon/fermat@^0.2.7