Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Jos de Jong 2024-08-01 11:17:20 +02:00
commit 4a119a3465
16 changed files with 185 additions and 75 deletions

View File

@ -114,10 +114,13 @@ Where :
- `args` is an Array with nodes of the parsed arguments.
- `math` is the math namespace against which the expression was compiled.
- `scope` is a `Map` containing the variables defined in the scope passed
via `evaluate(scope)`. In case of using a custom defined function like
`f(x) = rawFunction(x) ^ 2`, the scope passed to `rawFunction` also contains
the current value of parameter `x`.
- `scope` is a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
interface containing the variables defined in the scope
passed via `evaluate(scope)`. The passed scope is always a `Map` interface,
and normally a `PartitionedMap` is used to separate local function variables
like `x` in a custom defined function `f(x) = rawFunction(x) ^ 2` from the
scope variables. Note that a `PartitionedMap` can recursively link to another
`PartitionedMap`.
Raw functions must be imported in the `math` namespace, as they need to be
processed at compile time. They are not supported when passed via a scope

View File

@ -20,7 +20,7 @@ In this case, the expression `sqrt(2 + x)` is parsed as:
ConstantNode 2 x SymbolNode
```
Alternatively, this expression tree can be build by manually creating nodes:
Alternatively, this expression tree can be built by manually creating nodes:
```js
const node1 = new math.ConstantNode(2)

View File

@ -22,9 +22,19 @@ math.evaluate([expr1, expr2, expr3, ...], scope)
Function `evaluate` accepts a single expression or an array with
expressions as the first argument and has an optional second argument
containing a scope with variables and functions. The scope can be a regular
JavaScript Object, or Map. The scope will be used to resolve symbols, and to write
assigned variables or function.
containing a `scope` with variables and functions. The scope can be a regular
JavaScript `Map` (recommended), a plain JavaScript `object`, or any custom
class that implements the `Map` interface with methods `get`, `set`, `keys`
and `has`. The scope will be used to resolve symbols, and to write assigned
variables and functions.
When an `Object` is used as scope, mathjs will internally wrap it in an
`ObjectWrappingMap` interface since the internal functions can only use a `Map`
interface. In case of custom defined functions like `f(x) = x^2`, the scope
will be wrapped in a `PartitionedMap`, which reads and writes the function
variables (like `x` in this example) from a temporary map, and reads and writes
other variables from the original scope. The original scope is never copied, it
is only wrapped around when needed.
The following code demonstrates how to evaluate expressions.

View File

@ -2,8 +2,8 @@ import { all, create } from '../../lib/esm/index.js'
const math = create(all)
// The expression evaluator accepts an optional scope object.
// This is the symbol table for variable defintions and function declations.
// The expression evaluator accepts an optional scope Map or object that can
// be used to keep additional variables and functions.
// Scope can be a bare object.
function withObjectScope () {
@ -28,11 +28,11 @@ function withMapScope (scope, name) {
math.evaluate('area(length, width) = length * width * scalar', scope)
math.evaluate('A = area(x, y)', scope)
console.log(`Map-like scope (${name}):`, scope.localScope)
console.log(`Map-like scope (${name}):`, scope)
}
// This is a minimal set of functions to look like a Map.
class MapScope {
class CustomMap {
constructor () {
this.localScope = new Map()
}
@ -61,7 +61,7 @@ class MapScope {
* used in mathjs.
*
*/
class AdvancedMapScope extends MapScope {
class AdvancedCustomMap extends CustomMap {
constructor (parent) {
super()
this.parentScope = parent
@ -91,25 +91,19 @@ class AdvancedMapScope extends MapScope {
return this.localScope.clear()
}
/**
* Creates a child scope from this one. This is used in function calls.
*
* @returns a new Map scope that has access to the symbols in the parent, but
* cannot overwrite them.
*/
createSubScope () {
return new AdvancedMapScope(this)
}
toString () {
return this.localScope.toString()
}
}
// Use a plain JavaScript object
withObjectScope()
// Where safety is important, scope can also be a Map
withMapScope(new Map(), 'simple Map')
// Where flexibility is important, scope can duck type appear to be a Map.
withMapScope(new MapScope(), 'MapScope example')
// Extra methods allow even finer grain control.
withMapScope(new AdvancedMapScope(), 'AdvancedScope example')
// use a Map (recommended)
withMapScope(new Map(), 'Map example')
// Use a custom Map implementation
withMapScope(new CustomMap(), 'CustomMap example')
// Use a more advanced custom Map implementation
withMapScope(new AdvancedCustomMap(), 'AdvancedCustomMap example')

View File

@ -1,14 +1,14 @@
import typedFunction from 'typed-function'
import { deepFlatten, isLegacyFactory } from '../utils/object.js'
import * as emitter from './../utils/emitter.js'
import { importFactory } from './function/import.js'
import { configFactory } from './function/config.js'
import { ArgumentsError } from '../error/ArgumentsError.js'
import { DimensionError } from '../error/DimensionError.js'
import { IndexError } from '../error/IndexError.js'
import { factory, isFactory } from '../utils/factory.js'
import {
isAccessorNode,
isArray,
isArrayNode,
isAssignmentNode,
isBigInt,
isBigNumber,
isBlockNode,
isBoolean,
@ -26,30 +26,33 @@ import {
isHelp,
isIndex,
isIndexNode,
isMap,
isMatrix,
isNode,
isNull,
isNumber,
isObject,
isObjectNode,
isObjectWrappingMap,
isOperatorNode,
isParenthesisNode,
isPartitionedMap,
isRange,
isRangeNode,
isRelationalNode,
isRegExp,
isRelationalNode,
isResultSet,
isSparseMatrix,
isString,
isSymbolNode,
isUndefined,
isUnit,
isBigInt
isUnit
} from '../utils/is.js'
import { ArgumentsError } from '../error/ArgumentsError.js'
import { DimensionError } from '../error/DimensionError.js'
import { IndexError } from '../error/IndexError.js'
import { deepFlatten, isLegacyFactory } from '../utils/object.js'
import * as emitter from './../utils/emitter.js'
import { DEFAULT_CONFIG } from './config.js'
import { configFactory } from './function/config.js'
import { importFactory } from './function/import.js'
/**
* Create a mathjs instance from given factory functions and optionally config
@ -126,6 +129,9 @@ export function create (factories, config) {
isDate,
isRegExp,
isObject,
isMap,
isPartitionedMap,
isObjectWrappingMap,
isNull,
isUndefined,

View File

@ -36,11 +36,14 @@
* @returns {function} The created typed-function.
*/
import typedFunction from 'typed-function'
import { factory } from '../../utils/factory.js'
import {
isAccessorNode,
isArray,
isArrayNode,
isAssignmentNode,
isBigInt,
isBigNumber,
isBlockNode,
isBoolean,
@ -58,6 +61,7 @@ import {
isHelp,
isIndex,
isIndexNode,
isMap,
isMatrix,
isNode,
isNull,
@ -68,19 +72,16 @@ import {
isParenthesisNode,
isRange,
isRangeNode,
isRelationalNode,
isRegExp,
isRelationalNode,
isResultSet,
isSparseMatrix,
isString,
isSymbolNode,
isUndefined,
isUnit, isBigInt
isUnit
} from '../../utils/is.js'
import typedFunction from 'typed-function'
import { digits } from '../../utils/number.js'
import { factory } from '../../utils/factory.js'
import { isMap } from '../../utils/map.js'
// returns a new instance of typed-function
let _createTyped = function () {

View File

@ -29,6 +29,9 @@ export {
isString,
isUndefined,
isObject,
isMap,
isPartitionedMap,
isObjectWrappingMap,
isObjectNode,
isOperatorNode,
isParenthesisNode,

View File

@ -92,7 +92,7 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
* invoke a list with arguments on a node
* @param {./Node | string} fn
* Item resolving to a function on which to invoke
* the arguments, typically a SymboNode or AccessorNode
* the arguments, typically a SymbolNode or AccessorNode
* @param {./Node[]} args
*/
constructor (fn, args) {

View File

@ -12,6 +12,8 @@
// for security reasons, so these functions are not exposed in the expression
// parser.
import { ObjectWrappingMap } from './map.js'
export function isNumber (x) {
return typeof x === 'number'
}
@ -125,6 +127,38 @@ export function isObject (x) {
!isFraction(x))
}
/**
* Returns `true` if the passed object appears to be a Map (i.e. duck typing).
*
* Methods looked for are `get`, `set`, `keys` and `has`.
*
* @param {Map | object} object
* @returns
*/
export function isMap (object) {
// We can use the fast instanceof, or a slower duck typing check.
// The duck typing method needs to cover enough methods to not be confused with DenseMatrix.
if (!object) {
return false
}
return object instanceof Map ||
object instanceof ObjectWrappingMap ||
(
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.keys === 'function' &&
typeof object.has === 'function'
)
}
export function isPartitionedMap (object) {
return isMap(object) && isMap(object.a) && isMap(object.b)
}
export function isObjectWrappingMap (object) {
return isMap(object) && isObject(object.wrappedObject)
}
export function isNull (x) {
return x === null
}

View File

@ -1,5 +1,5 @@
import { setSafeProperty, hasSafeProperty, getSafeProperty } from './customs.js'
import { isObject } from './is.js'
import { getSafeProperty, hasSafeProperty, setSafeProperty } from './customs.js'
import { isMap, isObject } from './is.js'
/**
* A map facade on a bare object.
@ -202,30 +202,6 @@ export function toObject (map) {
return object
}
/**
* Returns `true` if the passed object appears to be a Map (i.e. duck typing).
*
* Methods looked for are `get`, `set`, `keys` and `has`.
*
* @param {Map | object} object
* @returns
*/
export function isMap (object) {
// We can use the fast instanceof, or a slower duck typing check.
// The duck typing method needs to cover enough methods to not be confused with DenseMatrix.
if (!object) {
return false
}
return object instanceof Map ||
object instanceof ObjectWrappingMap ||
(
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.keys === 'function' &&
typeof object.has === 'function'
)
}
/**
* Copies the contents of key-value pairs from each `objects` in to `map`.
*

View File

@ -181,6 +181,9 @@ const knownUndocumented = new Set([
'isDate',
'isRegExp',
'isObject',
'isMap',
'isPartitionedMap',
'isObjectWrappingMap',
'isNull',
'isUndefined',
'isAccessorNode',

View File

@ -2293,6 +2293,9 @@ Factory Test
math.isDate,
math.isRegExp,
math.isObject,
math.isMap,
math.isPartitionedMap,
math.isObjectWrappingMap,
math.isNull,
math.isUndefined,
math.isAccessorNode,

View File

@ -1,6 +1,7 @@
import assert from 'assert'
import math from '../../../src/defaultInstance.js'
import Decimal from 'decimal.js'
import { ObjectWrappingMap, PartitionedMap } from '../../../src/utils/map.js'
const math2 = math.create()
describe('typed', function () {
@ -177,6 +178,37 @@ describe('typed', function () {
assert.strictEqual(math.isNull(), false)
})
it('should test whether a value is an object', function () {
assert.strictEqual(math.isObject({}), true)
assert.strictEqual(math.isObject({ a: 2 }), true)
assert.strictEqual(math.isObject(Object.create({})), true)
assert.strictEqual(math.isObject(null), false)
assert.strictEqual(math.isObject([]), false)
assert.strictEqual(math.isObject(), false)
assert.strictEqual(math.isObject(undefined), false)
})
it('should test whether a value is a Map', function () {
assert.strictEqual(math.isMap({}), false)
assert.strictEqual(math.isMap(new Map()), true)
assert.strictEqual(math.isMap(new ObjectWrappingMap({})), true)
assert.strictEqual(math.isMap(new PartitionedMap(new Map(), new Map(), new Set(['x']))), true)
})
it('should test whether a value is a PartitionedMap', function () {
assert.strictEqual(math.isPartitionedMap({}), false)
assert.strictEqual(math.isPartitionedMap(new Map()), false)
assert.strictEqual(math.isPartitionedMap(new ObjectWrappingMap({})), false)
assert.strictEqual(math.isPartitionedMap(new PartitionedMap(new Map(), new Map(), new Set(['x']))), true)
})
it('should test whether a value is an ObjectWrappingMap', function () {
assert.strictEqual(math.isObjectWrappingMap({}), false)
assert.strictEqual(math.isObjectWrappingMap(new Map()), false)
assert.strictEqual(math.isObjectWrappingMap(new ObjectWrappingMap({})), true)
assert.strictEqual(math.isObjectWrappingMap(new PartitionedMap(new Map(), new Map(), new Set(['x']))), false)
})
it('should test whether a value is undefined', function () {
assert.strictEqual(math.isUndefined(undefined), true)
assert.strictEqual(math.isUndefined(math.matrix()), false)
@ -201,7 +233,7 @@ describe('typed', function () {
assert.strictEqual(math.isConstantNode(), false)
})
it('should test whether a value is a SymolNode', function () {
it('should test whether a value is a SymbolNode', function () {
assert.strictEqual(math.isSymbolNode(new math.SymbolNode('')), true)
assert.strictEqual(math.isSymbolNode(new math2.SymbolNode('')), true)
assert.strictEqual(math.isSymbolNode({ isSymbolNode: true }), false)

View File

@ -1,8 +1,10 @@
// test parse
import assert from 'assert'
import { approxEqual, approxDeepEqual } from '../../../tools/approx.js'
import math from '../../../src/defaultInstance.js'
import { isMap, isObjectWrappingMap, isPartitionedMap } from '../../../src/utils/is.js'
import { PartitionedMap } from '../../../src/utils/map.js'
import { approxDeepEqual, approxEqual } from '../../../tools/approx.js'
const parse = math.parse
const ConditionalNode = math.ConditionalNode
@ -1599,6 +1601,31 @@ describe('parse', function () {
assert.strictEqual(scope.a, false)
})
it('should always pass a Map as scope to a rawArgs function', function () {
const myMath = math.create()
function myFunction (args, _math, _scope) {
return {
type: isObjectWrappingMap(_scope)
? 'ObjectWrappingMap'
: isPartitionedMap(_scope)
? 'PartitionedMap'
: isMap(_scope)
? 'Map'
: 'unknown',
scope: _scope
}
}
myFunction.rawArgs = true
myMath.import({ myFunction })
assert.strictEqual(myMath.parse('myFunction()').evaluate({}).type, 'PartitionedMap')
const map = new Map()
assert.strictEqual(myMath.parse('myFunction()').evaluate(map).type, 'PartitionedMap')
assert.strictEqual(myMath.parse('myFunction()').evaluate(map).scope.a, map)
assert.strictEqual(myMath.parse('myFunction()').evaluate(new PartitionedMap(new Map(), new Map(), new Set('x'))).type, 'PartitionedMap')
assert.deepStrictEqual(myMath.parse('f(x) = myFunction(x); f(2)').evaluate(new Map()).entries[0].type, 'PartitionedMap')
})
it('should parse logical xor', function () {
assert.strictEqual(parseAndEval('2 xor 6'), false)
assert.strictEqual(parseAndEval('2 xor 0'), true)

View File

@ -1,5 +1,6 @@
import assert from 'assert'
import { isMap, ObjectWrappingMap, toObject, createMap, assign, PartitionedMap } from '../../../src/utils/map.js'
import { isMap } from '../../../src/utils/is.js'
import { assign, createMap, ObjectWrappingMap, PartitionedMap, toObject } from '../../../src/utils/map.js'
describe('maps', function () {
it('should provide isMap, a function to tell maps from non-maps', function () {

17
types/index.d.ts vendored
View File

@ -3381,6 +3381,14 @@ export interface MathJsInstance extends MathJsFactory {
isObject(x: unknown): boolean
isMap<T, U>(x: unknown): x is Map<T, U>
isPartitionedMap<T, U>(x: unknown): x is PartitionedMap<T, U>
isObjectWrappingMap<T extends string | number | symbol, U>(
x: unknown
): x is ObjectWrappingMap<T, U>
isNull(x: unknown): x is null
isUndefined(x: unknown): x is undefined
@ -4157,6 +4165,15 @@ export interface UnitDefinition {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Index {}
export interface PartitionedMap<T, U> {
a: Map<T, U>
b: Map<T, U>
}
export interface ObjectWrappingMap<T extends string | number | symbol, U> {
wrappedObject: Record<T, U>
}
export interface EvalFunction {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
evaluate(scope?: any): any