mathjs/src/utils/map.js
2024-02-21 12:17:30 +01:00

253 lines
5.2 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
this[Symbol.iterator] = this.entries
}
keys () {
return Object.keys(this.wrappedObject).values()
}
get (key) {
return getSafeProperty(this.wrappedObject, key)
}
set (key, value) {
setSafeProperty(this.wrappedObject, key, value)
return this
}
has (key) {
return hasSafeProperty(this.wrappedObject, key)
}
entries () {
return mapIterator(this.keys(), key => [key, this.get(key)])
}
forEach (callback) {
for (const key of this.keys()) {
callback(this.get(key), key, this)
}
}
delete (key) {
delete this.wrappedObject[key]
}
clear () {
for (const key of this.keys()) {
this.delete(key)
}
}
get size () {
return Object.keys(this.wrappedObject).length
}
}
/**
* Create a map with two partitions: a and b.
* The set with bKeys determines which keys/values are read/written to map b,
* all other values are read/written to map a
*
* For example:
*
* const a = new Map()
* const b = new Map()
* const p = new PartitionedMap(a, b, new Set(['x', 'y']))
*
* In this case, values `x` and `y` are read/written to map `b`,
* all other values are read/written to map `a`.
*/
export class PartitionedMap {
/**
* @param {Map} a
* @param {Map} b
* @param {Set} bKeys
*/
constructor (a, b, bKeys) {
this.a = a
this.b = b
this.bKeys = bKeys
this[Symbol.iterator] = this.entries
}
get (key) {
return this.bKeys.has(key)
? this.b.get(key)
: this.a.get(key)
}
set (key, value) {
if (this.bKeys.has(key)) {
this.b.set(key, value)
} else {
this.a.set(key, value)
}
return this
}
has (key) {
return this.b.has(key) || this.a.has(key)
}
keys () {
return new Set([
...this.a.keys(),
...this.b.keys()
])[Symbol.iterator]()
}
entries () {
return mapIterator(this.keys(), key => [key, this.get(key)])
}
forEach (callback) {
for (const key of this.keys()) {
callback(this.get(key), key, this)
}
}
delete (key) {
return this.bKeys.has(key)
? this.b.delete(key)
: this.a.delete(key)
}
clear () {
this.a.clear()
this.b.clear()
}
get size () {
return [...this.keys()].length
}
}
/**
* Create a new iterator that maps over the provided iterator, applying a mapping function to each item
*/
function mapIterator (it, callback) {
return {
next: () => {
const n = it.next()
return (n.done)
? n
: {
value: callback(n.value),
done: false
}
}
}
}
/**
* 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
}