array/collection.js

/**
* The collection class, ported from [Eris](https://abal.moe/Eris)
* <br>
* Hold a bunch of values
* @since v2.5.2-beta.1
* @extends Map
* @prop {Class} baseObject - The base class for all items
* @prop {Number?} limit - Max number of items to hold
*/
class Collection extends Map {
	/**
    * Construct a Collection
    * @arg {Class} baseObject - The base class for all items
    * @arg {Number} [limit] - Max number of items to hold
    */
	constructor(baseObject, limit) {
		super();
		this.baseObject = baseObject;
		this.limit = limit;
	}

	/**
    * Update an object
    * @arg {Object} obj - The updated object data
    * @arg {String} obj.id - The ID of the object
    * @arg {Class} [extra] - An extra parameter the constructor may need
    * @arg {Boolean} [replace] - Whether to replace an existing object with the same ID
    * @returns {Class} The updated object
    */
	update(obj, extra, replace) {
		if(!obj.id && obj.id !== 0) {
			throw new Error('Missing object id');
		}
		const item = this.get(obj.id);
		if(!item) {
			return this.add(obj, extra, replace);
		}
		item.update(obj, extra);
		return item;
	}

	/**
    * Add an object
    * @arg {Object} obj - The object data
    * @arg {String} obj.id - The ID of the object
    * @arg {Class} [extra] - An extra parameter the constructor may need
    * @arg {Boolean} [replace] - Whether to replace an existing object with the same ID
    * @returns {Class} The existing or newly created object
    */
	add(obj, extra, replace) {
		if(this.limit === 0) {
			return (obj instanceof this.baseObject || obj.constructor.name === this.baseObject.name) ? obj : new this.baseObject(obj, extra);
		}
		if(obj.id == null) {
			throw new Error('Missing object id');
		}
		const existing = this.get(obj.id);
		if(existing && !replace) {
			return existing;
		}
		if(!(obj instanceof this.baseObject || obj.constructor.name === this.baseObject.name)) {
			obj = new this.baseObject(obj, extra);
		}

		this.set(obj.id, obj);

		if(this.limit && this.size > this.limit) {
			const iter = this.keys();
			while(this.size > this.limit) {
				this.delete(iter.next().value);
			}
		}
		return obj;
	}

	/**
     * Returns true if all elements satisfy the condition
     * @arg {Function} func - A function that takes an object and returns true or false
     * @returns {Boolean} Whether or not all elements satisfied the condition
     */
	every(func) {
		for(const item of this.values()) {
			if(!func(item)) {
				return false;
			}
		}
		return true;
	}

	/**
    * Return all the objects that make the function evaluate true
    * @arg {Function} func - A function that takes an object and returns true if it matches
    * @returns {Array<Class>} An array containing all the objects that matched
    */
	filter(func) {
		const arr = [];
		for(const item of this.values()) {
			if(func(item)) {
				arr.push(item);
			}
		}
		return arr;
	}

	/**
    * Return the first object to make the function evaluate true
    * @arg {Function} func - A function that takes an object and returns true if it matches
    * @returns {Class?} The first matching object, or undefined if no match
    */
	find(func) {
		for(const item of this.values()) {
			if(func(item)) {
				return item;
			}
		}
		return undefined;
	}

	/**
    * Return an array with the results of applying the given function to each element
    * @arg {Function} func - A function that takes an object and returns something
    * @returns {Array} An array containing the results
    */
	map(func) {
		const arr = [];
		for(const item of this.values()) {
			arr.push(func(item));
		}
		return arr;
	}

	/**
    * Get a random object from the Collection
    * @returns {Class?} The random object, or undefined if there is no match
    */
	random() {
		const index = Math.floor(Math.random() * this.size);
		const iter = this.values();
		for(let i = 0; i < index; ++i) {
			iter.next();
		}
		return iter.next().value;
	}

	/**
     * Returns a value resulting from applying a function to every element of the collection
     * @arg {Function} func - A function that takes the previous value and the next item and returns a new value
     * @arg {any} [initialValue] - The initial value passed to the function
     * @returns {any} The final result
     */
	reduce(func, initialValue) {
		const iter = this.values();
		let val;
		let result = initialValue === undefined ? iter.next().value : initialValue;
		while((val = iter.next().value) !== undefined) {
			result = func(result, val);
		}
		return result;
	}

	/**
    * Remove an object
    * @arg {Object} obj - The object
    * @arg {String} obj.id - The ID of the object
    * @returns {Class?} The removed object, or null if nothing was removed
    */
	remove(obj) {
		const item = this.get(obj.id);
		if(!item) {
			return null;
		}
		this.delete(obj.id);
		return item;
	}

	/**
     * Returns true if at least one element satisfies the condition
     * @arg {Function} func - A function that takes an object and returns true or false
     * @returns {Boolean} Whether or not at least one element satisfied the condition
     */
	some(func) {
		for(const item of this.values()) {
			if(func(item)) {
				return true;
			}
		}
		return false;
	}

	toString() {
		return `[Collection<${this.baseObject.name}>]`;
	}

	toJSON() {
		const json = {};
		for(const item of this.values()) {
			json[item.id] = item;
		}
		return json;
	}
}

export { Collection };