@moyal/js-linq
Version:
A lightweight, zero-dependency JavaScript library that brings LINQ-style querying capabilities to native arrays. Designed for both clarity and performance, it offers a fluent, chainable API for filtering, projecting, grouping, ordering, and aggregating da
1,077 lines (950 loc) • 104 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MoyalLinq = {}));
})(this, (function (exports) { 'use strict';
/*!
* File: src/errors.js
*/
class Errors {
/*
* Error messages
*/
static #_errorMessages = Object.freeze({
ALL_MUST_BE_ITERABLE: "All arguments must be iterable",
ARGUMENT_MUST_NOT_BE_NULL_key: "key argument MUST NOT be null",
INPUT_SEQUENCE_CONTAINS_MORE_THAN_ONE_ELEMENT: "The input sequence contains more than one element",
MORE_THAN_ELEMENT_SEQUENCE_SATISFIES_THE_CONDITION: "More than one element satisfies the condition in predicate",
MUST_BE_NON_NEGATIVE_INTEGER_count: "count must be a non negative integer",
MUST_BE_NON_NEGATIVE_INTEGER_factor: "`factor` must be a non negative integer",
MUST_BE_NON_NEGATIVE_INTEGER_index: "index must be a non negative integer",
MUST_BE_FUNCTION_OR_NULLISH_collectionSelector: "collectionSelector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_comparer: "comparer must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_elementSelector: "elementSelector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_equalityComparer: "equalityComparer must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_keyEqualityComparer: "keyEqualityComparer must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_keySelector: "keySelector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_leftKeySelector: "leftKeySelector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_predicate: "predicate must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_resultSelector: "resultSelector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_rightKeySelector: "rightKeySelector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_selector: "selector must be a function or nullish",
MUST_BE_FUNCTION_OR_NULLISH_valueSelector: "valueSelector must be a function or nullish",
MUST_BE_FUNCTION_accumulator: "accumulator must be a function",
MUST_BE_FUNCTION_callback: "callback must be a function",
MUST_BE_FUNCTION_predicate: "predicate must be a function",
MUST_BE_FUNCTION_transform: "transform must be a function",
MUST_BE_INTEGER_count: "count must be an integer",
MUST_BE_INTEGER_start: "start must be an integer",
MUST_BE_ITERABLE_iterable: "'iterable' argument must be iterable",
MUST_BE_ITERABLE_iterable1: "iterable1 must be iterable",
MUST_BE_ITERABLE_iterable2: "iterable2 must be iterable",
MUST_BE_ITERABLE_rightIterable: "rightIterable must be iterable",
MUST_BE_ITERABLE_secondIterable: "secondIterable must be iterable",
MUST_BE_NUMBER_all_sequence_elements: "At least one of the sequence's element is not a number",
NO_ELEMENT_SATISFIES_THE_CONDITION_IN_predicate: "No element satisfies the condition in predicate",
OUT_OF_RANGE_index: "index is out of range",
PRODUCES_DUPLICATE_KEYS_keySelector: "keySelector produces duplicate keys",
SEQUENCE_IS_EMPTY: "The sequence is empty",
});
/**
* Exposes the error messages for better unit-test coding and other.
*/
static get Messages() { return this.#_errorMessages; }
}
/*!
* File: src/typeCheck.js
*/
/**
* A numeric value (number or bigint).
* @typedef {(number|bigint)} NumericValue
*/
/**
* Utility class for runtime type checking.
* @ignore
*/
class TypeCheck {
/**
* Checks if the value is a number (primitive or Number object).
* @param {any} obj
* @returns {boolean}
*/
static isNumber(obj) {
return typeof obj === "number" || Object.prototype.toString.call(obj) === "[object Number]";
}
/**
* Checks if the value is a bigint (primitive or BigInt object).
* @param {any} obj
* @returns {boolean}
*/
static isBigInt(obj) { return typeof obj === "bigint" || Object.prototype.toString.call(obj) === "[object BigInt]"; }
/**
* Checks if the value is an integer (number or bigint),
* optionally applying a custom predicate.
* @param {any} obj
* @param {function(NumericValue):boolean} [additionalPredicate]
* @returns {boolean}
*/
static isIntegral(obj, additionalPredicate) {
const isIntegral =
this.isBigInt(obj) ||
(this.isNumber(obj) && Math.floor(obj) === obj);
return isIntegral && (additionalPredicate == null || additionalPredicate(obj) === true);
}
/**
* Checks if the object is iterable (has a `[Symbol.iterator]` method).
* @param {any} obj
* @returns {boolean}
*/
static isIterable(obj) {
return this.isFunctionOrGeneratorFunction(obj?.[Symbol.iterator]);
}
/**
* Checks if the value is a standard function.
* @param {any} obj
* @returns {boolean}
*/
static isFunction(obj) {
const type = typeof obj;
return (type === "object" || type === "function") && Object.prototype.toString.call(obj) === "[object Function]";
}
/**
* Checks if the value is a function or a generator function.
* @param {any} obj
* @returns {boolean}
*/
static isFunctionOrGeneratorFunction(obj) {
const type = typeof obj;
const tag = Object.prototype.toString.call(obj);
return (type === "object" || type === "function") && (tag === "[object Function]" || tag === "[object GeneratorFunction]");
}
}
/**
* @module
* @ignore
*/
/**
* @ignore
*/
class BuildInfo {
static version = "1.0.4";
}
/**
* File: src/LinqStatistics.js
*/
/**
* Represents statistical measurements for a numeric sequence.
*
* Provides access to summary statistics (count, min, max, average, range, sum, variance, standard deviation)
* and, optionally, extended calculations (mode, median) if computed during construction.
*
* Use `clone()` to create a duplicate of an instance, and `toJSON()` to serialize it.
*
* @class
*/
class Statistics {
#_count = 0;
#_minimum = undefined;
#_maximum = undefined;
#_range = undefined;
#_average = undefined;
#_summary = 0;
#_mode = undefined;
#_median = undefined;
#_variance = undefined;
#_standardDeviation = undefined;
/**
* Initializes a new instance of Statistics.
* @param {number} count - The number of items in the sequence.
* @param {(number | undefined)} minimum - The minimal value in the sequence, or undefined if not applicable.
* @param {(number | undefined)} maximum - The maximal value in the sequence, or undefined if not applicable.
* @param {(number | undefined)} range - The range of the sequence (maximum - minimum), or undefined if not applicable.
* @param {(number | undefined)} average - The average value of the sequence, or undefined if not applicable.
* @param {number} summary - The sum of all values in the sequence.
* @param {(Array<number> | undefined)} mode - The most frequent values, or undefined if not calculated.
* @param {(number | undefined)} median - The middle value when sorted, or undefined if not calculated.
* @param {(number | undefined)} variance - The variance of the sequence, or undefined if not calculated.
* @param {(number | undefined)} standardDeviation - The standard deviation (square root of variance), or undefined if not calculated.
*/
constructor(count, minimum, maximum, range, average, summary, mode, median, variance, standardDeviation) {
this.#_count = count ?? 0;
this.#_minimum = minimum ?? undefined;
this.#_maximum = maximum ?? undefined;
this.#_range = range ?? undefined;
this.#_average = average ?? undefined;
this.#_summary = summary ?? 0;
this.#_mode = mode == null ? undefined : Array.isArray(mode) ? mode : [mode];
this.#_median = median ?? undefined;
this.#_variance = variance ?? undefined;
this.#_standardDeviation = standardDeviation ?? undefined;
}
/**
* The number of items in the sequence.
*
* @returns {number} - The number of items in the sequence.
*/
get count() { return this.#_count; }
/**
* The minimal value in the sequence, or undefined if not applicable.
*
* @returns {(number | undefined)} - The minimal value in the sequence, or undefined if not applicable.
*/
get minimum() { return this.#_minimum; }
/**
* The maximal value in the sequence, or undefined if not applicable.
*
* @returns {(number | undefined)} - The maximal value in the sequence, or undefined if not applicable.
*/
get maximum() { return this.#_maximum; }
/**
* The range of the sequence (maximum - minimum), or undefined if not applicable.
*
* @returns {(number | undefined)} - The range of the sequence (maximum - minimum), or undefined if not applicable.
*/
get range() { return this.#_range; }
/**
* The average value of the sequence, or undefined if not applicable.
*
* @returns {(number | undefined)} - The average value of the sequence, or undefined if not applicable.
*/
get average() { return this.#_average; }
/**
* The sum of all values in the sequence.
*
* @returns {number} - The sum of all values in the sequence.
*/
get summary() { return this.#_summary; }
/**
* The most frequent values, or undefined if not calculated.
*
* @returns {(Array<number> | undefined)} - The most frequent values, or undefined if not calculated.
*/
get mode() { return this.#_mode; }
/**
* The middle value when sorted, or undefined if not calculated.
*
* @returns {(number | undefined)} - The middle value when sorted, or undefined if not calculated.
*/
get median() { return this.#_median; }
/**
* The variance of the sequence, or undefined if not calculated.
*
* @returns {(number | undefined)} - The variance of the sequence, or undefined if not calculated.
*/
get variance() { return this.#_variance; }
/**
* The standard deviation (square root of variance), or undefined if not calculated.
*
* @returns {(number | undefined)} - The standard deviation (square root of variance), or undefined if not calculated.
*/
get standardDeviation() { return this.#_standardDeviation; }
/**
* Creates a Statistics instance representing an empty sequence.
* @returns {Statistics} An instance with zero or undefined values and no extended calculations.
*/
static empty() {
return new Statistics(
0, // count
undefined, // minimum
undefined, // maximum
undefined, // range
undefined, // average
0, // summary
undefined, // mode
undefined, // median
undefined, // variance
undefined // standardDeviation
);
}
/**
* Creates a deep copy of the current `Statistics` instance.
*
* @returns {Statistics} A cloned `Statistics` instance with the same values.
*/
clone() {
return new Statistics(
this.#_count,
this.#_minimum,
this.#_maximum,
this.#_range,
this.#_average,
this.#_summary,
this.#_mode == null ? undefined : [...this.#_mode],
this.#_median,
this.#_variance,
this.#_standardDeviation
);
}
/**
* Returns a plain JSON object representing the current `Statistics` instance.
*
* @returns {Object} A JSON-serializable object containing all statistical properties.
*/
toJSON() {
return {
count: this.#_count,
minimum: this.#_minimum,
maximum: this.#_maximum,
range: this.#_range,
average: this.#_average,
summary: this.#_summary,
mode: this.#_mode,
median: this.#_median,
variance: this.#_variance,
standardDeviation: this.#_standardDeviation
};
}
/**
* Creates a `Statistics` instance from a plain JSON object.
*
* @param {Object} json - A plain object with the same properties as produced by `toJSON()`.
* @returns {Statistics} A new `Statistics` instance populated from the given JSON object.
* @throws {Error} If the input is not an object or missing required fields.
*/
static fromJSON(json) {
if (typeof json !== "object" || json === null) {
throw new Error("Invalid JSON object for Statistics.");
}
return new Statistics(
json.count ?? 0,
json.minimum ?? undefined,
json.maximum ?? undefined,
json.range ?? undefined,
json.average ?? undefined,
json.summary ?? 0,
json.mode == null ? undefined : Array.isArray(json.mode) ? [...json.mode] : [json.mode],
json.median ?? undefined,
json.variance ?? undefined,
json.standardDeviation ?? undefined
);
}
static #_areNumbersEqualUpToDigits(a, b, n) {
if(a == null && b == null)
return true;
if(a == null || b == null)
return false;
// Handle full comparison if n is null/undefined
if (n == null) {
return a === b;
}
if (typeof n !== 'number' || n < 0 || !Number.isFinite(n)) {
throw new Error('Invalid n: must be a non-negative finite number');
}
if (!Number.isFinite(a) || !Number.isFinite(b)) {
return a === b; // still allow Infinity comparisons
}
const factor = 10 ** n;
return Math.round((a + Number.EPSILON) * factor) === Math.round((b + Number.EPSILON) * factor);
}
static #_areArraysEqual(a, b) {
// Treat null and undefined as equivalent
if (a == null && b == null) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
/**
* Returns `true` if the current object equal to the specified value.
*
* @param {Statistics} value - `true` if the current object equal to the specified.
* @param {number} precision - The number of digits after the decimal point to compare.
*/
equal(value, precision) {
const res = value != null
&& this.count === value.count
&& this.minimum === value.minimum
&& this.maximum === value.maximum
&& Statistics.#_areNumbersEqualUpToDigits(this.summary, value.summary, precision)
&& Statistics.#_areNumbersEqualUpToDigits(this.average, value.average, precision)
&& Statistics.#_areNumbersEqualUpToDigits(this.range, value.range, precision)
&& Statistics.#_areNumbersEqualUpToDigits(this.median, value.median, precision)
&& Statistics.#_areNumbersEqualUpToDigits(this.variance, value.variance, precision)
&& Statistics.#_areNumbersEqualUpToDigits(this.standardDeviation, value.standardDeviation, precision);
return res && Statistics.#_areArraysEqual(this.mode, value.mode);
}
}
/*!
* File: src/linq.js
*/
let LinqOrder$1;
let LinqGroup$1;
/**
* Linq - A static entry point for LINQ-style operations on arrays.
*
* Provides methods to initiate query chains, enabling advanced data transformations
* such as filtering, mapping, grouping, ordering, and aggregating using a fluent syntax.
*
* All operations are performed on native JavaScript arrays, with no external dependencies.
*
* Example usage:
*
* ```js
* const query = Linq.from([1, 2, 3, 4])
* .where(x => x > 2)
* .select(x => x * 10)
* .toArray(); // [30, 40]
* ```
*
* or
*
* ```js
* const query = new Linq([1, 2, 3, 4])
* .where(x => x > 2)
* .select(x => x * 10)
* .toArray(); // [30, 40]
* ```
*
* @class
*/
class Linq {
/**
* Setups `Link` class with neessary types.
*
* @param {LinqOrder} linqOrderClass
* @param {LinqGroup} linqGroupClass
* @ignore
*/
static __setup(linqOrderClass, linqGroupClass) {
LinqOrder$1 = linqOrderClass;
LinqGroup$1 = linqGroupClass;
}
/* basics and defaults */
static #_defaultComparer = (obj1, obj2) => obj1 > obj2 ? 1 : (obj1 < obj2 ? - 1 : 0);
static #_defaultEqualityComparer = (obj1, obj2) => obj1 === obj2;
static #_defaultSelector = (element) => element;
static #_defaultResultSelector(...args) {
return args.length > 1 ? args : args[0];
}
static *#_createSimpleGenerator(iterable) { for (let item of iterable) { yield item; } }
/**
* Returns the version of this LINQ library.
* This is a read-only property used for diagnostics or compatibility checks.
* @returns {string} Semantic version string.
*/
static get Version() {
return BuildInfo.version;
}
/*
* Instance implementation starts here.
*/
#_iterable = null;
#_thisArg = null;
/**
* Initialize a new instance of Linq object and wrap the specified iterable within it.
* @param {Iterable<any>} iterable - An iterable object (e.g. Array, string, Map, Set...)
* @param {any} thisArg - A "this" pointer to be applied when calling user's callbacks.
* @throws {Error} `iterable` argument must be iterable
*/
constructor(iterable, thisArg) {
if (!TypeCheck.isIterable(iterable)) {
throw new Error(Errors.Messages.MUST_BE_ITERABLE_iterable);
}
this.#_iterable = iterable;
this.#_thisArg = thisArg;
}
/**
* Makes this object iterable.
*/
*[Symbol.iterator]() {
for (let item of this.#_iterable) {
yield item;
}
}
/**
* Initializes a LINQ query from the specified iterable.
*
* @param {Iterable} iterable - An iterable object.
* @param {any} [thisArg] - An object to be used as `this` pointer in callback calls.
* @returns {Linq} - A LINQ query from the specified iterable.
*/
static from(iterable, thisArg) { return new this(iterable, thisArg); }
/**
* Applies an accumulator function over the sequence. The specified seed value is used as the initial accumulator value, and the specified function is used to select the result value.
* @param {any} seed - The initial accumulator value. The first parameter is the current sequence item. The second parameter is the value accumulated so far. The third parameter is the index of the current item (the first parameter) in the sequence.
* @param {Function} accumulator - An accumulator function to be invoked on each element.
* @param {Function} [resultSelector] - An optional function to transform the final accumulator value into the result value.
* @returns {any} The transformed final accumulator value.
* @throws `accumulator` must be a function
* @throws `resultSelector` must be a function or nullish
*/
aggregate(seed, accumulator, resultSelector) { return Linq.#_aggregate(this.#_thisArg, seed, this, accumulator, resultSelector); }
static #_aggregate(thisArg, seed, iterable, accumulator, resultSelector) {
if (!TypeCheck.isFunction(accumulator)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_accumulator); }
if (resultSelector != null && !TypeCheck.isFunction(resultSelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_resultSelector); }
thisArg = thisArg ?? this;
resultSelector = resultSelector ?? this.#_defaultResultSelector;
let accumulatedSoFar = seed, index = 0;
for (let item of iterable) {
accumulatedSoFar = accumulator.call(thisArg, item, accumulatedSoFar, index);
index++;
}
return resultSelector.call(thisArg, accumulatedSoFar);
}
/**
* Determines whether all elements of the sequence satisfy a condition.
*
* @param {Function} predicate - A function to test each source element for a condition; The first parameter is a source element, the second parameter represents the index of the source element in the sequence.
* @returns {boolean} true if every element of the sequence passes the test in the specified predicate, or if the sequence is empty; otherwise, false.
* @throws `predicate` must be a function.
*/
all(predicate) { return Linq.#_all(this.#_thisArg, this, predicate); }
static #_all(thisArg, iterable, predicate) {
if (!TypeCheck.isFunction(predicate)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_predicate); }
thisArg = thisArg ?? this;
let index = 0;
for (let item of iterable) {
if (predicate.call(thisArg, item, index) !== true) { return false; }
index++;
}
return true;
}
/**
* Determines whether any element of the sequence exists or satisfies a condition.
*
* @param {Function} [predicate] - A function to test each source element for a condition; The first parameter is a source element, the second parameter represents the index of the source element in the sequence.
* @returns {boolean} true if the sequence contains any elements; otherwise, false. BUT In case that a predicate was specified: true if the sequence is not empty and at least one of its elements passes the test in the specified predicate; otherwise, false.
* @throws `predicate` must be a function or nullish.
*/
any(predicate) { return Linq.#_any(this.#_thisArg, this, predicate); }
static #_any(thisArg, iterable, predicate) {
if (predicate != null && !TypeCheck.isFunction(predicate)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_predicate); }
thisArg = thisArg ?? this;
let index = 0;
for (let item of iterable) {
if (predicate == null || predicate.call(thisArg, item, index) === true) { return true; }
index++;
}
return false;
}
/**
* Appends a value to the end of the sequence. The original sequence (the current) is not changed.
*
* @param {any} value - The value to be appended to the returned sequence.
* @returns {Linq} A sequence consist of the current sequence plus the specified value.
*/
append(value) { return Linq.#_append(this.#_thisArg, this, value); }
static #_append(thisArg, iterable, value) { return new this(this.#_appendGen(thisArg, iterable, value)); }
static *#_appendGen(thisArg, iterable, value) {
for (let item of iterable) { yield item; }
yield value;
}
/**
* Computes the average of the sequence of numeric values.
* If the sequence is empty or at least one element of the sequence is not a number, the return value is undefined.
*
* @returns {number|undefined} The numeric average of the sequence,or undefined if the sequence contains at least oneitem that is not a number.
*/
average() { return Linq.#_average(this.#_thisArg, this); }
static #_average(thisArg, iterable) {
let sum = 0, count = 0;
for (let item of iterable) {
if (TypeCheck.isNumber(item)) {
sum += item;
count++;
}
else {
sum = undefined;
break;
}
}
return (sum === undefined || count === 0) ? undefined : sum / count;
}
/**
* Concatenates the current sequence with the specified sequences. Note that the current sequence is not modified.
*
* @param {...Iterable} withIterables - Iterable sequences be appended to the endof this sequence to form a single sequence.
* @returns {Linq} An iterable that contains the concatenated elements of the current sequence with all input sequences.
* @throw At least one of the argument is not iterable.
*/
concat(...withIterables) { return Linq.#_concat(this.#_thisArg, this, ...withIterables); }
static #_concat(thisArg, iterable, ...withIterables) {
/* the validation took place here and NOT within #_concat, because that is a deferred execution generator function*/
for (let iter of withIterables) {
if (!TypeCheck.isIterable(iter)) { throw new Error(Errors.Messages.ALL_MUST_BE_ITERABLE); }
}
return new this(this.#_concatGen(thisArg, ...[iterable, ...withIterables]), thisArg);
}
static *#_concatGen(thisArg, ...itrables) {
for (let iter of itrables) {
for (let item of iter) { yield item; }
}
}
/**
* Determines whether the sequence contains the specified element using strict equality comparison (===) or the specified equality comparer.
*
* @param {any} value - The value to locate in the sequence.
* @param {Function} [equalityComparer] - An optional equality comparer to compare values. The first parameter to the comparer is the specified value. The second parameter is an item from the sequence. The third parameter is the index of the item in the sequence.
* @returns {boolean} true if the sequence contains the specified value using strict equality comparison (===) or the specified equality comparer; otherwise, false.
* @throws `equalityComparer` must be a function or nullish.
*/
contains(value, equalityComparer = null) { return Linq.#_contains(this.#_thisArg, this, value, equalityComparer); }
static #_contains(thisArg, iterable, value, equalityComparer) {
if (equalityComparer != null && !TypeCheck.isFunction(equalityComparer)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_equalityComparer); }
equalityComparer = equalityComparer ?? this.#_defaultEqualityComparer;
let index = 0;
for (let item of iterable) {
if (thisArg == null ? equalityComparer(value, item, index) === true : equalityComparer.call(thisArg, value, item, index)) {
return true;
}
index++;
}
return false;
}
/**
* Returns the number of elements in a sequence.
*
* @returns {number} The number of elements in a sequence.
*/
count() { return Linq.#_count(this.#_thisArg, this); }
static #_count(thisArg, iterable) {
let c = 0;
// eslint-disable-next-line no-unused-vars
for (let _ of iterable) {
c++;
}
return c;
}
/**
* Returns the elements of the sequence, or a default valued singleton collection if the sequence is empty.
*
* @param {any} defaultSingletonValue - A default value to be used as a singleton in the returned sequence if the current sequence is empty.
* @returns {Iterable} The elements of the sequence, or a default valued singleton collection if the sequence is empty.
*/
defaultIfEmpty(defaultSingletonValue) { return Linq.#_defaultIfEmpty(this.#_thisArg, this, defaultSingletonValue); }
static #_defaultIfEmpty(thisArg, iterable, defaultSingletonValue) {
return new this(this.#_defaultIfEmptyGen(thisArg, iterable, defaultSingletonValue), thisArg);
}
static *#_defaultIfEmptyGen(thisArg, iterable, defaultSingletonValue) {
let yielded = false;
for (let item of iterable) {
yielded = true;
yield item;
}
if (!yielded) { yield defaultSingletonValue; }
}
/**
* Returns distinct elements from the sequence.
*
* @param {Function} [equalityComparer] - An equality comparer to compare values.
* @returns {Linq} Distinct elements from a sequence.
* @throws `equalityComparer` must be a function or nullish
*/
distinct(equalityComparer) { return Linq.#_distinct(this.#_thisArg, this, equalityComparer); }
static #_distinct(thisArg, iterable, equalityComparer) {
/* the validation took place here and NOT within #_distinctGen, because that is a deferred execution generator function*/
if (equalityComparer != null && !TypeCheck.isFunction(equalityComparer)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_equalityComparer); }
return new this(this.#_distinctGen(thisArg, iterable, equalityComparer), thisArg);
}
static *#_distinctGen(thisArg, iterable, equalityComparer) {
equalityComparer = equalityComparer ?? this.#_defaultEqualityComparer;
let arr = [];
for (let item of iterable) {
let found = false;
for (var i = 0; !found && i < arr.length; i++) {
found = (thisArg == null ? equalityComparer(item, arr[i]) === true : equalityComparer.call(thisArg, item, arr[i]) === true);
}
if (!found) { arr.push(item); }
}
for (let item of arr) { yield item; }
}
/**
* Duplicate the sequence number of times as specified `factor` parameter.
* If the factor is non integral number, the integer value less than or equal to the factor is used.
* If factor is less than 1, an empty sequence returns.
* This function does not modify the current sequence.
*
* @param {number} factor - The number of times to duplicate the
* @param {boolean} [inplace] - If set to true, duplicate each element and places the duplication right after this element, instead of duplicating the whole sequence and return them one after the other. The default is false.
* @returns {Linq} A sequence consist of duplication of the current sequence.
* @throws `factor` must be a non negative integer
*/
duplicate(factor, inplace) { return Linq.#_duplicate(this.#_thisArg, this, factor, inplace); }
static #_duplicate(thisArg, iterable, factor, inplace) {
if (!TypeCheck.isNumber(factor) || Math.floor(factor) !== factor || factor < 0) { throw new Error(Errors.Messages.MUST_BE_NON_NEGATIVE_INTEGER_factor); }
return new this(this.#_duplicateGen(thisArg, iterable, factor, inplace === true), thisArg);
}
static *#_duplicateGen(thisArg, iterable, factor, inplace) {
if (factor >= 1.0) {
if (inplace === true) {
for (let item of iterable) {
for (let i = 0; i < factor; i++) { yield item; }
}
}
else {
for (let i = 0; i < factor; i++) {
for (let item of iterable) { yield item; }
}
}
}
}
/**
* Returns the element at a specified index in the sequence.
*
* @param {number} index - The zero-based index of the element to retrieve.
* @returns {any} The element at a specified index in a sequence.
* @throws `index` must be a non negative integer
* @throws The sequence is empty
* @throws `index` is out of range
*/
elementAt(index) { return Linq.#_elementAt(this.#_thisArg, this, index, false, null); }
/**
* Returns the element at a specified index in the sequence, or the specified default value if the specified index is negative or out of range.
* @param {number} index - The zero-based index of the element to retrieve.
* @param {any} defaultValue - The default value to be returned.
* @returns {any} The element at a specified index in the sequence, or the default value if the specified index is negative or out of range.
* @throws `index` must be a non negative integer
*/
elementAtOrDefault(index, defaultValue) { return Linq.#_elementAt(this.#_thisArg, this, index, true, defaultValue); }
static #_elementAt(thisArg, iterable, index, hasDefaultValue, defaultValue) {
if (!TypeCheck.isNumber(index) || Math.floor(index) != index || (index < 0 && hasDefaultValue !== true)) {
throw new Error(Errors.Messages.MUST_BE_NON_NEGATIVE_INTEGER_index);
}
let current = 0;
for (let item of iterable) {
if (current++ === index) { return item; }
}
if (hasDefaultValue === true) { return defaultValue; }
throw new Error(current === 0 ? Errors.Messages.SEQUENCE_IS_EMPTY : Errors.Messages.OUT_OF_RANGE_index);
}
/**
* Returns an empty sequence.
* @param {any} thisArg - Optional object to be used as this argument in any call in the returned sequence.
* @returns {Linq} An empty sequence.
*/
static empty(thisArg) { return new this([], thisArg); }
/**
* Produces a set of unique items from the current sequence that do not appear in the specified scond sequence.
* @param {Iterable} secondIterable - The second sequence.
* @param {Function} [equalityComparer] - An equality comparer to be used to compare two items.
* @returns {Linq} A set of unique items from the current sequence that do not appear in the specified scond sequence.
* @throws `secondIterable` must be iterable
* @throws `equalityComparer` must be a function or nullish
*/
except(secondIterable, equalityComparer) { return Linq.#_van(this.#_thisArg, this, secondIterable, equalityComparer, "except"); }
static #_van(thisArg, sourceIterable, secondIterable, equalityComparer, algorithm) {
/* #_van and #_vanGen are inner implementation of three functions:
* except
* intersect
* union
*
* The implementation tak under considation the possibility that the specified equalityComparer is not commotative!
*/
if (!TypeCheck.isIterable(secondIterable)) { throw new Error(Errors.Messages.MUST_BE_ITERABLE_secondIterable); }
if (equalityComparer != null && !TypeCheck.isFunction(equalityComparer)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_equalityComparer); }
return new this(this.#_vanGen(thisArg, sourceIterable, secondIterable, equalityComparer, algorithm), thisArg);
}
static *#_vanGen(thisArg, sourceIterable, secondIterable, equalityComparer, algorithm) {
let mask = algorithm === "except" ? false : (algorithm === "intersect" ? true : null /* union */);
thisArg = thisArg ?? this;
equalityComparer = equalityComparer ?? this.#_defaultEqualityComparer;
let secondUnique = Array.from(this.#_distinctGen(thisArg, secondIterable
/*, remove items that STRICTLY equal!!!
* Here i'm not using the specified equalityComparer to eliminate duplicate
* values from the secondIterable, because that comparer is not assured
* to be commutative (that is, while a === b it is possible that b !== a).
*/
));
let skip, passed = [];
for (let a of sourceIterable) {
skip = null;
for (let p of passed) {
/* It is possible that the equality comparer is not commotative, so:
* 1. values that passed we place in passed array and not in the filter array.
* 2. we strict compare to what was already passed. */
if (a === p) {
skip = true;
break;
}
}
if (mask != null /* not union */) {
if (skip == null /* not already passed, so there´s no need to check against secondUnique */) {
skip = mask;
for (let b of secondUnique) {
if (equalityComparer.call(thisArg, a, b) === true) {
skip = !mask; /* */
break;
}
}
}
}
else if (skip === null) { skip = false; }
if (skip === false /*strict comparison!*/) {
passed.push(a);
yield a;
}
}
if (mask === null /*union*/) {
/* secondUnique contains unique items from the second iterable! */
for (let b of secondUnique) {
skip = false;
for (let p of passed) {
/* The 'passed' array contains the unique items that passed from the first sequence.
* Because the equality comparer might be a not commotative comparer, in the call
* we pass the item from the first sequence as the first parameter! */
if (equalityComparer.call(thisArg, p, b) === true) {
skip = true;
break;
}
}
if (skip === false) {
/* no need to add it to passed! */
yield b;
}
}
}
}
/**
* Returns the first element in the sequence, or the first element in the sequence that meets the specified condition, if such one was specified.
* @param {Function} [predicate] - An optional function to test each element of the sequence for a condition; The first parameter is a source element, the second parameter is the index of the source element within the sequence.
* @returns {any} The first element in the sequence, or the first element in the sequence that satisfies a specified condition, if such one was specified.
* @throws `predicate` must be a function, null or undefined.
* @throws The sequence is empty.
* @throws No element satisfies the condition in predicate.
*/
first(predicate) { return Linq.#_first(this.#_thisArg, this, predicate, false); }
/**
* Returns the first element of the sequence, or the first one that meets the specified condition, if such one was specified. A default value is returned if the sequence is empty or no element was found.
* @param {any} defaultValue - The default value to return in case that sequence is empty, or none of the sequence items meets the specified condition.
* @param {Function} [predicate] - A function to test each source element for a condition; The first parameter is a source element, the second parameter of the function represents the index of the source element.
* @returns {any} The first element of the sequence, or the first one that meets the specified condition, if such one was specified. Or the specified default value if the sequence is empty or no element was found.
* @throws `predicate` must be a function, null or undefined.
*/
firstOrDefault(defaultValue, predicate) { return Linq.#_first(this.#_thisArg, this, predicate, true, defaultValue); }
static #_first(thisArg, iterable, predicate, hasDefaultValue, defaultValue) {
if (predicate != null && !TypeCheck.isFunction(predicate)) {
throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_predicate);
}
thisArg = thisArg ?? this;
let index = 0;
for (let item of iterable) {
if (predicate == null || predicate.call(thisArg, item, index) === true) { return item; }
index++;
}
if (hasDefaultValue) { return defaultValue; }
throw new Error(index > 0 && predicate != null ? Errors.Messages.NO_ELEMENT_SATISFIES_THE_CONDITION_IN_predicate: Errors.Messages.SEQUENCE_IS_EMPTY);
}
/**
* Iterates through all elements in the sequence, and call the specified function for each.
* @param {Function} callback - A callback function to be called for each element of the sequence until all elements of the sequence processed,
* or until a boolean false value was returned by the function;
* The first parameter to the callback is an element of the sequence, the second parameter represents the index of the element within the sequence.
* The callback might return false to stop the iteration.
* @throws `callback` must be a function
*/
forEach(callback) { return Linq.#_forEach(this.#_thisArg, this, callback); }
static #_forEach(thisArg, iterable, callback) {
if (!TypeCheck.isFunction(callback)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_callback); }
thisArg = thisArg ?? this;
let index = 0;
for (let item of iterable) {
let cont = callback.call(thisArg, item, index);
index++;
if (cont === false /* explicity stop the loop */) { break; }
}
}
/**
*
* @param {any} keySelector key creator
* @param {any} [elementSelector] source element to group element: groupElementType func (sourceElementType)
* @param {any} [resultSelector] resultType func(key, groupElementType)
* @param {any} [keyEqualityComparer] bool (a, b)
*/
groupBy(keySelector, elementSelector, resultSelector, keyEqualityComparer) {
return Linq.#_groupBy(this.#_thisArg, this, keySelector, elementSelector, resultSelector, keyEqualityComparer);
}
static #_groupBy(thisArg, iterable, keySelector, elementSelector, resultSelector, keyEqualityComparer) {
if (keySelector != null && !TypeCheck.isFunction(keySelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_keySelector); }
if (elementSelector != null && !TypeCheck.isFunction(elementSelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_elementSelector); }
if (resultSelector != null && !TypeCheck.isFunction(resultSelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_resultSelector); }
if (keyEqualityComparer != null && !TypeCheck.isFunction(keyEqualityComparer)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_keyEqualityComparer); }
return new this(this.#_groupByGen(thisArg, iterable, keySelector, elementSelector, resultSelector, keyEqualityComparer), thisArg);
}
static *#_groupByGen(thisArg, iterable, keySelector, elementSelector, resultSelector, keyEqualityComparer) {
keySelector = keySelector ?? this.#_defaultSelector;
elementSelector = elementSelector ?? this.#_defaultSelector;
resultSelector = resultSelector ?? this.#_defaultResultSelector;
keyEqualityComparer = keyEqualityComparer ?? this.#_defaultEqualityComparer;
thisArg = thisArg ?? this;
let segments = []; /* Array and not map because the keys might be objects, and Map does not support getHashCode, there is no efficiency benefit in using Map. */
let index = 0;
for (let item of iterable) {
let key = keySelector.call(thisArg, item, index);
let element = elementSelector.call(thisArg, item, index);
let appended = false;
for (let seg of segments) {
if (keyEqualityComparer.call(thisArg, key, seg.key, index) === true) {
seg.elements.push(element);
appended = true;
break;
}
}
if (!appended) {
segments.push({ key: key, elements: [element] });
}
index++;
}
let groups = [];
for (let seg of segments) {
groups.push(resultSelector.call(thisArg, new LinqGroup$1(seg.key, seg.elements, thisArg)));
}
yield* groups;
}
/**
* Correlates the elements of two sequences based on matching keys: this sequence (which considered to be the left one) and the specified sequence (the right).
* The result sequence contains each and every element from the left sequence along with its matched-key elements, group together, from the right.
* The order of the result sequence keeps the order of the left sequence (this sequence), and for each match from the right sequence the order is also maintained.
* It is possible to control the returned object by specifying a result selector which accepts the element from the left (this sequence) and its matched element from the right grouped as a single Linq object.
* @param {Iterable} rightIterable - The right side sequence to group-join to the left sequence (this one).
* @param {Function} [leftKeySelector] - An optional function to extract the join key from each element of the left sequence (this one). By default the element itself is used as both key and value.
* @param {Function} [rightKeySelector] - An optional function to extract the join key from each element of the right sequence. By default the element itself is used as both key and value.
* @param {Function} [resultSelector] - An optional function to create a result element from matching elements, a single element from the left (this sequence) and a group of elements from the right (the specified sequence). By default, the result element is an array of these two.
* @param {Function} [keyEqualityComparer] - An optional function to check the equality of two keys. By default a strict comparison is made (===).
* @returns {Linq} An iterable object that has elements that are obtained by performing an group join on the two sequences.
* @throws `rightIterable` must be iterable.
* @throws `leftKeySelector` must be a function or nullish.
* @throws `rightKeySelector` must be a function or nullish
* @throws `resultSelector` must be a function or nullish
* @throws `keyEqualityComparer` must be a function or nullish
*/
groupJoin(rightIterable, leftKeySelector, rightKeySelector, resultSelector, keyEqualityComparer) {
return Linq.#_groupJoin(this.#_thisArg, this, rightIterable, leftKeySelector, rightKeySelector, resultSelector, keyEqualityComparer);
}
static #_groupJoin(thisArg, iterable, rightIterable, leftKeySelector, rightKeySelector, resultSelector, keyEqualityComparer) {
if (!TypeCheck.isIterable(rightIterable)) { throw new Error(Errors.Messages.MUST_BE_ITERABLE_rightIterable); }
if (leftKeySelector != null && !TypeCheck.isFunction(leftKeySelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_leftKeySelector); }
if (rightKeySelector != null && !TypeCheck.isFunction(rightKeySelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_rightKeySelector); }
if (resultSelector != null && !TypeCheck.isFunction(resultSelector)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_resultSelector); }
if (keyEqualityComparer != null && !TypeCheck.isFunction(keyEqualityComparer)) { throw new Error(Errors.Messages.MUST_BE_FUNCTION_OR_NULLISH_keyEqualityComparer); }
return new this(this.#_groupJoinGen(thisArg, iterable, rightIterable, leftKeySelector, rightKeySelector, resultSelector, keyEqualityComparer), thisArg);
}
static *#_groupJoinGen(thisArg, leftIterable, rightIterable, leftKeySelector, rightKeySelector, resultSelector, keyEqualityComparer) {
leftKeySelector = leftKeySelector ?? this.#_defaultSelector;
rightKeySelector = rightKeySelector ?? this.#_defaultSelector;
resultSelector = resultSelector ?? this.#_defaultResultSelector;
keyEqualityComparer = keyEqualityComparer ?? this.#_defaultEqualityComparer;
thisArg = thisArg ?? this;
for (let a of leftIterable) {
let leftKey = leftKeySelector.call(thisArg, a);
yield resultSelector.call(thisArg, a, new Linq(rightIterable).where(item => keyEqualityComparer.call(thisArg, leftKey, rightKeySelector.call(thisArg, item)) === true));
}
}
/**
* Produces a sequence of UNIQUE items that appear in this sequence as well as the specified sequence. The order of the items remains the same as it was in this sequence.
* @param {Iterable} secondIterable - The second sequence.
* @param {Function} [equalityComparer] - An equality comparer to be used to compare two items.
* @returns {Linq} A sequence of UNIQUE items that ALSO appear in the specified sequence. The order of the items remains the same as it was in this sequence.
* @throws `secondIterable` must be iterable
* @throws `equalityComparer` must be a function or nullish
*/
intersect(secondIterable, equalityComparer) { return Linq.#_van(this.#_thisArg, this, secondIterable, equalityComparer, "intersect"); }
/**
* Correlates the elements of two sequences based on matching keys: this sequence (which considered to be the left one) and the specified sequence (the right).
* The result sequence contains each element from the left sequence along with its matched-key element from the right - and only these.
* The order of the result sequence keeps the order of the left sequence (this sequence), and for each match from the right sequence the order is also maintained.
* @param {Iterable} rightIterable - The right side sequence to join to the left sequence (this one).
* @param {Function} [leftKeySelector] - An optional function to e