From 86f9e517d3da2fb19bc256531291d8d08b6e00c3 Mon Sep 17 00:00:00 2001 From: jtenner Date: Mon, 25 Mar 2019 15:52:10 -0400 Subject: [PATCH] feat(without): add without support to fromfrom --- package-lock.json | 41 ++++++++++++++++++++++++++++----------- src/Sequence.ts | 30 ++++++++++++++++++++++++++++ src/transforms/without.ts | 39 +++++++++++++++++++++++++++++++++++++ src/types.ts | 2 ++ test/fromfrom.test.ts | 31 +++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 src/transforms/without.ts diff --git a/package-lock.json b/package-lock.json index 58e2494..8cbe804 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7053,7 +7053,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7074,12 +7075,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7094,17 +7097,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7221,7 +7227,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7233,6 +7240,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7247,6 +7255,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7254,12 +7263,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7278,6 +7289,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7358,7 +7370,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7370,6 +7383,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7455,7 +7469,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7491,6 +7506,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7510,6 +7526,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7553,12 +7570,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/Sequence.ts b/src/Sequence.ts index 0e553ba..d6f81bd 100644 --- a/src/Sequence.ts +++ b/src/Sequence.ts @@ -8,6 +8,7 @@ import { ReduceCallbackFn, NumberKeyedObject, StringKeyedObject, + ComparePredicate, } from "./types"; import { createConcatIterable } from "./transforms/concat"; import { createDistinctIterable } from "./transforms/distinct"; @@ -21,6 +22,7 @@ import { createTakeIterable } from "./transforms/take"; import { createSortByIterable } from "./transforms/sortBy"; import { createTakeWhileIterable } from "./transforms/takeWhile"; import { createSkipWhileIterable } from "./transforms/skipWhile"; +import { createWithoutIterable } from "./transforms/without"; const identityPredicateFn = (x: any): boolean => x; @@ -526,6 +528,34 @@ export class Sequence implements Iterable { return new Sequence(createTakeWhileIterable(this._iterable, predicate)); } + /** + * Returns elements from a sequence as long as they don't exist in the specified iterable items. + * + * @param items The provided set of items that should not be in the returned Sequence. + * @param predicate The optional predicate that determines if two TItem items are equal. + * + * @example + * ```ts + * // returns [2, 4, 6] + * from([1, 2, 3, 4, 5, 6]) + * .without([1, 3, 5]) + * .toArray(); + * + * // returns [{ id: 1 }, { id: 3 }] + * from([{ id: 1 }, { id: 2 }, { id: 3 }]) + * .without([{ id: 2 }], (a, b) => a.id === b.id) + * .toArray(); + * ``` + */ + without( + items: Iterable, + predicate?: ComparePredicate + ): Sequence { + return new Sequence( + createWithoutIterable(this._iterable, items, predicate) + ); + } + /** * Converts the sequence to an array * diff --git a/src/transforms/without.ts b/src/transforms/without.ts new file mode 100644 index 0000000..b9b9c07 --- /dev/null +++ b/src/transforms/without.ts @@ -0,0 +1,39 @@ +import { ComparePredicate } from "../types"; +import { createFilterIterable } from "./filter"; +import { IterableCreatorIterable } from "../IterableCreatorIterable"; + +export const createWithoutIterable = ( + iterator: Iterable, + withoutItems: Iterable, + comparePredicate?: ComparePredicate +): Iterable => { + if (!comparePredicate) { + const withoutSet = new Set(withoutItems); + // fast path, create a filter iterable with the `Set.prototype.has` function call + return createFilterIterable(iterator, item => !withoutSet.has(item)); + } else { + // Must compare each item for equality for each item + return new IterableCreatorIterable(function* filter(): IterableIterator< + TItem + > { + // cache already found results + const cache = new Set(); + + outer: for (const item of iterator) { + // fast path, this item was already found, don't loop + if (cache.has(item)) continue; + // slow path, loop over each item in the set, determine if it matches the ComparePredicate + for (const withoutItem of withoutItems) { + // if the item is found, add it to the cache and skip the item + if (comparePredicate(item, withoutItem)) { + cache.add(item); + continue outer; + } + } + + // we can safely yield the item + yield item; + } + }); + } +}; diff --git a/src/types.ts b/src/types.ts index 552caf2..f10272e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,8 @@ export type ComparerFn = (a: TItem, b: TItem) => number; export type IteratorCreatorFn = () => Iterator; +export type ComparePredicate = (a: TItem, b: TItem) => boolean; + export interface Grouping { key: TKey; items: TElement[]; diff --git a/test/fromfrom.test.ts b/test/fromfrom.test.ts index 8380cb6..d74b28e 100644 --- a/test/fromfrom.test.ts +++ b/test/fromfrom.test.ts @@ -933,6 +933,37 @@ describe("fromfrom", () => { }); }); + describe("without", () => { + const numbers = [1, 2, 3, 4, 5, 6]; + it("should return a sequence without values", () => { + const sequence = from(numbers).without([1, 3, 5]); + + expect(Array.from(sequence)).toStrictEqual([2, 4, 6]); + }); + + it("should use a predicate function", () => { + const items = [ + { id: 0, name: "John", age: 20 }, + { id: 1, name: "Tony", age: 30 }, + { id: 2, name: "Mark", age: 30 }, + { id: 3, name: "Jane", age: 20 }, + { id: 4, name: "Lisa", age: 30 }, + ]; + const without = [ + { id: 0, name: "", age: 0 }, + { id: 1, name: "", age: 0 }, + { id: 2, name: "", age: 0 }, + ]; + + const sequence = from(items).without(without, (a, b) => a.id === b.id); + + expect(Array.from(sequence)).toStrictEqual([ + { id: 3, name: "Jane", age: 20 }, + { id: 4, name: "Lisa", age: 30 }, + ]); + }); + }); + describe("toArray", () => { it("returns an array", () => { expect(from([1, 2]).toArray()).toEqual([1, 2]);