@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
JavaScript
/**
* @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