@ugo-code/streamline.js
Version:
A utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript
233 lines (232 loc) • 9.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArraySL = void 0;
/**
* An extended Array class providing convenient getters and powerful utility methods.
* It's fully generic, type-safe, and designed for seamless method chaining.
*
* @template T The type of elements in the array. Defaults to `unknown`.
*/
class ArraySL extends Array {
/**
* Creates a new ArraySL instance. This constructor is compatible with the
* standard Array constructor signatures.
*/
constructor(item = []) {
// This logic handles the different ways an Array can be constructed.
// 1. new ArraySL(5) -> called by .map() etc., creates an empty array of length 5.
// 2. new ArraySL(item1, item2) -> creates an array with the given items.
if (typeof item === "number") {
super(item);
}
else {
super(...item);
}
// This is the crucial fix for ES5 targets.
// It manually sets the prototype of the new instance to ArraySL.prototype.
Object.setPrototypeOf(this, new.target.prototype);
}
/**
* Ensures that methods inherited from Array (like .map, .filter, .slice)
* return an instance of ArraySL instead of a plain Array. This makes
* method chaining seamless.
*/
static get [Symbol.species]() {
return this;
}
/**
* Returns the first element of the array.
* @returns {T | undefined} The first element, or `undefined` if the array is empty.
*/
get first() {
return this[0];
}
/**
* Returns the last element of the array.
* @returns {T | undefined} The last element, or `undefined` if the array is empty.
*/
get last() {
return this[this.length - 1];
}
/**
* Returns a random element from the array.
* @returns {T | undefined} A random element, or `undefined` if the array is empty.
*/
get random() {
if (this.length === 0) {
return undefined;
}
return this[Math.floor(Math.random() * this.length)];
}
/**
* Returns the middle item or items of the array based on their index.
*
* This method finds the element(s) at the center of the array without any sorting.
* - If the array has an odd number of elements, it returns the single middle item.
* - If the array has an even number of elements, it returns the two middle items.
*
* This is distinct from a median calculation, which would require the array to be sorted by value first.
*
* @returns {ArraySL<T>} A new ArraySL containing the middle item(s). Returns an empty array if the source array is empty.
* @example
* // For an array with an odd length
* const oddList = new ArraySL(['a', 'b', 'c', 'd', 'e']);
* console.log(oddList.middle()); // Returns ArraySL['c']
*
* // For an array with an even length
* const evenList = new ArraySL([10, 20, 30, 40]);
* console.log(evenList.middle()); // Returns ArraySL[20, 30]
*
* // For an empty array
* const emptyList = new ArraySL([]);
* console.log(emptyList.middle()); // Returns ArraySL[]
*/
middle() {
if (this.length === 0) {
return new ArraySL();
}
const midIndex = Math.floor(this.length / 2);
if (this.length % 2 !== 0) {
// Odd length: return the single middle item by index
return new ArraySL([this[midIndex]]);
}
else {
// Even length: return the two middle items by index
return new ArraySL([this[midIndex - 1], this[midIndex]]);
}
}
/**
* Returns a new ArraySL containing only the unique elements from the original array.
* Uniqueness is based on the element itself or on a key generated by the provided accessor.
* This method is highly performant (O(n)).
*
* @param options An optional configuration object.
* - `accessor`: An optional function that takes an element and returns a value to be used
* for the uniqueness check. If not provided, the element itself is used. Keys that are
* objects or arrays are compared by their JSON string representation.
* @returns {ArraySL<T>} A new ArraySL instance with duplicate elements removed.
*/
unique(options = {}) {
const { accessor = (value) => value } = options;
const seen = new Set();
return this.filter((value, index, array) => {
const key = accessor(value, index, array);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
/**
* Returns a new ArraySL containing duplicate elements from the original array.
* This method is highly configurable and performant, operating in a single pass (O(n)).
*
* @param options An optional configuration object.
* - `accessor`: An optional function that takes an element and returns a value to be used
* for the uniqueness check. If not provided, the element itself is used. Keys that are
* objects or arrays are compared by their JSON string representation.
* - `mode`: Determines which duplicate items to include in the result.
* - 'all' (default): Returns all instances of items that are duplicates.
* - 'first': Returns only the first instance of each duplicated item.
* - 'subsequent': Returns only the duplicate instances that appear after the first one.
* @returns {ArraySL<T>} A new ArraySL instance containing the specified duplicate elements.
*/
duplicates(options = {}) {
const { mode = "all", accessor = (value) => value } = options;
const result = new ArraySL();
// The map tracks the state of each key using a dedicated object.
const seen = new Map();
this.forEach((item, index, array) => {
const key = accessor(item, index, array);
const state = seen.get(key);
if (state === undefined) {
// First time seeing this key. Store the item with processed: false.
seen.set(key, { item: item, processed: false });
}
else {
// This is a subsequent encounter (2nd, 3rd, etc.).
if (!state.processed) {
// This block runs only on the second encounter for a given key.
if (mode === "first" || mode === "all") {
// Add the original item we stored earlier.
result.push(state.item);
}
// Mark as processed so this block doesn't run again for this key.
state.processed = true;
}
// This part runs for all subsequent encounters (2nd, 3rd, 4th...).
if (mode === "subsequent" || mode === "all") {
result.push(item);
}
}
});
return result;
}
/**
* Finds the most frequently occurring item(s) in the array.
*
* This method identifies which items appear most often, based on the item itself or a
* key extracted from the item. If multiple items share the same highest frequency,
* all of them are returned.
*
* @param accessor An optional function to extract a comparable key from an item. If not provided,
* the item itself is used for comparison.
* @returns {ArraySL<T>} A new ArraySL containing the most frequent item(s).
* Returns an empty array if there is no unique most frequent item (e.g., in `[1, 1, 2, 2]`).
* @example
* const users = new ArraySL([
* { name: 'Alice', city: 'Paris' },
* { name: 'Bob', city: 'Tokyo' },
* { name: 'Charlie', city: 'Paris' }
* ]);
* console.log(users.mostFrequent((u) => u.city)); // Returns ArraySL containing the 'Alice' and 'Charlie' objects
*
* const numbers = new ArraySL([1, 2, 2, 3, 3, 3]);
* console.log(numbers.mostFrequent()); // Returns ArraySL[3, 3, 3]
*/
mostFrequent(accessor = (value) => value) {
if (this.length === 0) {
return new ArraySL();
}
const frequencies = new Map();
let maxFreq = 0;
this.forEach((value, index, array) => {
const key = accessor(value, index, array);
const newCount = (frequencies.get(key) || 0) + 1;
frequencies.set(key, newCount);
if (newCount > maxFreq) {
maxFreq = newCount;
}
});
if (maxFreq <= 1 && this.length > 1) {
return new ArraySL();
}
const modeKeys = new Set();
for (const [key, freq] of frequencies.entries()) {
if (freq === maxFreq) {
modeKeys.add(key);
}
}
if (modeKeys.size > 1 && (modeKeys.size * maxFreq) === this.length) {
return new ArraySL();
}
return this.filter((value, index, array) => modeKeys.has(accessor(value, index, array)));
}
filter(predicate, thisArg) {
// The implementation is inherited from Array, so we just call super.
return super.filter(predicate, thisArg);
}
// Overrides the default return type of `Array.prototype.map` to return `ArraySL<U>`.
map(callbackfn, thisArg) {
return super.map(callbackfn, thisArg);
}
// Overrides the default return type of `Array.prototype.slice` to return `ArraySL<T>`.
slice(start, end) {
return super.slice(start, end);
}
concat(...items) {
return super.concat(...items);
}
}
exports.ArraySL = ArraySL;