UNPKG

@daiso-tech/core

Version:

The library offers flexible, framework-agnostic solutions for modern web applications, built on adaptable components that integrate seamlessly with popular frameworks like Next Js.

863 lines 29.6 kB
/** * @module Collection */ import { isIterable } from "../../../collection/implementations/_shared.js"; import { ItemNotFoundCollectionError, MultipleItemsFoundCollectionError, UnexpectedCollectionError, TypeCollectionError, EmptyCollectionError, } from "../../../collection/contracts/_module-exports.js"; import { isInvokable, resolveInvokable, } from "../../../utilities/_module-exports.js"; import { resolveLazyable } from "../../../utilities/_module-exports.js"; /** * All methods in `ListCollection` are executed eagerly. * * IMPORT_PATH: `"@daiso-tech/core/collection"` * @group Adapters */ export class ListCollection { /** * The `concat` static method is a convenient utility for easily concatenating multiple {@link Iterable | `Iterable`}. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * class MyIterable implements Iterable<number> { * *[Symbol.iterator](): Iterator<number> { * yield 1; * yield 2; * yield 3; * } * } * * const collection = ListCollection.concat([ * new MyIterable(), * new Set([1, 2, 3]), * new Map([["a", 1], ["b", 2]]), * ["a", "b", "c"] * ]); * collection.toArray(); * // [1, 2, 3, 1, 2, 3, ["a", 1], ["b", 2], "a", "b", "c"] * ``` */ static concat(iterables) { let array = []; for (const iterable of iterables) { array = [...array, ...iterable]; } return new ListCollection(array); } /** * The `difference` static method is used to compute the difference between two {@link Iterable | `Iterable`} instances. By default, the equality check is performed on each item. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * const collection = ListCollection.difference( * [1, 2, 2, 3, 4, 5], * [2, 4, 6, 8] * ); * collection.toArray(); * // [1, 3, 5] * ``` * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * const collection = ListCollection.difference( * [ * { name: "iPhone 6", brand: "Apple", type: "phone" }, * { name: "iPhone 5", brand: "Apple", type: "phone" }, * { name: "Apple Watch", brand: "Apple", type: "watch" }, * { name: "Galaxy S6", brand: "Samsung", type: "phone" }, * { name: "Galaxy Gear", brand: "Samsung", type: "watch" }, * ], * [ * { name: "Apple Watch", brand: "Apple", type: "watch" }, * ], * (product) => product.type * ); * collection.toArray(); * // [ * // { name: "iPhone 6", brand: "Apple", type: "phone" }, * // { name: "iPhone 5", brand: "Apple", type: "phone" }, * // { name: "Galaxy S6", brand: "Samsung", type: "phone" }, * // ] * ``` */ static difference(iterableA, iterableB, selectFn) { return new ListCollection(iterableA).difference(iterableB, selectFn); } /** * The `zip` static method merges together the values of `iterableA` with the values of the `iterableB` at their corresponding index. * The returned collection has size of the shortest collection. * @example * ```ts * import { ListCollection } from "@daiso-tech/core";; * * const collection = ListCollection.zip(["Chair", "Desk"], [100, 200]); * collection.toArray(); * // [["Chair", 100], ["Desk", 200]] * ``` * @example * ```ts * import { ListCollection } from "@daiso-tech/core";; * * const collection = ListCollection.zip(["Chair", "Desk", "Couch"], [100, 200]); * collection.toArray(); * // [["Chair", 100], ["Desk", 200]] * ``` * @example * ```ts * import { ListCollection } from "@daiso-tech/core";; * * const collection = ListCollection.zip(["Chair", "Desk"], [100, 200, 300]); * collection.toArray(); * // [["Chair", 100], ["Desk", 200]] * ``` */ static zip(iterableA, iterableB) { return new ListCollection(iterableA).zip(iterableB); } static deserialize(serializedValue) { return new ListCollection(serializedValue); } array; /** * The `constructor` takes an {@link Iterable | `Iterable`}. * * Works with `Array`. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * const collection = new ListCollection([1, 2, 3, 4]); * ``` * * Works with `String`. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * const collection = new ListCollection("ABCDE"); * ``` * * Works with `Set`. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * const collection = new ListCollection(new Set([1, 2, 2 4])); * ``` * * Works with `Map`. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * const collection = new ListCollection(new Map([["a", 1], ["b", 2]])); * ``` * * Works with any `Iterable`. * @example * ```ts * import { ListCollection } from "@daiso-tech/core"; * * class MyIterable implements Iterable<number> { * *[Symbol.iterator](): Iterator<number> { * yield 1; * yield 2; * yield 3; * } * } * const collection = new ListCollection(new MyIterable()); * ``` */ constructor(iterable = []) { this.array = [...iterable]; } serialize() { return this.toArray(); } *[Symbol.iterator]() { yield* this.array; } toIterator() { return this[Symbol.iterator](); } entries() { return new ListCollection(this.array.entries()); } keys() { return new ListCollection(this.array.keys()); } values() { return new ListCollection(this); } filter(predicateFn) { return new ListCollection(this.array.filter((item, index) => resolveInvokable(predicateFn)(item, index, this))); } reject(predicateFn) { return new ListCollection(this.array.filter((item, index) => !resolveInvokable(predicateFn)(item, index, this))); } map(mapFn) { return new ListCollection(this.array.map((item, index) => resolveInvokable(mapFn)(item, index, this))); } reduce(reduceFn, initialValue) { if (initialValue === undefined && this.isEmpty()) { throw new TypeCollectionError("Reduce of empty array must be inputed a initial value"); } if (initialValue !== undefined) { return this.array.reduce((initialValue, item, index) => resolveInvokable(reduceFn)(initialValue, item, index, this), initialValue); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.array.reduce((initialValue, item, index) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any const reduce = reduceFn; // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return reduce(initialValue, item, index, this); // eslint-disable-next-line @typescript-eslint/no-explicit-any }); } join(separator = ",") { for (const item of this) { if (typeof item !== "string") { throw new TypeCollectionError("Item type is invalid must be string"); } } return this.array.join(separator); } collapse() { const items = []; for (const item of this.array) { if (isIterable(item)) { items.push(...item); } else { items.push(item); } } return new ListCollection(items); } flatMap(mapFn) { return new ListCollection(this.array.flatMap((item, index) => [ ...resolveInvokable(mapFn)(item, index, this), ])); } change(predicateFn, mapFn) { return new ListCollection(this.array.map((item, index) => { if (resolveInvokable(predicateFn)(item, index, this)) { return resolveInvokable(mapFn)(item, index, this); } return item; })); } set(index, value) { if (index < 0) { return this; } if (index >= this.array.length) { return this; } const item = this.array[index]; if (item === undefined) { return this; } if (isInvokable(value)) { value = resolveInvokable(value)(item, index, this); } const newArray = [...this.array]; newArray[index] = value; return new ListCollection(newArray); } get(index) { return this.array[index] ?? null; } getOrFail(index) { const item = this.get(index); if (item === null) { throw new ItemNotFoundCollectionError("Item was not found"); } return item; } page(page, pageSize) { if (page < 0) { return this.skip(page * pageSize).take(pageSize); } return this.skip((page - 1) * pageSize).take(pageSize); } sum() { if (this.isEmpty()) { throw new EmptyCollectionError("Collection is empty therby operation cannot be performed"); } const result = this.reduce((sum, item) => { if (typeof item !== "number") { throw new TypeCollectionError("Item type is invalid must be number"); } return sum + item; }, 0); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any return result; } average() { if (this.isEmpty()) { throw new EmptyCollectionError("Collection is empty therby operation cannot be performed"); } return (this.sum() / this.size()); } median() { if (this.isEmpty()) { throw new EmptyCollectionError("Collection is empty therby operation cannot be performed"); } const nbrs = this.array.map((item) => { if (typeof item !== "number") { throw new TypeCollectionError("Item type is invalid must be number"); } return item; }), index = Math.floor(this.array.length / 2), isEven = this.array.length % 2 === 0, a = nbrs[index]; if (a === undefined) { throw new UnexpectedCollectionError("Is in invalid state"); } const b = nbrs[index - 1]; if (isEven) { if (b === undefined) { throw new UnexpectedCollectionError("Is in invalid state"); } return ((a + b) / 2); } return a; } min() { if (this.isEmpty()) { throw new EmptyCollectionError("Collection is empty therby operation cannot be performed"); } let min = 0; for (const item of this.array) { if (typeof item !== "number") { throw new TypeCollectionError("Item type is invalid must be number"); } if (min === 0) { min = item; } else if (min > item) { min = item; } } return min; } max() { if (this.isEmpty()) { throw new EmptyCollectionError("Collection is empty therby operation cannot be performed"); } let max = 0; for (const item of this.array) { if (typeof item !== "number") { throw new TypeCollectionError("Item type is invalid must be number"); } if (max === 0) { max = item; } else if (max < item) { max = item; } } return max; } percentage(predicateFn) { if (this.isEmpty()) { throw new EmptyCollectionError("Collection is empty therby operation cannot be performed"); } return (this.count(predicateFn) / this.size()) * 100; } some(predicateFn) { return this.array.some((item, index) => resolveInvokable(predicateFn)(item, index, this)); } every(predicateFn) { return this.array.every((item, index) => resolveInvokable(predicateFn)(item, index, this)); } take(limit) { return new ListCollection(this.array.slice(0, limit)); } takeUntil(predicateFn) { const items = []; for (const [index, item] of this.array.entries()) { if (resolveInvokable(predicateFn)(item, index, this)) { break; } items.push(item); } return new ListCollection(items); } takeWhile(predicateFn) { return this.takeUntil((...arguments_) => !resolveInvokable(predicateFn)(...arguments_)); } skip(offset) { return new ListCollection(this.array.slice(offset)); } skipUntil(predicateFn) { let hasMatched = false; const items = []; for (const [index, item] of this.array.entries()) { if (!hasMatched) { hasMatched = resolveInvokable(predicateFn)(item, index, this); } if (hasMatched) { items.push(item); } } return new ListCollection(items); } skipWhile(predicateFn) { return this.skipUntil((...arguments_) => !resolveInvokable(predicateFn)(...arguments_)); } when(condition, callback) { if (condition) { return resolveInvokable(callback)(this); } return this; } whenEmpty(callback) { return this.when(this.isEmpty(), callback); } whenNot(condition, callback) { return this.when(!condition, callback); } whenNotEmpty(callback) { return this.when(this.isNotEmpty(), callback); } pipe(callback) { return resolveInvokable(callback)(this); } tap(callback) { resolveInvokable(callback)(new ListCollection(this)); return this; } chunk(chunkSize) { const chunks = []; for (let index = 0; index < this.size(); index += chunkSize) { chunks.push(new ListCollection(this.array.slice(index, index + chunkSize))); } return new ListCollection(chunks); } chunkWhile(predicateFn) { let currentChunk = new ListCollection([]); const chunks = []; for (const [index, item] of this.array.entries()) { if (index === 0) { currentChunk = currentChunk.append([item]); } else if (resolveInvokable(predicateFn)(item, index, currentChunk)) { currentChunk = currentChunk.append([item]); } else { chunks.push(currentChunk); currentChunk = new ListCollection([item]); } } chunks.push(currentChunk); return new ListCollection(chunks); } split(chunkAmount) { const size = this.size(), minChunkSize = Math.floor(size / chunkAmount), restSize = size % chunkAmount, chunkSizes = Array.from({ length: chunkAmount, }).fill(minChunkSize); for (let index = 1; index <= restSize; index++) { const chunkIndex = (index - 1) % chunkAmount; if (chunkSizes[chunkIndex]) { chunkSizes[chunkIndex] = chunkSizes[chunkIndex] + 1; } } let end = 0, start = 0; const chunks = []; for (const chunkSize of chunkSizes) { end += chunkSize; chunks.push(new ListCollection(this.array.slice(start, end))); start += chunkSize; } return new ListCollection(chunks); } partition(predicateFn) { const chunkA = [], chunkB = []; for (const [index, item] of this.array.entries()) { if (resolveInvokable(predicateFn)(item, index, this)) { chunkA.push(item); } else { chunkB.push(item); } } return new ListCollection([ new ListCollection(chunkA), new ListCollection(chunkB), ]); } sliding(chunkSize, step = chunkSize - 1) { let chunks = new ListCollection([]); if (step <= 0) { return new ListCollection([]); } for (let index = 0; index < this.size(); index += step) { const start = index; const end = index + chunkSize; chunks = chunks.append([this.slice(start, end)]); if (end >= this.size()) { break; } } return chunks; } groupBy(selectFn = (item) => item) { const map = new Map(); for (const [index, item] of this.array.entries()) { const key = resolveInvokable(selectFn)(item, index, this); let collection = map.get(key); if (collection === undefined) { collection = new ListCollection([]); map.set(key, collection); } map.set(key, collection.append([item])); } return new ListCollection( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any map); } countBy(selectFn = (item) => item) { const map = new Map(); for (const [index, item] of this.array.entries()) { const key = resolveInvokable(selectFn)(item, index, this); if (!map.has(key)) { map.set(key, 0); } const counter = map.get(key); if (counter !== undefined) { map.set(key, counter + 1); } } return new ListCollection(map); } unique(selectFn = (item) => item) { const set = new Set([]), items = []; for (const [index, item] of this.array.entries()) { const item_ = resolveInvokable(selectFn)(item, index, this); if (!set.has(item_)) { items.push(item); } set.add(item_); } return new ListCollection(items); } difference(iterable, selectFn = (item) => item) { const differenceCollection = new ListCollection(iterable); return this.filter((item, index, collection) => { return !differenceCollection.some((matchItem, matchIndex, matchCollection) => { return (resolveInvokable(selectFn)(item, index, collection) === resolveInvokable(selectFn)(matchItem, matchIndex, matchCollection)); }); }); } repeat(amount) { let collection = new ListCollection([]); for (let index = 0; index < amount; index++) { collection = collection.append(this); } return collection; } padStart(maxLength, fillItems) { const fillItemsArray = [...fillItems]; const size = this.size(); const repeat = Math.floor((maxLength - size) / fillItemsArray.length); const resultItemsArray = []; for (let index = 0; index < repeat; index++) { resultItemsArray.push(...fillItemsArray); } const restAmount = maxLength - (repeat * fillItemsArray.length + size); for (let index = 0; index < restAmount; index++) { const fillItem = fillItemsArray[index]; if (fillItem) { resultItemsArray.push(fillItem); } } resultItemsArray.push(...this); return new ListCollection(resultItemsArray); } padEnd(maxLength, fillItems) { const fillItemsArray = [...fillItems]; const size = this.size(); const repeat = Math.floor((maxLength - size) / fillItemsArray.length); const resultItemsArray = []; for (let index = 0; index < repeat; index++) { resultItemsArray.push(...fillItemsArray); } const restAmount = maxLength - (repeat * fillItemsArray.length + size); for (let index = 0; index < restAmount; index++) { const fillItem = fillItemsArray[index]; if (fillItem) { resultItemsArray.push(fillItem); } } resultItemsArray.unshift(...this); return new ListCollection(resultItemsArray); } slice(start, end) { return new ListCollection(this.array.slice(start, end)); } prepend(iterable) { return new ListCollection([...iterable, ...this.array]); } append(iterable) { return new ListCollection([...this.array, ...iterable]); } insertBefore(predicateFn, iterable) { const index = this.array.findIndex((item, index) => resolveInvokable(predicateFn)(item, index, this)); if (index === -1) { return new ListCollection(this.array); } const newArray = [...this.array]; newArray.splice(index, 0, ...iterable); return new ListCollection(newArray); } insertAfter(predicateFn, iterable) { const index = this.array.findIndex((item, index) => resolveInvokable(predicateFn)(item, index, this)); if (index === -1) { return new ListCollection(this.array); } const firstPart = this.array.slice(0, index + 1), lastPart = this.array.slice(index + 1); return new ListCollection([ ...firstPart, ...iterable, ...lastPart, ]); } crossJoin(iterable) { const array = []; for (const itemA of this) { for (const itemB of iterable) { array.push([ ...(isIterable(itemA) ? itemA : [itemA]), ...(isIterable(itemB) ? itemB : [itemB]), ]); } } return new ListCollection(array); } zip(iterable) { const iterableArray = [...iterable]; let size = iterableArray.length; if (size > this.size()) { size = this.size(); } const items = []; for (let index = 0; index < size; index++) { const itemA = this.array[index], itemB = iterableArray[index]; if (itemA === undefined || itemB === undefined) { continue; } items.push([itemA, itemB]); } return new ListCollection(items); } sort(comparator) { if (comparator === undefined) { return new ListCollection(this.array.sort()); } return new ListCollection(this.array.sort(resolveInvokable(comparator))); } reverse(_chunkSize) { return new ListCollection([...this.array].reverse()); } shuffle(mathRandom = Math.random) { const newArray = [...this.array]; for (let i = newArray.length - 1; i > 0; i--) { const j = Math.floor(mathRandom() * (i + 1)); const temp = newArray[i]; if (newArray[j] !== undefined) { newArray[i] = newArray[j]; } if (temp !== undefined) { newArray[j] = temp; } } return new ListCollection(newArray); } first(predicateFn) { return this.firstOr(null, predicateFn); } firstOr(defaultValue, predicateFn) { if (predicateFn) { for (const [index, item] of this.array.entries()) { if (resolveInvokable(predicateFn)(item, index, this)) { return item; } } } else { const firstItem = this.array[0]; if (firstItem) { return firstItem; } } return resolveLazyable(defaultValue); } firstOrFail(predicateFn) { const item = this.first(predicateFn); if (item === null) { throw new ItemNotFoundCollectionError("Item was not found"); } return item; } last(predicateFn) { return this.lastOr(null, predicateFn); } lastOr(defaultValue, predicateFn) { if (predicateFn) { let matchedItem = null; for (const [index, item] of this.array.entries()) { if (resolveInvokable(predicateFn)(item, index, this)) { matchedItem = item; } } if (matchedItem) { return matchedItem; } } else { const lastItem = this.array.at(-1); if (lastItem) { return lastItem; } } return resolveLazyable(defaultValue); } lastOrFail(predicateFn) { const item = this.last(predicateFn); if (item === null) { throw new ItemNotFoundCollectionError("Item was not found"); } return item; } before(predicateFn) { return this.beforeOr(null, predicateFn); } beforeOr(defaultValue, predicateFn) { for (const [index, item] of this.array.entries()) { const beforeItem = this.array[index - 1]; if (resolveInvokable(predicateFn)(item, index, this) && beforeItem !== undefined) { return beforeItem; } } return resolveLazyable(defaultValue); } beforeOrFail(predicateFn) { const item = this.before(predicateFn); if (item === null) { throw new ItemNotFoundCollectionError("Item was not found"); } return item; } after(predicateFn) { return this.afterOr(null, predicateFn); } afterOr(defaultValue, predicateFn) { for (const [index, item] of this.array.entries()) { const beforeItem = this.array[index + 1]; if (resolveInvokable(predicateFn)(item, index, this) && beforeItem !== undefined) { return beforeItem; } } return resolveLazyable(defaultValue); } afterOrFail(predicateFn) { const item = this.after(predicateFn); if (item === null) { throw new ItemNotFoundCollectionError("Item was not found"); } return item; } sole(predicateFn) { const matchedItems = []; for (const [index, item] of this.array.entries()) { if (resolveInvokable(predicateFn)(item, index, this)) { matchedItems.push(item); } if (matchedItems.length > 1) { throw new MultipleItemsFoundCollectionError("Multiple items were found"); } } const [matchedItem] = matchedItems; if (matchedItem === undefined) { throw new ItemNotFoundCollectionError("Item was not found"); } return matchedItem; } nth(step) { return this.filter((_item, index) => index % step === 0); } count(predicateFn) { return this.filter(predicateFn).size(); } size() { return this.array.length; } isEmpty() { return this.array.length === 0; } isNotEmpty() { return this.array.length !== 0; } searchFirst(predicateFn) { return this.array.findIndex((item, index) => resolveInvokable(predicateFn)(item, index, this)); } searchLast(predicateFn) { let matchedIndex = -1; for (const [index, item] of this.array.entries()) { if (resolveInvokable(predicateFn)(item, index, this)) { matchedIndex = index; } } return matchedIndex; } forEach(callback) { for (const [index, item] of this.array.entries()) { resolveInvokable(callback)(item, index, this); } } toArray() { return [...this.array]; } toRecord() { const record = {}; for (const item of this) { if (!Array.isArray(item)) { throw new TypeCollectionError("Item type is invalid must be a tuple of size 2 where first tuple item is a string or number or symbol"); } if (item.length !== 2) { throw new TypeCollectionError("Item type is invalid must be a tuple of size 2 where first tuple item is a string or number or symbol"); } const [key, value] = item; if (!(typeof key === "string" || typeof key === "number" || typeof key === "symbol")) { throw new TypeCollectionError("Item type is invalid must be a tuple of size 2 where first tuple item is a string or number or symbol"); } record[key] = value; } return record; } toMap() { const map = new Map(); for (const item of this) { if (!Array.isArray(item)) { throw new TypeCollectionError("Item type is invalid must be a tuple of size 2"); } if (item.length !== 2) { throw new TypeCollectionError("Item type is invalid must be a tuple of size 2"); } const [key, value] = item; map.set(key, value); } return map; } } //# sourceMappingURL=list-collection.js.map