mathjs/src/utils/map.js
jhugman e80995d52d
Add support for scopes with get and set methods (#2166)
* Add support for scopes with get and set methods

* Fix build for node v12

* Fixup cli and parser tests

* Add tests for simplify and evaluate

* Add example for a custom scope object

* Function calls need child scopes

* Transitionary step: Separate Safe and Scope Property calls

* Renamed identifiers in FunctionNode

* Evaluate with ObjectScopeWrapper

* Simplify tests passing

* Assume all scopes are map-like. Except parser

* Remove isMapLike check in customs.*SafeProperty() methods

* Change MapLike to Map

* Move keywords from an Object to a Set

* Move ScopeProperty functions in to scope.js

* Removed deprecation warning

* Rename scope.js to map.js

* Rename ScopeProperty to MapProperty

* Add tests and docs for map.js

* Put back the micro-optimization of function calls

* Use Map in the parser

* Called scope methods directly in cli.js

* Coercing of scope into a Map is done in Node, not evaluate

* Move createSubScope to its own file

* Fixup following self-review

* Add scope docs

* Final self-review changes

* Address reviewer comments

* Remove MapProperty witness marks

* Converted broken benchmark possibly lost in a rebase

* Use bare map as scope in benchmark

Co-authored-by: Jos de Jong <wjosdejong@gmail.com>
2021-05-16 13:33:01 +02:00

130 lines
2.9 KiB
JavaScript

import { setSafeProperty, hasSafeProperty, getSafeProperty } from './customs.js'
import { isObject } from './is.js'
/**
* A map facade on a bare object.
*
* The small number of methods needed to implement a scope,
* forwarding on to the SafeProperty functions. Over time, the codebase
* will stop using this method, as all objects will be Maps, rather than
* more security prone objects.
*/
export class ObjectWrappingMap {
constructor (object) {
this.wrappedObject = object
}
keys () {
return Object.keys(this.wrappedObject)
}
get (key) {
return getSafeProperty(this.wrappedObject, key)
}
set (key, value) {
setSafeProperty(this.wrappedObject, key, value)
return this
}
has (key) {
return hasSafeProperty(this.wrappedObject, key)
}
}
/**
* Creates an empty map, or whatever your platform's polyfill is.
*
* @returns an empty Map or Map like object.
*/
export function createEmptyMap () {
return new Map()
}
/**
* Creates a Map from the given object.
*
* @param { Map | { [key: string]: unknown } | undefined } mapOrObject
* @returns
*/
export function createMap (mapOrObject) {
if (!mapOrObject) {
return createEmptyMap()
}
if (isMap(mapOrObject)) {
return mapOrObject
}
if (isObject(mapOrObject)) {
return new ObjectWrappingMap(mapOrObject)
}
throw new Error('createMap can create maps from objects or Maps')
}
/**
* Unwraps a map into an object.
*
* @param {Map} map
* @returns { [key: string]: unknown }
*/
export function toObject (map) {
if (map instanceof ObjectWrappingMap) {
return map.wrappedObject
}
const object = {}
for (const key of map.keys()) {
const value = map.get(key)
setSafeProperty(object, key, value)
}
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`.
*
* Object is `objects` can be a `Map` or object.
*
* This is the `Map` analog to `Object.assign`.
*/
export function assign (map, ...objects) {
for (const args of objects) {
if (!args) {
continue
}
if (isMap(args)) {
for (const key of args.keys()) {
map.set(key, args.get(key))
}
} else if (isObject(args)) {
for (const key of Object.keys(args)) {
map.set(key, args[key])
}
}
}
return map
}