From 2604e5fe8db59c2ddff2f38c3ba3da4fd99d0ff5 Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 13:19:12 +0100 Subject: [PATCH 01/10] Implement initial map functions --- src/collection-fns.ts | 2 + src/maps.ts | 116 ++++++++++++++++++++++++++++++++++++++++++ test/maps.test.ts | 107 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 src/maps.ts create mode 100644 test/maps.test.ts diff --git a/src/collection-fns.ts b/src/collection-fns.ts index ea87097b..22dd9618 100644 --- a/src/collection-fns.ts +++ b/src/collection-fns.ts @@ -1,4 +1,6 @@ import * as iterables from './iterables' +import * as maps from './maps' export * from './pipes' export const Iterables = iterables +export const Maps = maps diff --git a/src/maps.ts b/src/maps.ts new file mode 100644 index 00000000..96bf1f94 --- /dev/null +++ b/src/maps.ts @@ -0,0 +1,116 @@ +import { Iterables } from './collection-fns' + +export function ofIterable(source: Iterable<[Key, T]>): Map { + return new Map(source) +} + +export function toIterable(source: Map): Iterable<[Key, T]> { + return source.entries() +} + +export function map(source: Map, mapping: (key: Key, value: T) => U): Map +export function map( + mapping: (key: Key, value: T) => U +): (source: Map) => Map +export function map(a: any, b?: any): any { + const partial = typeof a === 'function' + const mapping: (key: Key, value: T) => U = partial ? a : b + function exec(source: Map) { + const target = new Map() + for (const pair of source.entries()) { + const key = pair[0] + const mapped = mapping(key, pair[1]) + target.set(key, mapped) + } + return target + } + return partial ? exec : exec(a) +} + +export function filter( + source: Map, + predicate: (key: Key, value: T) => boolean +): Map +export function filter( + predicate: (key: Key, value: T) => boolean +): (source: Map) => Map +export function filter(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (key: Key, value: T) => boolean = partial ? a : b + function exec(source: Map) { + return new Map( + Iterables.filter(source.entries(), entry => predicate(entry[0], entry[1])) + ) + } + return partial ? exec : exec(a) +} + +export function choose( + source: Map, + chooser: (key: Key, value: T) => U | undefined +): Map +export function choose( + chooser: (key: Key, value: T) => U | undefined +): (source: Map) => Map +export function choose(a: any, b?: any): any { + const partial = typeof a === 'function' + const chooser: (key: Key, value: T) => U | undefined = partial ? a : b + function exec(source: Map) { + const target = new Map() + for (const pair of source.entries()) { + const key = pair[0] + const mapped = chooser(key, pair[1]) + if (mapped !== undefined) { + target.set(key, mapped) + } + } + return target + } + return partial ? exec : exec(a) +} + +export function find(source: Map, key: Key): T +export function find(key: Key): (source: Map) => T +export function find(a: any, b?: any): any { + const partial = b === undefined + const key: Key = partial ? a : b + function exec(source: Map) { + if (!source.has(key)) { + throw new Error('Specified key not found in source') + } + return source.get(key) as T + } + return partial ? exec : exec(a) +} + +export function tryFind(source: Map, key: Key): T | undefined +export function tryFind(key: Key): (source: Map) => T | undefined +export function tryFind(a: any, b?: any): any { + const partial = b === undefined + const key: Key = partial ? a : b + function exec(source: Map) { + return source.get(key) + } + return partial ? exec : exec(a) +} + +export function exists( + source: Map, + predicate: (key: Key, value: T) => boolean +): boolean +export function exists( + predicate: (key: Key, value: T) => boolean +): (source: Map) => boolean +export function exists(a: any, b?: any): any { + const partial = b === undefined + const predicate: (key: Key, value: T) => boolean = partial ? a : b + function exec(source: Map) { + for (const item of source) { + if (predicate(item[0], item[1])) { + return true + } + } + return false + } + return partial ? exec : exec(a) +} diff --git a/test/maps.test.ts b/test/maps.test.ts new file mode 100644 index 00000000..667c4d14 --- /dev/null +++ b/test/maps.test.ts @@ -0,0 +1,107 @@ +import { Maps, pipe, Iterables } from '../src/collection-fns' + +test('ofIterable', () => { + expect( + Maps.ofIterable( + (function*(): Iterable<[string, number]> { + yield ['a', 1] + yield ['b', 2] + })() + ) + ).toEqual(new Map([['a', 1], ['b', 2]])) +}) + +test('toIterable', () => { + expect(Iterables.toArray(Maps.toIterable(new Map([['a', 1], ['b', 2]])))).toEqual([ + ['a', 1], + ['b', 2] + ]) +}) + +describe('map', () => { + test('immediate', () => { + expect( + Maps.map(new Map([['a', 1], ['b', 2]]), (key, value) => { + return value * 2 + }) + ).toEqual(new Map([['a', 2], ['b', 4]])) + }) + test('piped', () => { + expect( + pipe(new Map([['a', 1], ['b', 2]])).then( + Maps.map((key, value) => { + return value * 2 + }) + ).result + ).toEqual(new Map([['a', 2], ['b', 4]])) + }) +}) + +describe('filter', () => { + test('immediate', () => { + expect(Maps.filter(new Map([['a', 1], ['b', 2]]), (key, value) => value % 2 === 0)).toEqual( + new Map([['b', 2]]) + ) + }) + test('piped', () => { + expect( + pipe(new Map([['a', 1], ['b', 2]])).then(Maps.filter((key, value) => value % 2 === 0)).result + ).toEqual(new Map([['b', 2]])) + }) +}) + +describe('choose', () => { + test('immediate', () => { + expect( + Maps.choose( + new Map([['a', 1], ['b', 2], ['c', 3]]), + (key, value) => (value % 2 === 1 ? key + value : undefined) + ) + ).toEqual(new Map([['a', 'a1'], ['c', 'c3']])) + }) + test('piped', () => { + expect( + pipe(new Map([['a', 1], ['b', 2], ['c', 3]])).then( + Maps.choose((key, value) => (value % 2 === 1 ? key + value : undefined)) + ).result + ).toEqual(new Map([['a', 'a1'], ['c', 'c3']])) + }) +}) + +describe('find', () => { + test('immediate', () => { + expect(Maps.find(new Map([['a', 1], ['b', 2]]), 'b')).toEqual(2) + }) + test('piped', () => { + expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.find('a')).result).toEqual(1) + }) + test('not found', () => { + expect(() => Maps.find(new Map([['a', 1], ['b', 2]]), 'c')).toThrow('') + }) +}) + +describe('tryFind', () => { + test('immediate', () => { + expect(Maps.tryFind(new Map([['a', 1], ['b', 2]]), 'b')).toEqual(2) + }) + test('piped', () => { + expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.tryFind('a')).result).toEqual(1) + }) + test('not found', () => { + expect(Maps.tryFind(new Map([['a', 1], ['b', 2]]), 'c')).toBeUndefined() + }) +}) + +describe('exists', () => { + test('immediate', () => { + expect(Maps.exists(new Map([['a', 1], ['b', 2]]), (key, value) => value === 2)).toEqual(true) + }) + test('piped', () => { + expect( + pipe(new Map([['a', 1], ['b', 2]])).then(Maps.exists((key, value) => value === 2)).result + ).toEqual(true) + }) + test('not found', () => { + expect(Maps.exists(new Map([['a', 1], ['b', 2]]), (key, value) => value === 3)).toEqual(false) + }) +}) From bd576863c00e81e3a0c17d46f54032c63185fde5 Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 13:22:04 +0100 Subject: [PATCH 02/10] Change const to function for consistency --- src/iterables.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/iterables.ts b/src/iterables.ts index 0a77b578..93301988 100644 --- a/src/iterables.ts +++ b/src/iterables.ts @@ -1,4 +1,4 @@ -export const toArray = (source: Iterable): T[] => { +export function toArray(source: Iterable): T[] { return Array.from(source) } @@ -82,7 +82,7 @@ export function append(a: any, b?: any): any { return partial ? exec : exec(a) } -export const concat = function*(sources: Iterable>): Iterable { +export function* concat(sources: Iterable>): Iterable { for (const source of sources) { for (const item of source) { yield item @@ -179,7 +179,7 @@ export function init(a: any, b?: any): any { return partial ? exec : exec(a) } -export const length = (source: Iterable): number => { +export function length(source: Iterable): number { let length = 0 for (const _ of source) { length++ From df6e479ea61dea45c2c2f73c88c83d1aa8b4d40a Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 13:28:26 +0100 Subject: [PATCH 03/10] Rename find to get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rational: - Get implies that you expect to find something … so not found is an exception. - Find implies that you’re looking, but it might not be there, so non-exceptional. --- src/maps.ts | 12 ++++++------ test/maps.test.ts | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/maps.ts b/src/maps.ts index 96bf1f94..08e4b385 100644 --- a/src/maps.ts +++ b/src/maps.ts @@ -69,9 +69,9 @@ export function choose(a: any, b?: any): any { return partial ? exec : exec(a) } -export function find(source: Map, key: Key): T -export function find(key: Key): (source: Map) => T -export function find(a: any, b?: any): any { +export function get(source: Map, key: Key): T +export function get(key: Key): (source: Map) => T +export function get(a: any, b?: any): any { const partial = b === undefined const key: Key = partial ? a : b function exec(source: Map) { @@ -83,9 +83,9 @@ export function find(a: any, b?: any): any { return partial ? exec : exec(a) } -export function tryFind(source: Map, key: Key): T | undefined -export function tryFind(key: Key): (source: Map) => T | undefined -export function tryFind(a: any, b?: any): any { +export function find(source: Map, key: Key): T | undefined +export function find(key: Key): (source: Map) => T | undefined +export function find(a: any, b?: any): any { const partial = b === undefined const key: Key = partial ? a : b function exec(source: Map) { diff --git a/test/maps.test.ts b/test/maps.test.ts index 667c4d14..8468387f 100644 --- a/test/maps.test.ts +++ b/test/maps.test.ts @@ -68,27 +68,27 @@ describe('choose', () => { }) }) -describe('find', () => { +describe('get', () => { test('immediate', () => { - expect(Maps.find(new Map([['a', 1], ['b', 2]]), 'b')).toEqual(2) + expect(Maps.get(new Map([['a', 1], ['b', 2]]), 'b')).toEqual(2) }) test('piped', () => { - expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.find('a')).result).toEqual(1) + expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.get('a')).result).toEqual(1) }) test('not found', () => { - expect(() => Maps.find(new Map([['a', 1], ['b', 2]]), 'c')).toThrow('') + expect(() => Maps.get(new Map([['a', 1], ['b', 2]]), 'c')).toThrow('') }) }) -describe('tryFind', () => { +describe('find', () => { test('immediate', () => { - expect(Maps.tryFind(new Map([['a', 1], ['b', 2]]), 'b')).toEqual(2) + expect(Maps.find(new Map([['a', 1], ['b', 2]]), 'b')).toEqual(2) }) test('piped', () => { - expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.tryFind('a')).result).toEqual(1) + expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.find('a')).result).toEqual(1) }) test('not found', () => { - expect(Maps.tryFind(new Map([['a', 1], ['b', 2]]), 'c')).toBeUndefined() + expect(Maps.find(new Map([['a', 1], ['b', 2]]), 'c')).toBeUndefined() }) }) From 4732c37719041f9119d161637dc23721c2e0bf7c Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 13:39:16 +0100 Subject: [PATCH 04/10] Implement get for iterables --- src/iterables.ts | 16 ++++++++++++++++ test/iterable.test.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/iterables.ts b/src/iterables.ts index 93301988..0518dfd4 100644 --- a/src/iterables.ts +++ b/src/iterables.ts @@ -124,6 +124,22 @@ export function exists(a: any, b?: any): any { return partial ? exec : exec(a) } +export function get(predicate: (item: T) => boolean): (source: Iterable) => T +export function get(source: Iterable, predicate: (item: T) => boolean): T +export function get(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: Iterable): T | undefined { + for (const item of source) { + if (predicate(item)) { + return item + } + } + throw new Error('Element not found matching criteria') + } + return partial ? exec : exec(a) +} + export function find(predicate: (item: T) => boolean): (source: Iterable) => T | undefined export function find(source: Iterable, predicate: (item: T) => boolean): T | undefined export function find(a: any, b?: any): any { diff --git a/test/iterable.test.ts b/test/iterable.test.ts index d2fc25e8..db2b5aab 100644 --- a/test/iterable.test.ts +++ b/test/iterable.test.ts @@ -234,6 +234,41 @@ describe('exists', () => { }) }) +describe('get', () => { + it('finds match', () => { + expect( + pipe( + (function*() { + yield { name: 'amy', id: 1 } + yield { name: 'bob', id: 2 } + })() + ).then(Iterables.get(x => x.name === 'bob')).result + ).toEqual({ name: 'bob', id: 2 }) + }) + it('throws when not found', () => { + expect( + () => + pipe( + (function*() { + yield { name: 'amy', id: 1 } + yield { name: 'bob', id: 2 } + })() + ).then(Iterables.get(x => x.name === 'cat')).result + ).toThrow('Element not found matching criteria') + }) + it('finds without partial application', () => { + expect( + Iterables.get( + (function*() { + yield { name: 'amy', id: 1 } + yield { name: 'bob', id: 2 } + })(), + x => x.name === 'bob' + ) + ).toEqual({ name: 'bob', id: 2 }) + }) +}) + describe('find', () => { it('finds match', () => { expect( From faec2ffba0992070299c5bc7a9f21b8d80965790 Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 13:39:31 +0100 Subject: [PATCH 05/10] Improve map get error & test --- src/maps.ts | 2 +- test/maps.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/maps.ts b/src/maps.ts index 08e4b385..e247baa5 100644 --- a/src/maps.ts +++ b/src/maps.ts @@ -76,7 +76,7 @@ export function get(a: any, b?: any): any { const key: Key = partial ? a : b function exec(source: Map) { if (!source.has(key)) { - throw new Error('Specified key not found in source') + throw new Error('Specified key not found') } return source.get(key) as T } diff --git a/test/maps.test.ts b/test/maps.test.ts index 8468387f..9f6b61b8 100644 --- a/test/maps.test.ts +++ b/test/maps.test.ts @@ -76,7 +76,7 @@ describe('get', () => { expect(pipe(new Map([['a', 1], ['b', 2]])).then(Maps.get('a')).result).toEqual(1) }) test('not found', () => { - expect(() => Maps.get(new Map([['a', 1], ['b', 2]]), 'c')).toThrow('') + expect(() => Maps.get(new Map([['a', 1], ['b', 2]]), 'c')).toThrow('Specified key not found') }) }) From 4c8f21d3901c3f3b44ac282d995d99971acc3b93 Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 15:13:42 +0100 Subject: [PATCH 06/10] Implement initial arrays module Mirror iterables implementation --- src/arrays.ts | 279 ++++++++++++++++++++++++++++++++++++ src/collection-fns.ts | 2 + test/arrays.test.ts | 318 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 599 insertions(+) create mode 100644 src/arrays.ts create mode 100644 test/arrays.test.ts diff --git a/src/arrays.ts b/src/arrays.ts new file mode 100644 index 00000000..edab2eb1 --- /dev/null +++ b/src/arrays.ts @@ -0,0 +1,279 @@ +export function ofIterable(source: Iterable): T[] { + return Array.from(source) +} + +export function map(mapping: (item: T) => U): (source: T[]) => U[] +export function map(source: T[], mapping: (item: T) => U): U[] +export function map(a: any, b?: any): any { + const partial = typeof a === 'function' + const mapping: (item: T) => U = partial ? a : b + function exec(source: T[]) { + return source.map(mapping) + } + return partial ? exec : exec(a) +} + +export function filter(predicate: (item: T) => boolean): (source: T[]) => T[] +export function filter(source: T[], predicate: (item: T) => boolean): T[] +export function filter(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: T[]) { + return source.filter(predicate) + } + return partial ? exec : exec(a) +} + +export function choose(chooser: (item: T) => U | undefined): (source: T[]) => U[] +export function choose(source: T[], chooser: (item: T) => U | undefined): U[] +export function choose(a: any, b?: any): any { + const partial = typeof a === 'function' + const chooser: (item: T) => U | undefined = partial ? a : b + function exec(source: T[]) { + const target = [] + for (const item of source) { + const chosen = chooser(item) + if (chosen !== undefined) { + target.push(chosen) + } + } + return target + } + return partial ? exec : exec(a) +} + +export function collect(mapping: (item: T) => U[]): (source: T[]) => U[] +export function collect(source: T[], mapping: (item: T) => U[]): U[] +export function collect(a: any, b?: any): any { + const partial = typeof a === 'function' + const mapping: (item: T) => U[] = partial ? a : b + function exec(source: T[]) { + const target = [] + for (const item of source) { + const children = mapping(item) + for (const child of children) { + target.push(child) + } + } + return target + } + return partial ? exec : exec(a) +} + +export function append(second: T[]): (first: T[]) => T[] +export function append(first: T[], second: T[]): T[] +export function append(a: any, b?: any): any { + const partial = b === undefined + const second: T[] = partial ? a : b + function exec(first: T[]): T[] { + return ([] as T[]).concat(first).concat(second) + } + return partial ? exec : exec(a) +} + +export function concat(sources: T[][]): T[] { + const target = [] + for (const source of sources) { + for (const item of source) { + target.push(item) + } + } + return target +} + +export function distinctBy(selector: (item: T) => Key): (source: T[]) => T[] +export function distinctBy(source: T[], selector: (item: T) => Key): T[] +export function distinctBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => Key = partial ? a : b + function exec(source: T[]): T[] { + const seen = new Map() + for (const item of source) { + const key = selector(item) + if (!seen.has(key)) { + seen.set(key, item) + } + } + return Array.from(seen.values()) + } + return partial ? exec : exec(a) +} + +export function exists(predicate: (item: T) => boolean): (source: T[]) => boolean +export function exists(source: T[], predicate: (item: T) => boolean): boolean +export function exists(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: T[]): boolean { + return source.some(predicate) + } + return partial ? exec : exec(a) +} + +export function get(predicate: (item: T) => boolean): (source: T[]) => T +export function get(source: T[], predicate: (item: T) => boolean): T +export function get(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: T[]): T | undefined { + for (const item of source) { + if (predicate(item)) { + return item + } + } + throw new Error('Element not found matching criteria') + } + return partial ? exec : exec(a) +} + +export function find(predicate: (item: T) => boolean): (source: T[]) => T | undefined +export function find(source: T[], predicate: (item: T) => boolean): T | undefined +export function find(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: T[]): T | undefined { + for (const item of source) { + if (predicate(item)) { + return item + } + } + return undefined + } + return partial ? exec : exec(a) +} + +export function groupBy(selector: (item: T) => Key): (source: T[]) => [Key, T[]][] +export function groupBy(source: T[], selector: (item: T) => Key): [Key, T[]][] +export function groupBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => Key = partial ? a : b + function exec(source: T[]): [Key, T[]][] { + const groups = new Map() + for (const item of source) { + const key = selector(item) + const group = groups.get(key) + if (group === undefined) { + groups.set(key, [item]) + } else { + group.push(item) + } + } + return Array.from(groups.entries()) + } + return partial ? exec : exec(a) +} + +export function init(count: number): (initializer: (index: number) => T) => T[] +export function init(initializer: (index: number) => T, count: number): T[] +export function init(a: any, b?: any): any { + const partial = typeof a === 'number' + const count: number = partial ? a : b + function exec(initializer: (index: number) => T): T[] { + const target = [] + for (let index = 0; index < count; index++) { + target.push(initializer(index)) + } + return target + } + return partial ? exec : exec(a) +} + +export function length(source: T[]): number { + return source.length +} + +function compareBy(getProp: (item: T) => any) { + return (a: T, b: T) => { + return getProp(a) > getProp(b) ? 1 : -1 + } +} + +export function sortBy(selector: (item: T) => Key): (source: T[]) => T[] +export function sortBy(source: T[], selector: (item: T) => Key): T[] +export function sortBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => Key = partial ? a : b + function exec(source: T[]): T[] { + const copy = Array.from(source) + copy.sort(compareBy(selector)) + return copy + } + return partial ? exec : exec(a) +} + +export function sumBy(selector: (item: T) => number): (source: T[]) => number +export function sumBy(source: T[], selector: (item: T) => number): number +export function sumBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => number = partial ? a : b + function exec(source: T[]): number { + let sum = 0 + for (const item of source) { + sum += selector(item) + } + return sum + } + return partial ? exec : exec(a) +} + +export function maxBy(selector: (item: T) => number): (source: T[]) => number +export function maxBy(source: T[], selector: (item: T) => number): number +export function maxBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => number = partial ? a : b + function exec(source: T[]): number { + let max: number | null = null + for (const item of source) { + const value = selector(item) + if (max === null || value > max) { + max = value + } + } + if (max === null) { + throw new Error(`Can't find max of an empty array`) + } + return max + } + return partial ? exec : exec(a) +} + +export function minBy(selector: (item: T) => number): (source: T[]) => number +export function minBy(source: T[], selector: (item: T) => number): number +export function minBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => number = partial ? a : b + function exec(source: T[]): number { + let min: number | null = null + for (const item of source) { + const value = selector(item) + if (min === null || value < min) { + min = value + } + } + if (min === null) { + throw new Error(`Can't find min of an empty array`) + } + return min + } + return partial ? exec : exec(a) +} + +export function meanBy(selector: (item: T) => number): (source: T[]) => number +export function meanBy(source: T[], selector: (item: T) => number): number +export function meanBy(a: any, b?: any): any { + const partial = typeof a === 'function' + const selector: (item: T) => number = partial ? a : b + function exec(source: T[]): number { + let sum = 0 + let count = 0 + for (const item of source) { + sum += selector(item) + count++ + } + if (count === 0) { + throw new Error(`Can't find mean of an empty array`) + } + return sum / count + } + return partial ? exec : exec(a) +} diff --git a/src/collection-fns.ts b/src/collection-fns.ts index 22dd9618..fc4d40e6 100644 --- a/src/collection-fns.ts +++ b/src/collection-fns.ts @@ -1,6 +1,8 @@ import * as iterables from './iterables' +import * as arrays from './arrays' import * as maps from './maps' export * from './pipes' export const Iterables = iterables +export const Arrays = arrays export const Maps = maps diff --git a/test/arrays.test.ts b/test/arrays.test.ts new file mode 100644 index 00000000..fc9ab1c3 --- /dev/null +++ b/test/arrays.test.ts @@ -0,0 +1,318 @@ +import { pipe, Arrays } from '../src/collection-fns' + +describe('ofIterable', () => { + it('constructs an array', () => { + const iterator = function*() { + yield 1 + yield 2 + } + expect(Arrays.ofIterable(iterator())).toEqual([1, 2]) + }) +}) + +describe('map', () => { + test('empty', () => { + expect(Arrays.map(x => x)([])).toEqual([]) + }) + test('piped', () => { + expect(pipe([1, 2]).then(Arrays.map(x => x * 2)).result).toEqual([2, 4]) + }) + test('invoke', () => { + expect(Arrays.map([1, 2], x => x * 2)).toEqual([2, 4]) + }) +}) + +describe('filter', () => { + test('empty', () => { + expect(Arrays.filter([], x => true)).toEqual([]) + }) + test('piped', () => { + expect(pipe([1, 2, 3, 4]).then(Arrays.filter(x => x % 2 === 0)).result).toEqual([2, 4]) + }) + test('invoke', () => { + expect(Arrays.filter([1, 2, 3, 4], x => x % 2 === 0)).toEqual([2, 4]) + }) +}) + +describe('choose', () => { + test('piped', () => { + expect( + pipe([1, 2, 3]).then(Arrays.choose(x => (x % 2 === 1 ? x * 2 : undefined))).result + ).toEqual([2, 6]) + }) + test('invoke', () => { + expect(Arrays.choose([1, 2, 3], x => (x % 2 === 1 ? x * 2 : undefined))).toEqual([2, 6]) + }) +}) + +describe('collect', () => { + test('piped', () => { + expect( + pipe([1, 2]).then( + Arrays.collect(function(x) { + return [x, x] + }) + ).result + ).toEqual([1, 1, 2, 2]) + }) + test('invoke', () => { + expect( + Arrays.collect([1, 2], function(x) { + return [x, x] + }) + ).toEqual([1, 1, 2, 2]) + }) +}) + +describe('append', () => { + test('piped', () => { + expect(pipe([1]).then(Arrays.append([2])).result).toEqual([1, 2]) + }) + test('invoke', () => { + expect(Arrays.append([1], [2])).toEqual([1, 2]) + }) +}) + +test('concat', () => { + expect(Arrays.concat([[1, 2], [3, 4], [5]])).toEqual([1, 2, 3, 4, 5]) +}) + +describe('distinctBy', () => { + test('piping', () => { + expect( + pipe([ + { name: 'amy', id: 1 }, + { name: 'bob', id: 2 }, + { name: 'bob', id: 3 }, + { name: 'cat', id: 3 } + ]).then(Arrays.distinctBy(x => x.name)).result + ).toEqual([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }, { name: 'cat', id: 3 }]) + }) + test('invoke', () => { + expect( + Arrays.distinctBy( + [ + { name: 'amy', id: 1 }, + { name: 'bob', id: 2 }, + { name: 'bob', id: 3 }, + { name: 'cat', id: 3 } + ], + x => x.name + ) + ).toEqual([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }, { name: 'cat', id: 3 }]) + }) +}) + +describe('exists', () => { + it('matches existance', () => { + expect(pipe([1, 2]).then(Arrays.exists(x => x === 1)).result).toEqual(true) + }) + it('matches non-existance', () => { + expect(pipe([1, 2]).then(Arrays.exists(x => x === 3)).result).toEqual(false) + }) + test('invoke', () => { + expect(Arrays.exists([1, 2], x => x === 1)).toEqual(true) + }) +}) + +describe('get', () => { + test('piped match', () => { + expect( + pipe([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }]).then(Arrays.get(x => x.name === 'bob')) + .result + ).toEqual({ name: 'bob', id: 2 }) + }) + test('no match', () => { + expect( + () => + pipe([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }]).then( + Arrays.get(x => x.name === 'cat') + ).result + ).toThrow('Element not found matching criteria') + }) + test('invoke', () => { + expect( + Arrays.get([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }], x => x.name === 'bob') + ).toEqual({ name: 'bob', id: 2 }) + }) +}) + +describe('find', () => { + test('piped match', () => { + expect( + pipe([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }]).then( + Arrays.find(x => x.name === 'bob') + ).result + ).toEqual({ name: 'bob', id: 2 }) + }) + it('returns undefined when not found', () => { + expect( + pipe([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }]).then( + Arrays.find(x => x.name === 'cat') + ).result + ).toBeUndefined() + }) + test('invoke', () => { + expect( + Arrays.find([{ name: 'amy', id: 1 }, { name: 'bob', id: 2 }], x => x.name === 'bob') + ).toEqual({ name: 'bob', id: 2 }) + }) +}) + +describe('groupBy', () => { + test('piped', () => { + expect( + pipe([{ name: 'amy', age: 1 }, { name: 'bob', age: 2 }, { name: 'cat', age: 2 }]).then( + Arrays.groupBy(x => x.age) + ).result + ).toEqual([ + [1, [{ name: 'amy', age: 1 }]], + [2, [{ name: 'bob', age: 2 }, { name: 'cat', age: 2 }]] + ]) + }) + test('invoke', () => { + expect( + Arrays.groupBy( + [{ name: 'amy', age: 1 }, { name: 'bob', age: 2 }, { name: 'cat', age: 2 }], + x => x.age + ) + ).toEqual([ + [1, [{ name: 'amy', age: 1 }]], + [2, [{ name: 'bob', age: 2 }, { name: 'cat', age: 2 }]] + ]) + }) +}) + +describe('init', () => { + test('piped', () => { + expect(pipe(Arrays.init(3)(i => i + 1)).result).toEqual([1, 2, 3]) + }) + test('empty', () => { + expect(pipe(Arrays.init(0)(i => i)).result).toEqual([]) + }) + test('invoke', () => { + expect(Arrays.init(i => i + 1, 3)).toEqual([1, 2, 3]) + }) +}) + +describe('length', () => { + test('zero length', () => { + expect(Arrays.length([])).toEqual(0) + }) + test('non-zero length', () => { + expect(Arrays.length([1, 2, 3, 4, 5])).toEqual(5) + }) +}) + +describe('sortBy', () => { + test('piped', () => { + expect( + pipe([{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }]).then( + Arrays.sortBy(x => x.age) + ).result + ).toEqual([{ name: 'bob', age: 2 }, { name: 'cat', age: 18 }, { name: 'amy', age: 21 }]) + }) + test('invoke', () => { + expect( + Arrays.sortBy( + [{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }], + x => x.age + ) + ).toEqual([{ name: 'bob', age: 2 }, { name: 'cat', age: 18 }, { name: 'amy', age: 21 }]) + }) +}) + +describe('sumBy', () => { + test('piping', () => { + expect( + pipe([{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }]).then( + Arrays.sumBy(x => x.age) + ).result + ).toEqual(41) + }) + test('invoke', () => { + expect( + Arrays.sumBy( + [{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }], + x => x.age + ) + ).toEqual(41) + }) +}) + +describe('maxBy', () => { + test('piping', () => { + expect( + pipe([{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }]).then( + Arrays.maxBy(x => x.age) + ).result + ).toEqual(21) + }) + it('fails on empty collection', () => { + expect(() => pipe([]).then(Arrays.maxBy(x => x)).result).toThrow( + `Can't find max of an empty array` + ) + }) + test('invoke', () => { + expect( + Arrays.maxBy( + [{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }], + x => x.age + ) + ).toEqual(21) + }) +}) + +describe('minBy', () => { + test('piping', () => { + expect( + pipe([{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }]).then( + Arrays.minBy(x => x.age) + ).result + ).toEqual(2) + }) + it('fails on empty collection', () => { + expect(() => pipe([]).then(Arrays.minBy(x => x)).result).toThrow( + `Can't find min of an empty array` + ) + }) + test('invoke', () => { + expect( + Arrays.minBy( + [{ name: 'amy', age: 21 }, { name: 'bob', age: 2 }, { name: 'cat', age: 18 }], + x => x.age + ) + ).toEqual(2) + }) +}) + +describe('meanBy', () => { + test('piping', () => { + expect( + pipe([ + { name: 'amy', age: 21 }, + { name: 'bob', age: 2 }, + { name: 'cat', age: 18 }, + { name: 'dot', age: 39 } + ]).then(Arrays.meanBy(x => x.age)).result + ).toEqual(20) + }) + it('fails on empty collection', () => { + expect(() => pipe([]).then(Arrays.meanBy(x => x)).result).toThrow( + `Can't find mean of an empty array` + ) + }) + test('invoke', () => { + expect( + Arrays.meanBy( + [ + { name: 'amy', age: 21 }, + { name: 'bob', age: 2 }, + { name: 'cat', age: 18 }, + { name: 'dot', age: 39 } + ], + x => x.age + ) + ).toEqual(20) + }) +}) From df5ca2cbc39e3c159772ffdfeb419f1747c3c1eb Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 17:10:55 +0100 Subject: [PATCH 07/10] Improve iterable init functions Create dedicated infinite version to avoid accidental infinite sequences. --- src/iterables.ts | 79 ++++++++++++++++++++++++-- test/iterable.test.ts | 128 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 193 insertions(+), 14 deletions(-) diff --git a/src/iterables.ts b/src/iterables.ts index 0518dfd4..d1049795 100644 --- a/src/iterables.ts +++ b/src/iterables.ts @@ -182,14 +182,81 @@ export function groupBy(a: any, b?: any): any { return partial ? exec : exec(a) } -export function init(count: number): (initializer: (index: number) => T) => Iterable -export function init(initializer: (index: number) => T, count: number): Iterable -export function init(a: any, b?: any): any { +export interface InitRange { + from: number + to: number + increment?: number +} + +export interface InitCount { + start?: number + count: number + increment?: number +} + +export function* init(options: number | InitRange | InitCount): Iterable { + function normaliseOptions() { + if (typeof options === 'number') { + return { + start: 0, + count: options, + increment: 1 + } + } + if ('from' in options) { + const sign = options.to < options.from ? -1 : 1 + if ( + options.increment !== undefined && + (options.increment === 0 || options.increment / sign < 0) + ) { + throw new Error( + 'Iterable will never complete.\nUse initInfinite if this is desired behaviour' + ) + } + const increment = options.increment ? options.increment : sign + return { + start: options.from, + count: (options.to - options.from) / increment + 1, + increment: increment + } + } + const start = options.start === undefined ? 0 : options.start + return { + start, + count: options.count, + increment: options.increment === undefined ? 1 : options.increment + } + } + const { start, count, increment } = normaliseOptions() + let current = start + for (let index = 0; index < count; index++) { + yield current + current += increment + } +} + +export function* initInfinite(options?: { start?: number; increment?: number }): Iterable { + const start = options !== undefined && options.start !== undefined ? options.start : 0 + const increment = options !== undefined && options.increment !== undefined ? options.increment : 1 + for (let index = start; true; index += increment) { + yield index + } +} + +export function take(count: number): (source: Iterable) => Iterable +export function take(source: Iterable, count: number): Iterable +export function take(a: any, b?: any): any { const partial = typeof a === 'number' const count: number = partial ? a : b - function* exec(initializer: (index: number) => T): Iterable { - for (let index = 0; index < count; index++) { - yield initializer(index) + function* exec(source: Iterable): Iterable { + let i = 0 + for (const item of source) { + if (i < count) { + i++ + yield item + } else { + break + } } } return partial ? exec : exec(a) diff --git a/test/iterable.test.ts b/test/iterable.test.ts index db2b5aab..1f72d974 100644 --- a/test/iterable.test.ts +++ b/test/iterable.test.ts @@ -340,23 +340,135 @@ describe('groupBy', () => { }) describe('init', () => { - it('creates elements', () => { - expect(pipe(Iterables.init(3)(i => i + 1)).then(Iterables.toArray).result).toEqual([1, 2, 3]) + test('empty', () => { + expect(pipe(Iterables.init(0)).then(Iterables.toArray).result).toEqual([]) }) - it('can create empty', () => { - expect(pipe(Iterables.init(0)(i => i)).then(Iterables.toArray).result).toEqual([]) + test('just count', () => { + expect(pipe(Iterables.init(5)).then(Iterables.toArray).result).toEqual([0, 1, 2, 3, 4]) }) - it('can init without partial application', () => { - expect(Iterables.toArray(Iterables.init(i => i + 1, 3))).toEqual([1, 2, 3]) + test('from-to', () => { + expect(pipe(Iterables.init({ from: 1, to: 3 })).then(Iterables.toArray).result).toEqual([ + 1, + 2, + 3 + ]) + }) + test('from-to-same', () => { + expect(pipe(Iterables.init({ from: 1, to: 1 })).then(Iterables.toArray).result).toEqual([1]) + }) + test('from-to fractional-increment', () => { + expect( + pipe(Iterables.init({ from: 1, to: 2, increment: 0.5 })).then(Iterables.toArray).result + ).toEqual([1, 1.5, 2]) + }) + test('from positive to negative', () => { + expect(pipe(Iterables.init({ from: 1, to: -1 })).then(Iterables.toArray).result).toEqual([ + 1, + 0, + -1 + ]) + }) + test('from negative to positive', () => { + expect(pipe(Iterables.init({ from: -1, to: 1 })).then(Iterables.toArray).result).toEqual([ + -1, + 0, + 1 + ]) + }) + test('from-to zero increment fails', () => { + expect( + () => pipe(Iterables.init({ from: 1, to: 2, increment: 0 })).then(Iterables.toArray).result + ).toThrow('Iterable will never complete.\nUse initInfinite if this is desired behaviour') + }) + test('from-to negative fails', () => { + expect( + () => pipe(Iterables.init({ from: 1, to: 2, increment: -0.1 })).then(Iterables.toArray).result + ).toThrow('Iterable will never complete.\nUse initInfinite if this is desired behaviour') + }) + test('from-to negative crossing zero fails', () => { + expect( + () => pipe(Iterables.init({ from: -1, to: 1, increment: -1 })).then(Iterables.toArray).result + ).toThrow('Iterable will never complete.\nUse initInfinite if this is desired behaviour') + }) + test('from-to reversed fails', () => { + expect( + () => pipe(Iterables.init({ from: 2, to: 1, increment: 1 })).then(Iterables.toArray).result + ).toThrow('Iterable will never complete.\nUse initInfinite if this is desired behaviour') + }) + test('from-to reversed crossing zero fails', () => { + expect( + () => pipe(Iterables.init({ from: 1, to: -1, increment: 0.1 })).then(Iterables.toArray).result + ).toThrow('Iterable will never complete.\nUse initInfinite if this is desired behaviour') + }) +}) + +describe('initInfinite', () => { + test('defaults', () => { + expect( + pipe(Iterables.initInfinite()) + .then(Iterables.take(5)) + .then(Iterables.toArray).result + ).toEqual([0, 1, 2, 3, 4]) + }) + test('no properties', () => { + expect( + pipe(Iterables.initInfinite({})) + .then(Iterables.take(5)) + .then(Iterables.toArray).result + ).toEqual([0, 1, 2, 3, 4]) + }) + test('just start', () => { + expect( + pipe(Iterables.initInfinite({ start: 5 })) + .then(Iterables.take(5)) + .then(Iterables.toArray).result + ).toEqual([5, 6, 7, 8, 9]) + }) + test('just increment', () => { + expect( + pipe(Iterables.initInfinite({ increment: 5 })) + .then(Iterables.take(5)) + .then(Iterables.toArray).result + ).toEqual([0, 5, 10, 15, 20]) + }) + test('fractional increment', () => { + expect( + pipe(Iterables.initInfinite({ increment: 0.5 })) + .then(Iterables.take(5)) + .then(Iterables.toArray).result + ).toEqual([0, 0.5, 1, 1.5, 2]) + }) + test('custom range', () => { + expect( + pipe(Iterables.initInfinite({ start: 5, increment: 0.5 })) + .then(Iterables.take(5)) + .then(Iterables.toArray).result + ).toEqual([5, 5.5, 6, 6.5, 7]) + }) +}) + +describe('take', () => { + test('piped', () => { + expect( + pipe( + (function*() { + while (true) { + yield 0 + } + })() + ) + .then(Iterables.take(3)) + .then(Iterables.toArray).result + ).toEqual([0, 0, 0]) }) }) describe('length', () => { it('can return zero length', () => { - expect(pipe(Iterables.init(0)(i => i)).then(Iterables.length).result).toEqual(0) + expect(pipe(Iterables.init({ count: 0 })).then(Iterables.length).result).toEqual(0) }) it('can return non-zero length', () => { - expect(pipe(Iterables.init(5)(i => i)).then(Iterables.length).result).toEqual(5) + expect(pipe(Iterables.init({ count: 5 })).then(Iterables.length).result).toEqual(5) }) }) From cd14028266a5781ccca303cb1cc9d25622658e9c Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Mon, 16 Jul 2018 17:38:01 +0100 Subject: [PATCH 08/10] Improve array init Improve test coverage too --- src/arrays.ts | 69 ++++++++++++++++++++++++++++++++++++------- test/arrays.test.ts | 64 +++++++++++++++++++++++++++++++++++---- test/iterable.test.ts | 28 ++++++++++++++++++ 3 files changed, 144 insertions(+), 17 deletions(-) diff --git a/src/arrays.ts b/src/arrays.ts index edab2eb1..00bcf05f 100644 --- a/src/arrays.ts +++ b/src/arrays.ts @@ -163,19 +163,66 @@ export function groupBy(a: any, b?: any): any { return partial ? exec : exec(a) } -export function init(count: number): (initializer: (index: number) => T) => T[] -export function init(initializer: (index: number) => T, count: number): T[] -export function init(a: any, b?: any): any { - const partial = typeof a === 'number' - const count: number = partial ? a : b - function exec(initializer: (index: number) => T): T[] { - const target = [] - for (let index = 0; index < count; index++) { - target.push(initializer(index)) +export interface InitRange { + from: number + to: number + increment?: number +} + +export interface InitCount { + start?: number + count: number + increment?: number +} + +export function init(options: number | InitRange | InitCount): number[] +export function init( + options: number | InitRange | InitCount, + initializer: (index: number) => T +): T[] +export function init( + options: number | InitRange | InitCount, + initializer?: (index: number) => T +): any[] { + function normaliseOptions() { + if (typeof options === 'number') { + return { + start: 0, + count: options, + increment: 1 + } + } + if ('from' in options) { + const sign = options.to < options.from ? -1 : 1 + if ( + options.increment !== undefined && + (options.increment === 0 || options.increment / sign < 0) + ) { + throw new Error('Requested array is of infinite size.') + } + const increment = options.increment ? options.increment : sign + return { + start: options.from, + count: (options.to - options.from) / increment + 1, + increment: increment + } + } + const start = options.start === undefined ? 0 : options.start + return { + start, + count: options.count, + increment: options.increment === undefined ? 1 : options.increment } - return target } - return partial ? exec : exec(a) + const { start, count, increment } = normaliseOptions() + const map = initializer === undefined ? (x: number) => x : initializer + const target = [] + let current = start + for (let index = 0; index < count; index++) { + target.push(map(current)) + current += increment + } + return target } export function length(source: T[]): number { diff --git a/test/arrays.test.ts b/test/arrays.test.ts index fc9ab1c3..15e12a05 100644 --- a/test/arrays.test.ts +++ b/test/arrays.test.ts @@ -184,14 +184,66 @@ describe('groupBy', () => { }) describe('init', () => { - test('piped', () => { - expect(pipe(Arrays.init(3)(i => i + 1)).result).toEqual([1, 2, 3]) - }) test('empty', () => { - expect(pipe(Arrays.init(0)(i => i)).result).toEqual([]) + expect(Arrays.init(0)).toEqual([]) }) - test('invoke', () => { - expect(Arrays.init(i => i + 1, 3)).toEqual([1, 2, 3]) + test('just count', () => { + expect(Arrays.init(5)).toEqual([0, 1, 2, 3, 4]) + }) + test('with mapping', () => { + expect(Arrays.init(5, x => x * x)).toEqual([0, 1, 4, 9, 16]) + }) + test('from-to', () => { + expect(Arrays.init({ from: 1, to: 3 })).toEqual([1, 2, 3]) + }) + test('from-to-same', () => { + expect(Arrays.init({ from: 1, to: 1 })).toEqual([1]) + }) + test('from-to fractional-increment', () => { + expect(Arrays.init({ from: 1, to: 2, increment: 0.5 })).toEqual([1, 1.5, 2]) + }) + test('from positive to negative', () => { + expect(Arrays.init({ from: 1, to: -1 })).toEqual([1, 0, -1]) + }) + test('from negative to positive', () => { + expect(Arrays.init({ from: -1, to: 1 })).toEqual([-1, 0, 1]) + }) + test('from-to zero increment fails', () => { + expect(() => Arrays.init({ from: 1, to: 2, increment: 0 })).toThrow( + 'Requested array is of infinite size.' + ) + }) + test('from-to negative fails', () => { + expect(() => Arrays.init({ from: 1, to: 2, increment: -0.1 })).toThrow( + 'Requested array is of infinite size.' + ) + }) + test('from-to negative crossing zero fails', () => { + expect(() => Arrays.init({ from: -1, to: 1, increment: -1 })).toThrow( + 'Requested array is of infinite size.' + ) + }) + test('from-to reversed fails', () => { + expect(() => Arrays.init({ from: 2, to: 1, increment: 1 })).toThrow( + 'Requested array is of infinite size.' + ) + }) + test('from-to reversed crossing zero fails', () => { + expect(() => Arrays.init({ from: 1, to: -1, increment: 0.1 })).toThrow( + 'Requested array is of infinite size.' + ) + }) + test('start-count', () => { + expect(Arrays.init({ start: 3, count: 5 })).toEqual([3, 4, 5, 6, 7]) + }) + test('count prop', () => { + expect(Arrays.init({ count: 5 })).toEqual([0, 1, 2, 3, 4]) + }) + test('start-count', () => { + expect(Arrays.init({ start: 3, count: 5 })).toEqual([3, 4, 5, 6, 7]) + }) + test('count-increment', () => { + expect(Arrays.init({ count: 5, increment: 3 })).toEqual([0, 3, 6, 9, 12]) }) }) diff --git a/test/iterable.test.ts b/test/iterable.test.ts index 1f72d974..ed524113 100644 --- a/test/iterable.test.ts +++ b/test/iterable.test.ts @@ -375,6 +375,11 @@ describe('init', () => { 1 ]) }) + test('from positive to negative with fractional increment', () => { + expect( + pipe(Iterables.init({ from: 1, to: -1, increment: -0.5 })).then(Iterables.toArray).result + ).toEqual([1, 0.5, 0, -0.5, -1]) + }) test('from-to zero increment fails', () => { expect( () => pipe(Iterables.init({ from: 1, to: 2, increment: 0 })).then(Iterables.toArray).result @@ -400,6 +405,15 @@ describe('init', () => { () => pipe(Iterables.init({ from: 1, to: -1, increment: 0.1 })).then(Iterables.toArray).result ).toThrow('Iterable will never complete.\nUse initInfinite if this is desired behaviour') }) + test('count prop', () => { + expect(Iterables.toArray(Iterables.init({ count: 5 }))).toEqual([0, 1, 2, 3, 4]) + }) + test('start-count', () => { + expect(Iterables.toArray(Iterables.init({ start: 3, count: 5 }))).toEqual([3, 4, 5, 6, 7]) + }) + test('count-increment', () => { + expect(Iterables.toArray(Iterables.init({ count: 5, increment: 3 }))).toEqual([0, 3, 6, 9, 12]) + }) }) describe('initInfinite', () => { @@ -461,6 +475,20 @@ describe('take', () => { .then(Iterables.toArray).result ).toEqual([0, 0, 0]) }) + test('invoke', () => { + expect( + Iterables.toArray( + Iterables.take( + (function*() { + while (true) { + yield 0 + } + })(), + 3 + ) + ) + ).toEqual([0, 0, 0]) + }) }) describe('length', () => { From 8b766911a17ce8d21df80a8563afeda2f21cefe0 Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Tue, 17 Jul 2018 10:12:55 +0100 Subject: [PATCH 09/10] feat(Sets): Add sets module Create initial functions for the sets module --- src/collection-fns.ts | 2 + src/sets.ts | 116 ++++++++++++++++++++++++++++++++++ test/sets.test.ts | 140 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 src/sets.ts create mode 100644 test/sets.test.ts diff --git a/src/collection-fns.ts b/src/collection-fns.ts index fc4d40e6..d564ae4d 100644 --- a/src/collection-fns.ts +++ b/src/collection-fns.ts @@ -1,8 +1,10 @@ import * as iterables from './iterables' import * as arrays from './arrays' +import * as sets from './sets' import * as maps from './maps' export * from './pipes' export const Iterables = iterables export const Arrays = arrays +export const Sets = sets export const Maps = maps diff --git a/src/sets.ts b/src/sets.ts new file mode 100644 index 00000000..725599bc --- /dev/null +++ b/src/sets.ts @@ -0,0 +1,116 @@ +import { Iterables } from './collection-fns' + +export function ofIterable(source: Iterable): Set { + return new Set(source) +} + +export function asIterable(source: Set): Iterable { + return source +} + +export function map(mapping: (item: T) => U): (source: Set) => Set +export function map(source: Set, mapping: (item: T) => U): Set +export function map(a: any, b?: any): any { + const partial = typeof a === 'function' + const mapping: (item: T) => U = partial ? a : b + function exec(source: Set) { + return new Set(Iterables.map(source.values(), mapping)) + } + return partial ? exec : exec(a) +} + +export function filter(predicate: (item: T) => boolean): (source: Set) => Set +export function filter(source: Set, predicate: (item: T) => boolean): Set +export function filter(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: Set) { + return new Set(Iterables.filter(source.values(), predicate)) + } + return partial ? exec : exec(a) +} + +export function choose(chooser: (item: T) => U | undefined): (source: Set) => Set +export function choose(source: Set, chooser: (item: T) => U | undefined): Set +export function choose(a: any, b?: any): any { + const partial = typeof a === 'function' + const chooser: (item: T) => U | undefined = partial ? a : b + function exec(source: Set) { + return new Set(Iterables.choose(source.values(), chooser)) + } + return partial ? exec : exec(a) +} + +export function collect(mapping: (item: T) => Iterable): (source: Set) => Set +export function collect(source: Set, mapping: (item: T) => Iterable): Set +export function collect(a: any, b?: any): any { + const partial = typeof a === 'function' + const mapping: (item: T) => Iterable = partial ? a : b + function exec(source: Set) { + return new Set(Iterables.collect(source, mapping)) + } + return partial ? exec : exec(a) +} + +export function append(second: Set): (first: Set) => Set +export function append(first: Set, second: Set): Set +export function append(a: any, b?: any): any { + const partial = b === undefined + const second: Set = partial ? a : b + function exec(first: Set): Set { + return new Set(Iterables.append(first, second)) + } + return partial ? exec : exec(a) +} + +export function concat(sources: Iterable>): Set { + const target = new Set() + for (const source of sources) { + for (const item of source) { + target.add(item) + } + } + return target +} + +export function exists(predicate: (item: T) => boolean): (source: Set) => boolean +export function exists(source: Set, predicate: (item: T) => boolean): boolean +export function exists(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: Set): boolean { + return Iterables.exists(source, predicate) + } + return partial ? exec : exec(a) +} + +export function contains(item: T): (source: Set) => boolean +export function contains(source: Set, item: T): boolean +export function contains(a: any, b?: any): any { + const partial = b === undefined + const item: T = partial ? a : b + function exec(source: Set): boolean { + return source.has(item) + } + return partial ? exec : exec(a) +} + +export function find(predicate: (item: T) => boolean): (source: Set) => T | undefined +export function find(source: Set, predicate: (item: T) => boolean): T | undefined +export function find(a: any, b?: any): any { + const partial = typeof a === 'function' + const predicate: (item: T) => boolean = partial ? a : b + function exec(source: Set): T | undefined { + for (const item of source) { + if (predicate(item)) { + return item + } + } + return undefined + } + return partial ? exec : exec(a) +} + +export function count(source: Set): number { + return source.size +} diff --git a/test/sets.test.ts b/test/sets.test.ts new file mode 100644 index 00000000..000a70f5 --- /dev/null +++ b/test/sets.test.ts @@ -0,0 +1,140 @@ +import { pipe, Sets, Iterables } from '../src/collection-fns' + +describe('ofIterable', () => { + it('constructs an array', () => { + const iterator = function*() { + yield 1 + yield 2 + yield 2 + } + expect(Sets.ofIterable(iterator())).toEqual(new Set([1, 2])) + }) +}) + +test('asIterable', () => { + expect(Iterables.length(Sets.asIterable(new Set(['a'])))).toEqual(1) +}) + +describe('map', () => { + test('empty', () => { + expect(Sets.map(x => '')(new Set())).toEqual(new Set()) + }) + test('piped', () => { + expect(pipe(new Set([1, 2, 3])).then(Sets.map(x => x % 2)).result).toEqual(new Set([0, 1])) + }) + test('invoke', () => { + expect(Sets.map(new Set([1, 2, 3]), x => x % 2)).toEqual(new Set([0, 1])) + }) +}) + +describe('filter', () => { + test('empty', () => { + expect(Sets.filter(new Set(), x => true)).toEqual(new Set([])) + }) + test('piped', () => { + expect(pipe(new Set([1, 2, 3, 4])).then(Sets.filter(x => x % 2 === 0)).result).toEqual( + new Set([2, 4]) + ) + }) + test('invoke', () => { + expect(Sets.filter(new Set([1, 2, 3, 4]), x => x % 2 === 0)).toEqual(new Set([2, 4])) + }) +}) + +describe('choose', () => { + test('piped', () => { + expect( + pipe(new Set([1, 2, 3])).then(Sets.choose(x => (x % 2 === 1 ? x * 2 : undefined))).result + ).toEqual(new Set([2, 6])) + }) + test('invoke', () => { + expect(Sets.choose(new Set([1, 2, 3]), x => (x % 2 === 1 ? x * 2 : undefined))).toEqual( + new Set([2, 6]) + ) + }) +}) + +describe('collect', () => { + test('piped', () => { + expect( + pipe(new Set([1, 2])).then( + Sets.collect(function(x) { + return [x, x + 1] + }) + ).result + ).toEqual(new Set([1, 2, 3])) + }) + test('invoke', () => { + expect( + Sets.collect(new Set([1, 2]), function(x) { + return [x, x + 1] + }) + ).toEqual(new Set([1, 2, 3])) + }) +}) + +describe('append', () => { + test('piped', () => { + expect(pipe(new Set([1, 2])).then(Sets.append(new Set([2, 3]))).result).toEqual( + new Set([1, 2, 3]) + ) + }) + test('invoke', () => { + expect(Sets.append(new Set([1, 2]), new Set([2, 3]))).toEqual(new Set([1, 2, 3])) + }) +}) + +test('concat', () => { + expect(Sets.concat([new Set([1, 2]), new Set([2, 3, 4]), new Set([])])).toEqual( + new Set([1, 2, 3, 4]) + ) +}) + +describe('exists', () => { + it('matches existance', () => { + expect(pipe(new Set([1, 2])).then(Sets.exists(x => x % 2 === 1)).result).toEqual(true) + }) + it('matches non-existance', () => { + expect(pipe(new Set([1, 2])).then(Sets.exists(x => x % 3 === 0)).result).toEqual(false) + }) + test('invoke', () => { + expect(Sets.exists(new Set([1, 2]), x => x % 2 === 1)).toEqual(true) + }) +}) + +describe('contains', () => { + test('piped match', () => { + expect(pipe(new Set(['amy', 'bob'])).then(Sets.contains('bob')).result).toEqual(true) + }) + test('no match', () => { + expect(pipe(new Set(['amy', 'bob'])).then(Sets.contains('cat')).result).toEqual(false) + }) + test('invoke', () => { + expect(Sets.contains(new Set(['amy', 'bob']), 'amy')).toEqual(true) + }) +}) + +describe('find', () => { + test('piped match', () => { + expect(pipe(new Set(['amy', 'bob'])).then(Sets.find(x => x.startsWith('b'))).result).toEqual( + 'bob' + ) + }) + it('returns undefined when not found', () => { + expect( + pipe(new Set(['amy', 'bob'])).then(Sets.find(x => x.startsWith('c'))).result + ).toBeUndefined() + }) + test('invoke', () => { + expect(Sets.find(new Set(['amy', 'bob']), x => x.startsWith('a'))).toEqual('amy') + }) +}) + +describe('count', () => { + test('zero length', () => { + expect(Sets.count(new Set())).toEqual(0) + }) + test('non-zero length', () => { + expect(Sets.count(new Set([1, 2, 3, 4, 5]))).toEqual(5) + }) +}) From 7a07703603a78c2cb2eb91b33b2b8fe50ae97731 Mon Sep 17 00:00:00 2001 From: Daniel Bradley Date: Tue, 17 Jul 2018 10:27:50 +0100 Subject: [PATCH 10/10] feat(Conversions): Add more complete collection conversion functions --- src/arrays.ts | 2 +- src/iterables.ts | 4 ++-- src/maps.ts | 12 ++++++++++-- src/sets.ts | 8 ++++++++ test/maps.test.ts | 12 ++++++++++-- test/sets.test.ts | 24 +++++++++++++++--------- 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/arrays.ts b/src/arrays.ts index 00bcf05f..9b8b4921 100644 --- a/src/arrays.ts +++ b/src/arrays.ts @@ -62,7 +62,7 @@ export function collect(a: any, b?: any): any { export function append(second: T[]): (first: T[]) => T[] export function append(first: T[], second: T[]): T[] -export function append(a: any, b?: any): any { +export function append(a: any, b?: any): any { const partial = b === undefined const second: T[] = partial ? a : b function exec(first: T[]): T[] { diff --git a/src/iterables.ts b/src/iterables.ts index d1049795..42377257 100644 --- a/src/iterables.ts +++ b/src/iterables.ts @@ -17,7 +17,7 @@ export function map(a: any, b?: any): any { export function filter(predicate: (item: T) => boolean): (source: Iterable) => Iterable export function filter(source: Iterable, predicate: (item: T) => boolean): Iterable -export function filter(a: any, b?: any): any { +export function filter(a: any, b?: any): any { const partial = typeof a === 'function' const predicate: (item: T) => boolean = partial ? a : b function* exec(source: Iterable) { @@ -68,7 +68,7 @@ export function collect(a: any, b?: any): any { export function append(second: Iterable): (first: Iterable) => Iterable export function append(first: Iterable, second: Iterable): Iterable -export function append(a: any, b?: any): any { +export function append(a: any, b?: any): any { const partial = b === undefined const second: Iterable = partial ? a : b function* exec(first: Iterable): Iterable { diff --git a/src/maps.ts b/src/maps.ts index e247baa5..f6595c17 100644 --- a/src/maps.ts +++ b/src/maps.ts @@ -4,8 +4,16 @@ export function ofIterable(source: Iterable<[Key, T]>): Map { return new Map(source) } -export function toIterable(source: Map): Iterable<[Key, T]> { - return source.entries() +export function ofArray(source: [Key, T][]): Map { + return new Map(source) +} + +export function ofSet(source: Set): Map { + return new Map(source.entries()) +} + +export function asIterable(source: Map): Iterable<[Key, T]> { + return source } export function map(source: Map, mapping: (key: Key, value: T) => U): Map diff --git a/src/sets.ts b/src/sets.ts index 725599bc..1c8548fd 100644 --- a/src/sets.ts +++ b/src/sets.ts @@ -4,10 +4,18 @@ export function ofIterable(source: Iterable): Set { return new Set(source) } +export function ofArray(source: T[]): Set { + return new Set(source) +} + export function asIterable(source: Set): Iterable { return source } +export function toArray(source: Set): T[] { + return Array.from(source) +} + export function map(mapping: (item: T) => U): (source: Set) => Set export function map(source: Set, mapping: (item: T) => U): Set export function map(a: any, b?: any): any { diff --git a/test/maps.test.ts b/test/maps.test.ts index 9f6b61b8..25f4cbe5 100644 --- a/test/maps.test.ts +++ b/test/maps.test.ts @@ -11,8 +11,16 @@ test('ofIterable', () => { ).toEqual(new Map([['a', 1], ['b', 2]])) }) -test('toIterable', () => { - expect(Iterables.toArray(Maps.toIterable(new Map([['a', 1], ['b', 2]])))).toEqual([ +test('ofArray', () => { + expect(Maps.ofArray([['a', 1], ['b', 2]])).toEqual(new Map([['a', 1], ['b', 2]])) +}) + +test('ofSet', () => { + expect(Maps.ofSet(new Set(['a', 'b']))).toEqual(new Map([['a', 'a'], ['b', 'b']])) +}) + +test('asIterable', () => { + expect(Iterables.toArray(Maps.asIterable(new Map([['a', 1], ['b', 2]])))).toEqual([ ['a', 1], ['b', 2] ]) diff --git a/test/sets.test.ts b/test/sets.test.ts index 000a70f5..39f228be 100644 --- a/test/sets.test.ts +++ b/test/sets.test.ts @@ -1,20 +1,26 @@ import { pipe, Sets, Iterables } from '../src/collection-fns' -describe('ofIterable', () => { - it('constructs an array', () => { - const iterator = function*() { - yield 1 - yield 2 - yield 2 - } - expect(Sets.ofIterable(iterator())).toEqual(new Set([1, 2])) - }) +test('ofIterable', () => { + const iterator = function*() { + yield 1 + yield 2 + yield 2 + } + expect(Sets.ofIterable(iterator())).toEqual(new Set([1, 2])) +}) + +test('ofArray', () => { + expect(Sets.ofArray([1, 2, 2, 3])).toEqual(new Set([1, 2, 3])) }) test('asIterable', () => { expect(Iterables.length(Sets.asIterable(new Set(['a'])))).toEqual(1) }) +test('toArray', () => { + expect(Sets.toArray(new Set(['a']))).toEqual(['a']) +}) + describe('map', () => { test('empty', () => { expect(Sets.map(x => '')(new Set())).toEqual(new Set())