mirror of
https://github.com/josdejong/mathjs.git
synced 2025-12-08 19:46:04 +00:00
Resolves #3353. * test: enhance nullish coalescing tests for arrays, matrices, and conditional expressions * docs: add nullish coalescing operator precedence details and update syntax documentation --------- Co-authored-by: Jos de Jong <wjosdejong@gmail.com> Co-authored-by: Glen Whitney <glen@studioinfinity.org>
283 lines
14 KiB
JavaScript
283 lines
14 KiB
JavaScript
import assert from 'assert'
|
|
import { approxEqual } from '../../../../tools/approx.js'
|
|
import math from '../../../../src/defaultInstance.js'
|
|
import { createMap } from '../../../../src/utils/map.js'
|
|
|
|
const Complex = math.Complex
|
|
const Unit = math.Unit
|
|
const ResultSet = math.ResultSet
|
|
|
|
describe('evaluate', function () {
|
|
it('should evaluate expressions', function () {
|
|
approxEqual(math.evaluate('(2+3)/4'), 1.25)
|
|
assert.deepStrictEqual(math.evaluate('sqrt(-4)'), new Complex(0, 2))
|
|
})
|
|
|
|
it('should evaluate a list of expressions', function () {
|
|
assert.deepStrictEqual(math.evaluate(['1+2', '3+4', '5+6']), [3, 7, 11])
|
|
assert.deepStrictEqual(math.evaluate(['a=3', 'b=4', 'a*b']), [3, 4, 12])
|
|
assert.deepStrictEqual(math.evaluate(math.matrix(['a=3', 'b=4', 'a*b'])), math.matrix([3, 4, 12]))
|
|
assert.deepStrictEqual(math.evaluate(['a=3', 'b=4', 'a*b']), [3, 4, 12])
|
|
})
|
|
|
|
it('should evaluate a series of expressions', function () {
|
|
assert.deepStrictEqual(math.evaluate('a=3\nb=4\na*b'), new ResultSet([3, 4, 12]))
|
|
assert.deepStrictEqual(math.evaluate('f(x) = a * x; a=2; f(4)'), new ResultSet([8]))
|
|
assert.deepStrictEqual(math.evaluate('b = 43; b * 4'), new ResultSet([172]))
|
|
})
|
|
|
|
it('should throw an error if wrong number of arguments', function () {
|
|
assert.throws(function () { math.evaluate() }, /TypeError: Too few arguments/)
|
|
assert.throws(function () { math.evaluate('', {}, 3) }, /TypeError: Too many arguments/)
|
|
})
|
|
|
|
it('should throw an error with a unit', function () {
|
|
assert.throws(function () { math.evaluate(new Unit(5, 'cm')) }, /TypeError: Unexpected type of argument/)
|
|
})
|
|
|
|
it('should throw an error with a complex number', function () {
|
|
assert.throws(function () { math.evaluate(new Complex(2, 3)) }, /TypeError: Unexpected type of argument/)
|
|
})
|
|
|
|
it('should evaluate a boolean', function () {
|
|
// TODO: this is odd. Boolean is turned in to string and then evaluated
|
|
assert.strictEqual(math.evaluate(true), true)
|
|
})
|
|
|
|
it('should handle the given scope', function () {
|
|
const scope = {
|
|
a: 3,
|
|
b: 4
|
|
}
|
|
assert.deepStrictEqual(math.evaluate('a*b', scope), 12)
|
|
assert.deepStrictEqual(math.evaluate('c=5', scope), 5)
|
|
assert.deepStrictEqual(math.format(math.evaluate('f(x) = x^a', scope)), 'f(x)')
|
|
|
|
assert.deepStrictEqual(Object.keys(scope).length, 4)
|
|
assert.deepStrictEqual(scope.a, 3)
|
|
assert.deepStrictEqual(scope.b, 4)
|
|
assert.deepStrictEqual(scope.c, 5)
|
|
assert.deepStrictEqual(typeof scope.f, 'function')
|
|
|
|
assert.strictEqual(scope.f(3), 27)
|
|
scope.a = 2
|
|
assert.strictEqual(scope.f(3), 9)
|
|
scope.hello = function (name) {
|
|
return 'hello, ' + name + '!'
|
|
}
|
|
assert.deepStrictEqual(math.evaluate('hello("jos")', scope), 'hello, jos!')
|
|
})
|
|
|
|
it('should handle the given Map scope', function () {
|
|
const scope = createMap({
|
|
a: 3,
|
|
b: 4
|
|
})
|
|
assert.deepStrictEqual(math.evaluate('a*b', scope), 12)
|
|
assert.deepStrictEqual(math.evaluate('c=5', scope), 5)
|
|
})
|
|
|
|
it('should LaTeX evaluate', function () {
|
|
const expr1 = math.parse('evaluate(expr)')
|
|
const expr2 = math.parse('evaluate(expr,scope)')
|
|
|
|
assert.strictEqual(expr1.toTex(), '\\mathrm{evaluate}\\left( expr\\right)')
|
|
assert.strictEqual(expr2.toTex(), '\\mathrm{evaluate}\\left( expr, scope\\right)')
|
|
})
|
|
|
|
describe('nullish coalescing operator', function () {
|
|
it('should handle basic nullish coalescing', function () {
|
|
assert.strictEqual(math.evaluate('null ?? 42'), 42)
|
|
assert.strictEqual(math.evaluate('undefined ?? 42'), 42)
|
|
assert.strictEqual(math.evaluate('0 ?? 42'), 0)
|
|
assert.strictEqual(math.evaluate('nullish(null, 42)'), 42)
|
|
assert.strictEqual(math.evaluate('false ?? 42'), false)
|
|
assert.strictEqual(math.evaluate('"" ?? 42'), '')
|
|
assert(isNaN(math.evaluate('NaN ?? 42'))) // Should return NaN, not 42
|
|
})
|
|
|
|
it('should handle nullish coalescing with variables', function () {
|
|
const scope1 = {}
|
|
assert.throws(() => math.evaluate('x ?? 42', scope1), /Undefined symbol x/)
|
|
assert.throws(() => math.evaluate('nullish(x, 42)', scope1), /Undefined symbol x/)
|
|
|
|
const scope2 = { x: null }
|
|
assert.strictEqual(math.evaluate('x ?? 42', scope2), 42)
|
|
assert.strictEqual(math.evaluate('nullish(x, 42)', scope2), 42)
|
|
|
|
const scope3 = { x: 0 }
|
|
assert.strictEqual(math.evaluate('x ?? 42', scope3), 0)
|
|
assert.strictEqual(math.evaluate('nullish(x, 42)', scope3), 0)
|
|
|
|
const scope4 = { x: undefined }
|
|
assert.strictEqual(math.evaluate('x ?? 42', scope4), 42)
|
|
assert.strictEqual(math.evaluate('nullish(x, 42)', scope4), 42)
|
|
|
|
const scope5 = { x: 5 }
|
|
assert.strictEqual(math.evaluate('x ?? 42', scope5), 5)
|
|
assert.strictEqual(math.evaluate('nullish(x, 42)', scope5), 5)
|
|
})
|
|
|
|
it('should handle chained nullish coalescing', function () {
|
|
assert.strictEqual(math.evaluate('null ?? undefined ?? 42'), 42)
|
|
assert.strictEqual(math.evaluate('nullish(null, undefined ?? 42)'), 42)
|
|
assert.strictEqual(math.evaluate('null ?? 10 ?? 42'), 10)
|
|
assert.strictEqual(math.evaluate('5 ?? null ?? 42'), 5)
|
|
assert.strictEqual(math.evaluate('nullish(5, null ?? 42)'), 5)
|
|
assert.strictEqual(math.evaluate('null ?? null ?? null ?? 99'), 99)
|
|
assert.strictEqual(math.evaluate('nullish(null ?? null, null ?? 99)'), 99)
|
|
})
|
|
|
|
it('should handle nullish coalescing with correct precedence', function () {
|
|
// ?? has higher precedence than arithmetic and logical operators
|
|
assert.strictEqual(math.evaluate('1 + null ?? 2'), 3) // 1 + (null ?? 2)
|
|
assert.strictEqual(math.evaluate('2 * null ?? 3'), 6) // 2 * (null ?? 3)
|
|
|
|
// ?? has higher precedence than exponentiation
|
|
assert.strictEqual(math.evaluate('2 ^ null ?? 3'), 8) // 2 ^ (null ?? 3)
|
|
|
|
assert.strictEqual(math.evaluate('null ?? false or true'), true) // (null ?? false) or true
|
|
assert.strictEqual(math.evaluate('true or null ?? 42'), true) // true or (null ?? 42)
|
|
assert.strictEqual(math.evaluate('false and null ?? 42'), false) // false and (null ?? 42)
|
|
assert.strictEqual(math.evaluate('true xor null ?? 42'), false) // true xor (null ?? 42)
|
|
|
|
// Parentheses can override precedence
|
|
assert.throws(() => math.evaluate('(1 + null) ?? 2'), /TypeError: Unexpected type of argument/)
|
|
assert.throws(() => math.evaluate('(2 * null) ?? 3'), /TypeError: Unexpected type of argument/)
|
|
assert.throws(() => math.evaluate('(2 ^ null) ?? 3'), /TypeError: Unexpected type of argument/)
|
|
assert.strictEqual(math.evaluate('2 * (null ?? 3)'), 6)
|
|
})
|
|
|
|
it('should handle nullish coalescing with higher precedence than exponentiation', function () {
|
|
// These tests specifically verify that ?? has higher precedence than ^
|
|
assert.strictEqual(math.evaluate('5 ?? 2 ^ 3'), 125) // (5 ?? 2) ^ 3 = 5 ^ 3 = 125
|
|
assert.strictEqual(math.evaluate('5 ?? (2 ^ 3)'), 5)
|
|
assert.strictEqual(math.evaluate('3 ^ null ?? 2 ^ 2'), 81) // 3 ^ (null ?? 2) ^ 2 = 3 ^ 2 ^ 2 = 3 ^ 4 = 81
|
|
assert.strictEqual(math.evaluate('false ?? 3 ^ 2'), 0) // false is not nullish, so (false ?? 3) ^ 2 = false ^ 2 = 0 ^ 2 = 0
|
|
})
|
|
|
|
it('should handle nullish coalescing precedence with dot power and left-hand operators', function () {
|
|
// dot power .^ should behave like ^ for scalars and bind lower than ??
|
|
assert.strictEqual(math.evaluate('2 .^ null ?? 3'), 8) // 2 .^ (null ?? 3)
|
|
assert.strictEqual(math.evaluate('5 ?? 2 .^ 3'), 125) // (5 ?? 2) .^ 3
|
|
|
|
// left-hand operators like factorial bind tighter than ??
|
|
assert.strictEqual(math.evaluate('5! ?? 2'), 120) // (5!) ?? 2
|
|
assert.strictEqual(math.evaluate('null ?? 3!'), 6) // null ?? (3!)
|
|
assert.strictEqual(math.evaluate('(null ?? 3)!'), 6) // parentheses with left-hand op
|
|
})
|
|
|
|
it('should handle nullish coalescing with scope lookup', function () {
|
|
const scope = { a: null, b: 5, c: 0 }
|
|
assert.strictEqual(math.evaluate('a ?? b * 2', scope), 10) // null ?? (5 * 2)
|
|
assert.strictEqual(math.evaluate('c ?? b * 2', scope), 0) // 0 ?? (5 * 2) = 0
|
|
assert.strictEqual(math.evaluate('(a ?? b) * 2', scope), 10) // (null ?? 5) * 2 = 10
|
|
// d is undefined, would throw error without fallback
|
|
assert.throws(() => math.evaluate('d ?? 0', scope), /Undefined symbol d/)
|
|
})
|
|
|
|
it('should handle nullish coalescing with strings', function () {
|
|
assert.strictEqual(math.evaluate('null ?? "hello"'), 'hello')
|
|
assert.strictEqual(math.evaluate('"world" ?? "hello"'), 'world')
|
|
assert.strictEqual(math.evaluate('"" ?? "hello"'), '') // empty string is not nullish
|
|
})
|
|
|
|
it('should handle nullish coalescing with matrices and arrays as operands', function () {
|
|
// Test arrays as operands
|
|
assert.deepStrictEqual(math.evaluate('null ?? [1, 2, 3]'), math.matrix([1, 2, 3]))
|
|
assert.deepStrictEqual(math.evaluate('[1, 2] ?? [3, 4]'), math.matrix([1, 2])) // Neither 1 nor 2 is nullish
|
|
assert.deepStrictEqual(math.evaluate('undefined ?? [5, 6]'), math.matrix([5, 6]))
|
|
assert.deepStrictEqual(math.evaluate('[null, null] ?? [7, 8]'), math.matrix([7, 8])) // Both null elements are nullish, so use [7, 8]
|
|
|
|
// Test matrices as operands
|
|
const matrix1 = math.matrix([1, 2])
|
|
assert.deepStrictEqual(math.evaluate('null ?? matrix([1, 2])'), matrix1)
|
|
assert.deepStrictEqual(math.evaluate('matrix([1, 2]) ?? matrix([3, 4])'), matrix1) // Neither 1 nor 2 is nullish
|
|
assert.deepStrictEqual(math.evaluate('undefined ?? matrix([5, 6])'), math.matrix([5, 6]))
|
|
|
|
// Test mixed arrays and matrices
|
|
assert.deepStrictEqual(math.evaluate('null ?? matrix([1, 2])'), matrix1)
|
|
assert.deepStrictEqual(math.evaluate('[1, 2] ?? matrix([3, 4])'), math.matrix([1, 2]))
|
|
assert.deepStrictEqual(math.evaluate('[null, 5] ?? 42'), math.matrix([42, 5]))
|
|
assert.deepStrictEqual(math.evaluate('[null, 5] ?? [1, 2]'), math.matrix([1, 5]))
|
|
|
|
// Test arrays/matrices containing expressions
|
|
assert.deepStrictEqual(math.evaluate(['null ?? 1', '2 ?? null', 'null ?? null ?? 3']), [1, 2, 3])
|
|
assert.deepStrictEqual(math.evaluate(math.matrix(['null ?? 1', '2 ?? null'])), math.matrix([1, 2]))
|
|
|
|
// Test shape mismatch with empty array
|
|
assert.throws(() => math.evaluate('[] ?? [7, 8]'), /RangeError/)
|
|
assert.throws(() => math.evaluate('[1] ?? [7, 8]'), /RangeError/)
|
|
})
|
|
|
|
it('should handle nullish coalescing with function calls', function () {
|
|
const scope = {
|
|
getValue: function () { return null },
|
|
getDefault: function () { return 42 }
|
|
}
|
|
assert.strictEqual(math.evaluate('getValue() ?? getDefault()', scope), 42)
|
|
|
|
const scope2 = {
|
|
getValue: function () { return 10 },
|
|
getDefault: function () { return 42 }
|
|
}
|
|
assert.strictEqual(math.evaluate('getValue() ?? getDefault()', scope2), 10)
|
|
})
|
|
|
|
it('should handle nullish function with arrays', function () {
|
|
assert.deepStrictEqual(math.evaluate('nullish(null, [1, 2, 3])'), math.matrix([1, 2, 3]))
|
|
assert.deepStrictEqual(math.evaluate('nullish([1, 2], [3, 4])'), math.matrix([1, 2]))
|
|
assert.deepStrictEqual(math.evaluate('nullish([null, 5], 42)'), math.matrix([42, 5]))
|
|
assert.deepStrictEqual(math.evaluate('nullish([null, 5], [1, 2])'), math.matrix([1, 5]))
|
|
})
|
|
|
|
it('should handle nullish coalescing with conditional expressions and correct precedence', function () {
|
|
// ?? has higher precedence than conditional (?:), so these test cases show the difference
|
|
assert.strictEqual(math.evaluate('5 ?? null ? 1 : 2'), 1) // (5 ?? null) ? 1 : 2 = 5 ? 1 : 2 = 1
|
|
assert.strictEqual(math.evaluate('null ?? 0 ? 1 : 2'), 2) // (null ?? 0) ? 1 : 2 = 0 ? 1 : 2 = 2
|
|
assert.strictEqual(math.evaluate('undefined ?? true ? 1 : 2'), 1) // (undefined ?? true) ? 1 : 2 = true ? 1 : 2 = 1
|
|
|
|
assert.strictEqual(math.evaluate('(5 ?? null) ? 1 : 2'), 1) // Explicit precedence
|
|
assert.strictEqual(math.evaluate('5 ?? (null ? 1 : 2)'), 5) // Different precedence
|
|
})
|
|
|
|
it('should short-circuit evaluation of the right-hand side when left is not nullish', function () {
|
|
// RHS throws if evaluated; must not be called
|
|
const scope = {
|
|
boom: function () { throw new Error('RHS evaluated unexpectedly') }
|
|
}
|
|
assert.strictEqual(math.evaluate('5 ?? boom()', scope), 5)
|
|
assert.strictEqual(math.evaluate('0 ?? boom()', scope), 0)
|
|
assert.strictEqual(math.evaluate('false ?? boom()', scope), false)
|
|
assert.strictEqual(math.evaluate('"" ?? boom()', scope), '')
|
|
assert.throws(() => math.evaluate('null ?? boom()', scope))
|
|
assert.throws(() => math.evaluate('undefined ?? boom()', scope))
|
|
})
|
|
|
|
it('should evaluate the right-hand side when left is nullish', function () {
|
|
let count = 0
|
|
const scope = {
|
|
inc: function () { count++; return 7 }
|
|
}
|
|
assert.strictEqual(math.evaluate('null ?? inc()', scope), 7)
|
|
assert.strictEqual(count, 1)
|
|
assert.strictEqual(math.evaluate('undefined ?? inc()', scope), 7)
|
|
assert.strictEqual(count, 2)
|
|
})
|
|
|
|
it('should not short-circuit for collections (element-wise evaluation requires RHS)', function () {
|
|
// When left is a collection, element-wise nullish requires evaluating RHS
|
|
let called = 0
|
|
const scope = {
|
|
getDefault: function () {
|
|
called++
|
|
return math.matrix([1, 2])
|
|
}
|
|
}
|
|
const res = math.evaluate('matrix([null, 5]) ?? getDefault()', scope)
|
|
assert.deepStrictEqual(res, math.matrix([1, 5]))
|
|
assert.strictEqual(called, 1)
|
|
})
|
|
})
|
|
})
|