Files
Foundry-VTT-Docker/resources/app/common/utils/collection.mjs
2025-01-04 00:34:03 +01:00

228 lines
7.5 KiB
JavaScript

/**
* A reusable storage concept which blends the functionality of an Array with the efficient key-based lookup of a Map.
* This concept is reused throughout Foundry VTT where a collection of uniquely identified elements is required.
* @template {string} K
* @template {*} V
* @extends {Map<K, V>}
*/
class Collection extends Map {
constructor(entries) {
super(entries);
}
/* -------------------------------------------- */
/**
* Then iterating over a Collection, we should iterate over its values instead of over its entries
* @returns {IterableIterator<V>}
*/
[Symbol.iterator]() {
return this.values();
}
/* -------------------------------------------- */
/**
* Return an Array of all the entry values in the Collection
* @type {V[]}
*/
get contents() {
return Array.from(this.values());
}
/* -------------------------------------------- */
/**
* Find an entry in the Map using a functional condition.
* @see {Array#find}
* @param {function(*,number,Collection): boolean} condition The functional condition to test. Positional
* arguments are the value, the index of iteration, and the collection being searched.
* @return {*} The value, if found, otherwise undefined
*
* @example Create a new Collection and reference its contents
* ```js
* let c = new Collection([["a", "A"], ["b", "B"], ["c", "C"]]);
* c.get("a") === c.find(entry => entry === "A"); // true
* ```
*/
find(condition) {
let i = 0;
for ( let v of this.values() ) {
if ( condition(v, i, this) ) return v;
i++;
}
return undefined;
}
/* -------------------------------------------- */
/**
* Filter the Collection, returning an Array of entries which match a functional condition.
* @see {Array#filter}
* @param {function(*,number,Collection): boolean} condition The functional condition to test. Positional
* arguments are the value, the index of iteration, and the collection being filtered.
* @return {Array<*>} An Array of matched values
*
* @example Filter the Collection for specific entries
* ```js
* let c = new Collection([["a", "AA"], ["b", "AB"], ["c", "CC"]]);
* let hasA = c.filters(entry => entry.slice(0) === "A");
* ```
*/
filter(condition) {
const entries = [];
let i = 0;
for ( let v of this.values() ) {
if ( condition(v, i , this) ) entries.push(v);
i++;
}
return entries;
}
/* -------------------------------------------- */
/**
* Apply a function to each element of the collection
* @see Array#forEach
* @param {function(*): void} fn A function to apply to each element
*
* @example Apply a function to each value in the collection
* ```js
* let c = new Collection([["a", {active: false}], ["b", {active: false}], ["c", {active: false}]]);
* c.forEach(e => e.active = true);
* ```
*/
forEach(fn) {
for ( let e of this.values() ) {
fn(e);
}
}
/* -------------------------------------------- */
/**
* Get an element from the Collection by its key.
* @param {string} key The key of the entry to retrieve
* @param {object} [options] Additional options that affect how entries are retrieved
* @param {boolean} [options.strict=false] Throw an Error if the requested key does not exist. Default false.
* @return {*|undefined} The retrieved entry value, if the key exists, otherwise undefined
*
* @example Get an element from the Collection by key
* ```js
* let c = new Collection([["a", "Alfred"], ["b", "Bob"], ["c", "Cynthia"]]);
* c.get("a"); // "Alfred"
* c.get("d"); // undefined
* c.get("d", {strict: true}); // throws Error
* ```
*/
get(key, {strict=false}={}) {
const entry = super.get(key);
if ( strict && (entry === undefined) ) {
throw new Error(`The key ${key} does not exist in the ${this.constructor.name} Collection`);
}
return entry;
}
/* -------------------------------------------- */
/**
* Get an entry from the Collection by name.
* Use of this method assumes that the objects stored in the collection have a "name" attribute.
* @param {string} name The name of the entry to retrieve
* @param {object} [options] Additional options that affect how entries are retrieved
* @param {boolean} [options.strict=false] Throw an Error if the requested name does not exist. Default false.
* @return {*} The retrieved entry value, if one was found, otherwise undefined
*
* @example Get an element from the Collection by name (if applicable)
* ```js
* let c = new Collection([["a", "Alfred"], ["b", "Bob"], ["c", "Cynthia"]]);
* c.getName("Alfred"); // "Alfred"
* c.getName("D"); // undefined
* c.getName("D", {strict: true}); // throws Error
* ```
*/
getName(name, {strict=false} = {}) {
const entry = this.find(e => e.name === name);
if ( strict && (entry === undefined) ) {
throw new Error(`An entry with name ${name} does not exist in the collection`);
}
return entry ?? undefined;
}
/* -------------------------------------------- */
/**
* Transform each element of the Collection into a new form, returning an Array of transformed values
* @param {function(*,number,Collection): *} transformer A transformation function applied to each entry value.
* Positional arguments are the value, the index of iteration, and the collection being mapped.
* @return {Array<*>} An Array of transformed values
*/
map(transformer) {
const transformed = [];
let i = 0;
for ( let v of this.values() ) {
transformed.push(transformer(v, i, this));
i++;
}
return transformed;
}
/* -------------------------------------------- */
/**
* Reduce the Collection by applying an evaluator function and accumulating entries
* @see {Array#reduce}
* @param {function(*,*,number,Collection): *} reducer A reducer function applied to each entry value. Positional
* arguments are the accumulator, the value, the index of iteration, and the collection being reduced.
* @param {*} initial An initial value which accumulates with each iteration
* @return {*} The accumulated result
*
* @example Reduce a collection to an array of transformed values
* ```js
* let c = new Collection([["a", "A"], ["b", "B"], ["c", "C"]]);
* let letters = c.reduce((s, l) => {
* return s + l;
* }, ""); // "ABC"
* ```
*/
reduce(reducer, initial) {
let accumulator = initial;
let i = 0;
for ( let v of this.values() ) {
accumulator = reducer(accumulator, v, i, this);
i++;
}
return accumulator;
}
/* -------------------------------------------- */
/**
* Test whether a condition is met by some entry in the Collection.
* @see {Array#some}
* @param {function(*,number,Collection): boolean} condition The functional condition to test. Positional
* arguments are the value, the index of iteration, and the collection being tested.
* @return {boolean} Was the test condition passed by at least one entry?
*/
some(condition) {
let i = 0;
for ( let v of this.values() ) {
const pass = condition(v, i, this);
i++;
if ( pass ) return true;
}
return false;
}
/* -------------------------------------------- */
/**
* Convert the Collection to a primitive array of its contents.
* @returns {object[]} An array of contained values
*/
toJSON() {
return this.map(e => e.toJSON ? e.toJSON() : e);
}
}
export default Collection;