Skip to content

Latest commit

 

History

History
1229 lines (874 loc) · 21.7 KB

README.md

File metadata and controls

1229 lines (874 loc) · 21.7 KB

LinqTS

Build Status Coverage Status

An api for lazy querying of iterables, implemented in TypeScript and inspired by .NET's LINQ methods.

Motivation

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.

Demo

Sync API

Equality Comparers

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.

Supported operations

  1. where
  2. select
  3. selectMany
  4. distinct
  5. distinctBy
  6. zip
  7. groupBy
  8. join
  9. orderBy
  10. orderByDescending
  11. reverse
  12. skip
  13. skipWhile
  14. take
  15. takeWhile
  16. except
  17. intersect
  18. concat
  19. union
  20. xOr
  21. aggregate
  22. windowed
  23. batch
  24. any
  25. all
  26. min
  27. minBy
  28. max
  29. maxBy
  30. average
  31. averageBy
  32. sequenceEquals
  33. indexOf
  34. lastIndexOf
  35. findIndex
  36. findLastIndex
  37. elementAt
  38. first
  39. firstOrDefault
  40. last
  41. lastOrDefault
  42. forEach
  43. toArray
  44. count
  45. seq
  46. id
  47. toMap
  48. toMapMany
  49. toSet
  50. append
  51. prepend
  52. tap
  53. 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)

Examples

Building and executing a query

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

Operations

Where

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

Select

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

SelectMany

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

Distinct

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 }

DistinctBy

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 }

Zip

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 }

GroupBy

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 ] ]

Join

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' }

OrderBy

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 }

OrderByDescending

Equivalent of orderBy. Orders elements in descending order based on a selector function.


Reverse

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

Skip

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 }

SkipWhile

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 }

Take

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 }

TakeWhile

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 }

Except

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

Intersect

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

Concat

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

Union

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

xOr

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

Aggregate

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

Windowed

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 ]

Batch

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 ]

Any

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

All

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

Min

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

MinBy

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 }

Max

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

MaxBy

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 }

Average

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

AverageBy

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

SequenceEquals

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

IndexOf

Gets the index of the first matching element in the sequence.

linq([1, 2, 2, 2, 3]).indexOf(2);
1

LastIndexOf

Gets the index of the last matching element in the sequence.

linq([1, 2, 2, 2, 3]).indexOf(2);
1

FindIndex

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

FindLastIndex

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

ElementAt

Gets the element at an index.

let numbers = [1, 2, 3];

let elementAtIndexOne = linq(numbers).elementAt(1);

console.log(elementAtIndexOne);
2

First

Gets the first element of the iterable.

let numbers = [1, 2, 3];

let firstElement = linq(numbers).first();

console.log(firstElement);
1

FirstOrDefault

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

Last

Gets the last element of the iterable.

let numbers = [1, 2, 3];

let lastElement = linq(numbers).last();

console.log(lastElement);
3

LastOrDefault

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

ForEach

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

ToArray

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 ]

Count

Counts the number of elements in the sequence.

let count = linq([1, 2, 3, 4]).count();

console.log(count);
4

Seq

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 ]

Id

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

ToMap

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 }

ToMapMany

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 ]
}

ToSet

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

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 ]

Prepend

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 ]

Tap

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 ]

Repeat

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]