From ba51f9a9296d3f1ebec73de3d67ba5b5fc4253e2 Mon Sep 17 00:00:00 2001 From: Maicol Battistini Date: Sat, 11 Nov 2023 16:06:12 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Added=20more=20methods=20to?= =?UTF-8?q?=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 + pnpm-lock.yaml | 9 ++ src/collection.ts | 358 +++++++++++++++++++++++++++++++--------------- 3 files changed, 258 insertions(+), 112 deletions(-) diff --git a/package.json b/package.json index f21f2b1..607333a 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "ts-node": "^10.9.1", "typedoc": "^0.25.3", "typescript": "^5.2.2" + }, + "dependencies": { + "ts-pattern": "^5.0.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0608158..818fb85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + ts-pattern: + specifier: ^5.0.5 + version: 5.0.5 + devDependencies: '@maicol07/eslint-config': specifier: ^2.3.0 @@ -3950,6 +3955,10 @@ packages: yn: 3.1.1 dev: true + /ts-pattern@5.0.5: + resolution: {integrity: sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA==} + dev: false + /tsconfig-paths@3.14.2: resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} dependencies: diff --git a/src/collection.ts b/src/collection.ts index 1597c9b..191bb87 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1,16 +1,26 @@ import {dataGet, value} from './helpers'; +import {match} from 'ts-pattern'; -export class Collection implements Iterable<[string, V]> { +export type CollectionKeyType = string | number; +export type CollectionInputType = V[] | [K, V][] | Iterable | Collection | Record | Map; + +export class Collection implements Iterable { + [index: number]: V; /** * The items contained in the collection */ - private items: Record; + private items: Map; /** - * Create a new collection. + * Creates a new instance of the Collection class. + * + * @param items - Items to be added to the collection. + * + * @return A new Proxy object for the Collection instance. + * + * @throws {TypeError} If the items parameter is not of type V, V array, iterable, Collection, or Record. */ - constructor(items?: V | V[] | Collection | Record) { - // @ts-ignore + constructor(items: CollectionInputType = []) { this.items = this.getObjectableItems(items); return new Proxy(this, { @@ -26,30 +36,17 @@ export class Collection start + (index * step)) - ); + yield this.get(this.itCount!); } /** * Get all the items in the collection. */ - public all() { - return this.isArray() ? this.values() : this.items; + public all(): K extends number ? V[] : Map { + return (this.isArray() ? this.values().all() : this.items) as K extends number ? V[] : Map; } /** @@ -65,7 +62,7 @@ export class Collection unknown) | string) { const mapper = this.valueRetriever(callback); - const items = this.map((item) => mapper(item)).filter((item) => item !== null); + const items = this.map((item) => mapper(item)).filter(); const count = items.count(); if (count) { @@ -75,10 +72,8 @@ export class Collection> { + // public chunk(size: number): Collection> { + // // if (size <= 0) { // return new Collection(); // } @@ -94,19 +89,18 @@ export class Collection(this.all()); } /** @@ -138,43 +132,54 @@ export class Collection boolean} Predicate to test every entry + * @param key + * @param operator + * @param value + * @return boolean */ - public contains(item: V | ((value: V, key: string) => boolean) | [string, V | unknown]) { - let callback: (value: V, key: string) => boolean = () => false; - if (this.useAsCallable(item)) { - callback = item as (value: V, key: string) => boolean; - } + public contains(key: ((value: V, key: K) => boolean) | V, operator: any = null, value: any = null): boolean { + if (arguments.length === 1) { + if (typeof key === 'function') { + let placeholder = {}; - if (Array.isArray(item)) { - // @ts-ignore - callback = (value, key) => [key, value] === item; + return this.first(key as (value: V, key: K) => boolean, placeholder) !== placeholder; + } + + return [...this.items.values()].includes(key); } - if (callback) { - let result = false; + return this.contains(this.operatorForWhere(...arguments)); + } - for (const [key, value] of this.entries()) { - // @ts-ignore - result = callback(value, key); - if (result) { - return result; - } - } + /** + * Determine if an item exists, using strict comparison. + * + * @param key A function that returns a boolean type, a TValue, or an array key + * @param value A TValue or null + * @return boolean + */ + public containsStrict(key: ((item: V) => boolean) | V, value: any = null): boolean { + if (arguments.length === 2) { + return this.contains((item: any) => dataGet(item, key) === value); } + + if (typeof key === 'function') { + return this.first(key as (item: V) => boolean) !== null; + } + + return this.contains(key); } + // TODO: crossJoin, diff, diffUsing, diffAssoc, diffAssocUsing, diffKeys, diffKeysUsing, duplicates, duplicatesStrict, + /** * Count the amount of items in the collection. */ public count() { - return this.keys().length; + return this.items.size; } - /** * Dump the items and end the script execution. */ @@ -193,24 +198,24 @@ export class Collection) { + const itemsValues = [...this.getObjectableItems(items).values()]; return new Collection(this.values().filter((v) => !itemsValues.includes(v))); } /** * Get the items in the collection that are not present in the given items. */ - public diffAssoc(items: V) { - // const itemsEntries = Object.entries(this.getObjectableItems(items)); - // return new Collection(this.values().filter((v) => !itemsEntries.includes(v))); + public diffAssoc(items: CollectionInputType) { + const itemsEntries = [...this.getObjectableItems(items).entries()]; + return new Collection(this.entries().filter((v) => !itemsEntries.includes(v))); } /** * Get the items in the collection that are not present in the given items. */ - public diffKeys(items: V) { - const itemsKeys = Object.keys(this.getObjectableItems(items)); + public diffKeys(items: CollectionInputType) { + const itemsKeys = [...this.getObjectableItems(items).keys()] return new Collection(this.keys().filter((v) => !itemsKeys.includes(v))); } @@ -221,7 +226,7 @@ export class Collection boolean} Predicate to test every entry */ - public doesntContain(item: V | ((value: V, key: string) => boolean) | [string, V | unknown]) { + public doesntContain(item: V | ((value: V, key: K) => boolean)) { return !this.contains(item); } @@ -236,9 +241,27 @@ export class Collection string)) { - const items = this.valueRetriever(callback); - //const unique = items.unique(); + // public duplicates(callback?: string | ((item: V) => string)) { + // const items = this.valueRetriever(callback); + // //const unique = items.unique(); + // } + + /** + * Execute a callback over each item. + * + * @param callback - The callback function to be applied to each element. + * The callback should accept two parameters: item and key. + * The item parameter represents the value of the current element. + * The key parameter represents the key of the current element. + * @return Returns the collection itself. + */ + public each(callback: (item: V, key: CollectionKeyType) => unknown) { + for (const [key, value] of this.entries()) { + if (callback(value, key) === false) { + break; + } + } + return this; } /** @@ -246,21 +269,51 @@ export class Collection boolean | V, operator?: unknown, value?: unknown): boolean { + if (arguments.length === 1) { + const callback = this.valueRetriever(key); + for (const [key, value] of this) { + if (!callback(value, key)) { + return false; + } + } + + return true; + } + + return this.every(this.operatorForWhere(...arguments)); } /** * Run a filter over each of the items. */ - public filter(callback?: (value: V, key: string) => boolean) { - if (callback) { - return new Collection( - // @ts-ignore - Object.fromEntries(this.entries().filter(([key, item]) => callback(item, key))) - ); + public filter(callback?: (value: V, key: K) => boolean): Collection { + return new Collection(this.entries().filter(([key, value]) => callback ? callback(value, key) : Boolean(value))); + } + + public first(callback?: (value: V, key: K) => boolean, defaultValue?: D | (() => D)): V | D | undefined { + if (!callback) { + if (this.items.size === 0) { + return value(defaultValue); + } + + return this.items.values().next().value; } - return new Collection(this.values().filter(Boolean)); + for (let [key, value] of this.items) { + if (callback(value, key)) { + return value; + } + } + + return value(defaultValue); + } + + public flatten(depth: number = 1): Collection { + return new Collection(this.values().flatten(depth)); } /** @@ -268,7 +321,7 @@ export class Collection(this.items.keys()); } /** * Run a map over each of the items. */ - public map(callback: (item: V, key: string) => T) { - const newObject = Object.fromEntries( - // @ts-ignore - this.entries().map(([key, item]) => [key, callback(item, key)]) - ); + public map(callback: (item: V, key: K) => T) { + const newObject = Object.fromEntries(this.entries().map(([key, item]) => [key, callback(item, key)])); return new Collection(newObject); } + /** + * Chunk the collection into chunks of the given size + */ + + public median(key?: K) { + const values = (key ? this.pluck(key) : this) + .filter() + .sort() + .values(); + const count = values.count(); + + if (count === 0) { + return Number.NaN; + } + + const middle = Math.floor(count / 2); + + if (count % 2) { + return values.get(middle); + } + + return (values.get(middle - 1) + values.get(middle)) / 2; + } + + public mode(key?: K) { + if (this.count() === 0) { + return Number.NaN; + } + + const collection: Collection = key ? this.pluck(key) : this; + const counts = new Collection(); + collection.each((value) => counts[value] = counts[value] ? counts[value] + 1 : 1); + + /* + $sorted = $counts->sort(); + + $highestValue = $sorted->last(); + + return $sorted->filter(fn ($value) => $value == $highestValue) + ->sort()->keys()->all(); + */ + let sorted = counts.sort(); + + let highestValue: number = sorted.last(); + + return sorted.filter((value: number) => value == highestValue) + .sort().map((value, index) => index); + } + + public pluck(value: string | number, key?: string | number) { + let results = new Collection(); + + const v = typeof value === 'string' ? value.split('.') : value; + const k = typeof key === 'string' ? key.split('.') : key; + + for (let [, item] of this.items) { + let itemValue = dataGet(item, value); + + // If the key is "null", we will just append the value to the array and keep + // looping. Otherwise, we will key the array using the value of the key we + // received from the developer. Then we'll return the final array form. + if (!key) { + results.put('0', itemValue); + } else { + let itemKey = dataGet(item, key) as string | object; + + if (itemKey && typeof itemKey === "object") { + itemKey = itemKey.toString(); + } + + results.put(itemKey, itemValue); + } + } + + return results; + } + /** * Put an item in the collection by key. */ - public put(key: string, newValue: V) { - if (this.isArray() && Number.isNaN(Number.parseInt(key, 10))) { - // @ts-ignore - this.items = {}; + public put(key: K, newValue: V) { + if (this.isArray() && Number.isNaN(Number.parseInt(key as string, 10))) { + this.items = new Map(); } - // @ts-ignore - this.items[key] = newValue; + this.items.set(key, newValue); return this; } + /** + * Create a collection with the given range. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#sequence_generator_range + * @return static + */ + public static range(start: number, stop: number, step = 1) { + return new Collection( + Array.from({length: (stop - start) / step + 1}, (_, index) => start + (index * step)) + ); + } + /** * Reduce the collection to a single value. */ @@ -323,6 +460,10 @@ export class Collection number) { + return new Collection(this.entries().sort(((a, b) => callback ? callback(a[1], b[1]) : String(a[1]).localeCompare(String(b[1]))))); + } + /** * Get the sum of the given values. */ @@ -332,29 +473,29 @@ export class Collection result as number + (callback(item) as number), 0); } + /** + * Reset the keys on the underlying array. + */ public values() { - return Object.values(this.items); + return new Collection(this.items.values()); } /** * Prepare items to be added to the {@link items} property. */ - protected getObjectableItems(items?: V | V[] | Collection | Record) { - let localItems = items; - if (localItems instanceof Collection) { - // @ts-ignore - localItems = localItems.all(); - } - - if (Array.isArray(localItems)) { - return Object.fromEntries(localItems.entries()); - } - - if (localItems && typeof localItems !== 'object') { - return {0: localItems}; - } - - return localItems ?? {}; + protected getObjectableItems(items: CollectionInputType): Map { + return match(items) + .returnType>() + .when((value) => value instanceof Collection, (value: Collection) => { + const values = value.all(); + return Array.isArray(values) ? new Map(values.map((item, index) => [index as K, item])) : this.getObjectableItems(values); + }) + .when((value) => value instanceof Map, (value: Map) => value) + // .when((value) => value instanceof Set, (value: Set) => new Map(value.entries())) + .when((value) => Array.isArray(value), (value: V[] | [K, V][]) => { + return value.every((item) => Array.isArray(item) && item.length == 2) ? new Map(value as [K, V][]) : new Map((value as V[]).map((item, index) => [index as K, item])); + }) + .otherwise((value) => new Map(Object.entries(value)) as Map); } /** @@ -364,19 +505,12 @@ export class Collection !Number.isNaN(Number.parseInt(key, 10))); } - /** - * Determine if the given value is callable, but not a string. - */ - protected useAsCallable(item: any) { - return typeof item !== 'string' && typeof item === 'function'; - } - /** * Get a value retrieving callback. */ - protected valueRetriever(callback?: ((item: V) => unknown) | string) { - if (this.useAsCallable(callback)) { - return callback as (item: V) => unknown; + protected valueRetriever(callback?: Function | string) { + if (typeof callback === 'function') { + return callback; } return (item: V) => dataGet(item, callback as string);