An api for lazy querying of iterables, implemented in TypeScript and inspired by .NET's LINQ methods.
To implement a lazy API similar by using iterators in order to simplify data-oriented workflows greatly and to provide an API for C# developers familiar with the LINQ extension methods.
Some of the methods (union, except, etc.) accept an optional equality comparer object in the form of { hash, equals }
.
hash
- a function that returns hashcode for a key.
equals
- a function that compares two keys for equality.
This is useful for cases where the iterables contain objects and not just primitives. These operations are backed by a custom map implementation which allows for any object to be used as a key.
For ease of use default comparers for number
, string
, boolean
, object
and Iterable
are provided OOTB.
It is advisable that a custom comparer is implemented tailored for the specific use case when a complex object is used a key.
- where
- select
- selectMany
- distinct
- distinctBy
- zip
- groupBy
- join
- orderBy
- orderByDescending
- reverse
- skip
- skipWhile
- take
- takeWhile
- except
- intersect
- concat
- union
- xOr
- aggregate
- windowed
- batch
- any
- all
- min
- minBy
- max
- maxBy
- average
- averageBy
- sequenceEquals
- indexOf
- lastIndexOf
- findIndex
- findLastIndex
- elementAt
- first
- firstOrDefault
- last
- lastOrDefault
- forEach
- toArray
- count
- seq
- id
- toMap
- toMapMany
- toSet
- append
- prepend
- tap
- repeat
The function print
can be used to display а tree-like representation of the operators in the console.
import { linq, print, seq } from './src/linq';
var elements = seq(1, 1, 10)
.union(seq(1, 1, 15))
.except([1,2])
.union(linq([1,2,3]).intersect([2,3]))
.skip(5)
.skipWhile(x => x < 3)
.groupBy(x => x % 2)
.zip(seq(1,5))
print(elements);
Linqable
└──Zip (selector: (a, b) => [a, b])
├──Group (selector: x => x % 2)
| └──SkipWhile (predicate: x => x < 3)
| └──Skip (count: 5)
| └──Union
| ├──Except
| | ├──Union
| | | ├──Sequence (start: 1, step: 1, end: 10)
| | | └──Sequence (start: 1, step: 1, end: 15)
| | └──1,2
| └──Intersect
| ├──1,2,3
| └──2,3
└──Sequence (start: 1, step: 5, end: Infinity)
The API any objects which are iterable in JavaScript. In order to use the method it is required to call linq
with the object that we want to iterate as a parameter. The result of linq
is Linqable
object which supports the api. The linq module exports linq
, linqAsync
, seq
, repeat
and id
.
linqAsync
is a wrapper which exposes a set of asynchronous API is, allowing for various callbacks to return promises.
The functions which cause immediate execution in the synchronous API return Promise in the linqAsync
API.
Other than that, the method definitions are analogous to the linq
API.
import { linq } from "./linq";
interface IPerson {
name: string;
age: number;
}
let people: IPerson[] = [
{ name: "Ivan", age: 24 },
{ name: "Deyan", age: 25 }
];
linq(people)
.where(p => p.age > 22)
.select(p => p.name)
.forEach(name => console.log(name))
Ivan
Deyan
Where filters the iterable based on a predicate function.
A sequence of the elements for which the predicate returns true
will be returned.
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let evenNumbers = linq(numbers).where(i => i % 2 == 0);
for (let number of evenNumbers) {
console.log(number)
}
2
4
6
8
10
Each element of an iterable is trasnformed into another value - the return value of the function passed to select
.
let numbers = [1, 2, 3, 4, 5];
let numbersTimes10 = linq(numbers).select(i => i * 10);
for (let number of numbersTimes10) {
console.log(number)
}
10
20
30
40
50
Flattens iterable elements into a single iterable sequence.
selectMany
expects a function which takes an element from the sequence returns an iterable. All of the results are flattent into a single sequence.
let numbers = [{
inner: [1, 2, 3]
}, {
inner: [4, 5, 6]
}];
let flattened = linq(numbers).selectMany(x => x.inner);
for (let number of flattened) {
console.log(number)
}
1
2
3
4
5
6
Gets the distinct elements of a sequence based on an equality comparer function. The function comapres the objects in the sequence and should return 'true' when they are considered equal.
let numbers = [{ value: 1 }, { value: 1 }, { value: 2 }, { value: 2 }, { value: 3 }, { value: 3 }];
let distinct = linq(numbers).distinct((first, second) => first.value === second.value);
for (let number of distinct) {
console.log(number)
}
{ value: 1 }
{ value: 2 }
{ value: 3 }
Gets the distinct elements of a sequence based on a selector function. If a selector function is not passed, it will get the distinct elements by reference.
let numbers = [{ value: 1 }, { value: 1 }, { value: 2 }, { value: 2 }, { value: 3 }, { value: 3 }];
let distinct = linq(numbers).distinctBy(el => el.value);
for (let number of distinct) {
console.log(number)
}
{ value: 1 }
{ value: 2 }
{ value: 3 }
Applies a transformation function to each corresponding pair of elements from the iterables. The paring ends when the shorter sequence ends, the remaining elements of the other sequence are ignored.
let odds = [1, 3, 5, 7];
let evens = [2, 4, 6, 8];
let oddEvenPairs = linq(odds)
.zip(evens, (odd, even) => ({ odd, even }));
for (let element of oddEvenPairs) {
console.log(element);
}
{ odd: 1, even: 2 }
{ odd: 3, even: 4 }
{ odd: 5, even: 6 }
{ odd: 7, even: 8 }
Groups elements based on a selector function. The function returns a sequence of arrays with the group key as the first element and an array of the group elements as the second element.
let groups = linq([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).groupBy(i => i % 2);
for (let group of groups) {
console.log(group);
}
[ 1, [ 1, 3, 5, 7, 9 ] ]
[ 0, [ 2, 4, 6, 8, 10 ] ]
Performs a join on objects matching property values according to the provided leftSelector and rightSelector. The matching objects are merged into another value by resultSelector.
let first = [{ name: "Ivan", age: 21 }];
let second = [{ name: "Ivan", phone: "0123456789" }];
let joined = linq(first).join(second, f => f.name, s => s.name, (f, s) => ({ name: f.name, age: f.age, phone: s.phone }));
for (let group of joined) {
console.log(group);
}
{ name: 'Ivan', age: 21, phone: '0123456789' }
Orders elements in asceding order based on a selector function.
let people = [
{ id: 1, age: 18 },
{ id: 2, age: 29 },
{ id: 3, age: 8 },
{ id: 4, age: 20 },
{ id: 5, age: 18 },
{ id: 6, age: 32 },
{ id: 7, age: 5 },
];
let ordered = linq(people).orderBy(p => p.age)
for (let element of ordered) {
console.log(element);
}
{ id: 7, age: 5 }
{ id: 3, age: 8 }
{ id: 1, age: 18 }
{ id: 5, age: 18 }
{ id: 4, age: 20 }
{ id: 2, age: 29 }
{ id: 6, age: 32 }
Equivalent of orderBy
.
Orders elements in descending order based on a selector function.
Reverses the order of the sequence, e.g. reverse (1, 2, 3) -> (3, 2, 1)
let reversed = linq([1, 2, 3, 4, 5, 6, 7, 8])
.reverse()
for (let element of reversed) {
console.log(element);
}
8
7
6
5
4
3
2
1
Skips a specific number of elements.
let people = [
{ id: 1, age: 18 },
{ id: 2, age: 29 },
{ id: 3, age: 8 },
{ id: 4, age: 20 },
{ id: 5, age: 18 },
{ id: 6, age: 32 },
{ id: 7, age: 5 },
];
let elements = linq(people).skip(3);
for (let element of elements) {
console.log(element);
}
{ id: 4, age: 20 }
{ id: 5, age: 18 }
{ id: 6, age: 32 }
{ id: 7, age: 5 }
Skips the elements in the sequence while the predicate returns true
.
let people = [
{ id: 1, age: 18 },
{ id: 2, age: 20 },
{ id: 3, age: 30 },
{ id: 4, age: 25 },
{ id: 5, age: 18 },
{ id: 6, age: 32 },
{ id: 7, age: 5 },
];
let elements = linq(people).skipWhile(p => p.age % 2 === 0);
for (let element of elements) {
console.log(element);
}
{ id: 4, age: 25 }
{ id: 5, age: 18 }
{ id: 6, age: 32 }
{ id: 7, age: 5 }
Takes a specific number of elements.
let people = [
{ id: 1, age: 18 },
{ id: 2, age: 20 },
{ id: 3, age: 30 },
{ id: 4, age: 25 },
{ id: 5, age: 18 },
{ id: 6, age: 32 },
{ id: 7, age: 5 },
];
let elements = linq(people).take(4);
for (let element of elements) {
console.log(element);
}
{ id: 1, age: 18 },
{ id: 2, age: 20 },
{ id: 3, age: 30 },
{ id: 4, age: 25 }
Takes elements from the sequence while the predicate returns true
.
let people = [
{ id: 1, age: 18 },
{ id: 2, age: 20 },
{ id: 3, age: 30 },
{ id: 4, age: 25 },
{ id: 5, age: 18 },
{ id: 6, age: 32 },
{ id: 7, age: 5 },
];
let elements = linq(people).takeWhile(p => p.age % 2 === 0);
for (let element of elements) {
console.log(element);
}
{ id: 1, age: 18 },
{ id: 2, age: 20 },
{ id: 3, age: 30 }
Returns a sequence of elements which are not present in the sequence passed to except
.
let elements = linq([1, 2, 3, 4, 5, 6]).except([3, 5, 6]);
for (let element of elements) {
console.log(element);
}
1
2
4
Returns a sequence representing the intersection of the sequences - elements present in both sequences.
let elements = linq([1, 2, 3, 4, 5, 6]).intersect([3, 5, 6, 7, 8]);
for (let element of elements) {
console.log(element);
}
3
5
6
Concatenates the sequences together.
let elements = linq([1, 2, 3]).concat([4, 5, 6]);
for (let element of elements) {
console.log(element);
}
1
2
3
4
5
6
Performs a union operation on the current sequence and the provided sequence and returns a sequence of unique elements present in the both sequences.
let elements = linq([1, 2, 3, 3, 4, 5]).union([4, 5, 5, 6]);
for (let element of elements) {
console.log(element);
}
1
2
3
4
5
6
Returns a sequence of the elements which are present in only one of the iterables.
let elements = linq([1, 2, 3, 4]).xOr([2, 4, 5]);
for (let element of elements) {
console.log(element);
}
1
3
5
Reduces the sequence into a value using an accumulator function.
let people = [
{ name: "Ivan", age: 20 },
{ name: "Deyan", age: 22 }
];
let sumOfAges = linq(people).aggregate(0, (total, person) => total += person.age);
console.log(sumOfAges);
42
Provides a sliding window of elements from the sequence. By default the windows slides 1 element over. A second parameter may be provided to change the number of elements being skipped.
let windows = linq([1, 2, 3, 4, 5, 6]).windowed(3, 2);
for (let window of windows) {
console.log(window);
}
[ 1, 2, 3 ]
[ 3, 4, 5 ]
[ 5, 6 ]
Splits the sequence into batches/cunks of the specified size.
let batches = linq([1, 2, 3, 4, 5, 6, 7, 8]).batch(3);
for (let batch of batches) {
console.log(batch);
}
[ 1, 2, 3 ]
[ 4, 5, 6 ]
[ 7, 8 ]
Checks if any of the elements match the provided predicate.
let containsEven = linq([1, 2, 4, 6]).any(n => n % 2 === 0);
console.log(containsEven);
true
Checks if all of the elements match the provided predicate.
let areAllEvent = linq([1, 2, 4, 6]).all(n => n % 2 === 0);
console.log(areAllEvent);
false
Gets the min element in a sequence. Applicable when the elements are of type string
or number
.
let people = [1,5,-10,8];
console.log(linq(people).min());
-10
Gets the min element in a sequence according to a transform function.
let people = [
{ name: "Ivan", age: 25 },
{ name: "Deyan", age: 22 }
];
let youngest = linq(people).min(p => p.age);
console.log(youngest);
{ name: 'Deyan', age: 22 }
Gets the max element in a sequence. Applicable when the elements are of type string
or number
.
let people = [1,5,-10,8];
console.log(linq(people).max());
8
Gets the max element in a sequence according to a transform function.
let people = [
{ name: "Ivan", age: 25 },
{ name: "Deyan", age: 22 }
];
let oldest = linq(people).max(p => p.age);
console.log(oldest);
{ name: "Ivan", age: 25 }
Gets the averege value for a sequence. Applicable when the elements are of type number
.
let people = [25, 22];
console.log(linq(people).average(p => p.age));
23.5
Gets the averege of the values provided by a selector function.
let people = [
{ name: "Ivan", age: 25 },
{ name: "Deyan", age: 22 }
];
let averageAge = linq(people).average(p => p.age);
console.log(averageAge);
23.5
Tests the equality of two seuqneces by checking each corresponding pair of elements against the provided predicate. If a predicate is not provided the elements will be compared using the strict equality (===) operator.
let first = [1, 2, 3];
let second = [1, 2, 3];
let areEqual = linq(first).sequenceEquals(second);
console.log(areEqual);
true
Gets the index of the first matching element in the sequence.
linq([1, 2, 2, 2, 3]).indexOf(2);
1
Gets the index of the last matching element in the sequence.
linq([1, 2, 2, 2, 3]).indexOf(2);
1
Gets the index of the first matching element in the sequence according to the predicate.
linq([-1, -2, 2, 2, 3]).findIndex(x => x > 0);
2
Gets the index of the last matching element in the sequence according to the predicate.
linq([-1, -2, 2, 2, 3]).findIndex(x => x > 0);
4
Gets the element at an index.
let numbers = [1, 2, 3];
let elementAtIndexOne = linq(numbers).elementAt(1);
console.log(elementAtIndexOne);
2
Gets the first element of the iterable.
let numbers = [1, 2, 3];
let firstElement = linq(numbers).first();
console.log(firstElement);
1
Gets the first element of the sequence. If a predicate is provided the first element matching the predicated will be returned. If there aren't any matching elements or if the sequence is empty a default value provided by the defaultInitializer will be returned.
let numbers = [1, 2, 3];
let firstEvenElement = linq(numbers).firstOrDefault(n => n % 2 === 0);
let firstElementLargerThanFive = linq(numbers).firstOrDefault(n => n > 5, () => -1);
console.log(firstEvenElement);
console.log(firstElementLargerThanFive);
2
-1
Gets the last element of the iterable.
let numbers = [1, 2, 3];
let lastElement = linq(numbers).last();
console.log(lastElement);
3
Gets the last element of the sequence. If a predicate is provided the last element matching the predicated will be returned. If there aren't any matching elements or if the sequence is empty a default value provided by the defaultInitializer will be returned.
let numbers = [1, 2, 3, 4];
let lastEvenElement = linq(numbers).lastOrDefault(n => n % 2 === 0);
let lastElementLargerThanFive = linq(numbers).lastOrDefault(n => n > 5, () => -1);
console.log(lastEvenElement);
console.log(lastElementLargerThanFive);
4
-1
Calls a function for each element of the sequence. The function receives the element and its index in the seqeunce as parameters.
linq([1, 2, 3, 4]).forEach(console.log);
1 0
2 1
3 2
4 3
Turns the sequence to an array.
let array = linq([1, 2, 3, 4])
.concat([5, 6, 7])
.toArray();
console.log(array);
[ 1, 2, 3, 4, 5, 6, 7 ]
Counts the number of elements in the sequence.
let count = linq([1, 2, 3, 4]).count();
console.log(count);
4
Generates a sequence of numbers from start to end (if specified), increasing by the speficied step.
let limited = seq(1, 2, 10).toArray();
console.log(limited);
let unlimited = seq(1, 2).take(15).toArray();
console.log(unlimited);
[ 1, 3, 5, 7, 9 ]
[ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 ]
The identity function (x => x). It takes an element and returns it. It can be useful for operaions like min, max, average, and in general in cases where we want the transform function to return the same element.
let average = linq([1, 2, 3, 4, 5, 6]).average(id);
console.log(average);
3.5
Turns the sequence into a map.
The key is provided by a function which takes an element as a parameter and returns the value to be used as a key.
An optional vlaue selector can be provided to select the value that will be put in the map.
An optional equality comparer can be provided for special cases or complex keys. In that case a LinqMap instance will be returned.
An error will be thrown if multiple elements have the same key.
const elements = linq([1,2,3,4,5]).toMap({ keySelector: id, x => x * 10 });
console.log(elements);
Map { 1 => 10, 2 => 20, 3 => 30, 4 => 40, 5 => 50 }
Turns the sequence into a map.
The key is provided by a function which takes an element as a parameter and returns the value to be used as a key.
An optional vlaue selector can be provided to select the value that will be put in the map.
An optional equality comparer can be provided for special cases or complex keys. In that case a LinqMap instance will be returned.
The values for each key will be aggregated into arrays.
linq([1,1,2,3,3,4,5]).toMapMany({ keySelector: id, valueSelector: x => x * 10 });
Map {
1 => [ 10, 10 ],
2 => [ 20 ],
3 => [ 30, 30 ],
4 => [ 40 ],
5 => [ 50 ]
}
Turns the sequence into a set.
The values of the sequence will be aggregated into a Set.
An optional equality comparer can be provided for special cases or complex keys. In that case a LinqSet instance will be returned.
An error will be thrown if multiple elements have the same key.
const elements = linq([1,2,3,4,5]).toMap({ keySelector: id, x => x * 10 });
console.log(elements);
Map { 1 => 10, 2 => 20, 3 => 30, 4 => 40, 5 => 50 }
Append the provided elements at the end of the sequence.
linq([1,2,3,4,5]).append(6,7,8,9).toArray();
[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Prepends the provided elements at the beginning of the sequence.
linq([6,7,8,9]).prepend(1,2,3,4,5).toArray();
[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Executes an action on each element of the sequence and yields the element.
linq([6,7,8,9]).tap(el => console.log(el - 5)).toArray();
1
2
3
4
[ 6, 7, 8, 9 ]
Repeats the sequence when provided with a positive value, infinitely when provided with a negative value or without a count parameter.
Available also as a top-level function which repeats the provided element.
linq([1,2,3]).repeat(3).toArray();
repeat(1, 3).toArray();
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
[1, 1, 1]