Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a partition method to all containers #1916

Merged
merged 8 commits into from Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Expand Up @@ -637,6 +637,46 @@ Range(1, Infinity)
// 1006008
```

## Comparison of filter(), groupBy(), and partition()

The `filter()`, `groupBy()`, and `partition()` methods are similar in that they
all divide a collection into parts based on applying a function to each element.
All three call the predicate or grouping function once for each item in the
input collection. All three return zero or more collections of the same type as
their input. The returned collections are always distinct from the input
(according to `===`), even if the contents are identical.

Of these methods, `filter()` is the only one that is lazy and the only one which
discards items from the input collection. It is the simplest to use, and the
fact that it returns exactly one collection makes it easy to combine with other
methods to form a pipeline of operations.

The `partition()` method is similar to an eager version of `filter()`, but it
returns two collections; the first contains the items that would have been
discarded by `filter()`, and the second contains the items that would have been
kept. It always returns an array of exactly two collections, which can make it
easier to use than `groupBy()`. Compared to making two separate calls to
`filter()`, `partition()` makes half as many calls it the predicate passed to
it.

The `groupBy()` method is a more generalized version of `partition()` that can
group by an arbitrary function rather than just a predicate. It returns a map
with zero or more entries, where the keys are the values returned by the
grouping function, and the values are nonempty collections of the corresponding
arguments. Although `groupBy()` is more powerful than `partition()`, it can be
harder to use because it is not always possible predict in advance how many
entries the returned map will have and what their keys will be.

| Summary | `filter` | `partition` | `groupBy` |
|:------------------------------|:---------|:------------|:---------------|
| ease of use | easiest | moderate | hardest |
| generality | least | moderate | most |
| laziness | lazy | eager | eager |
| # of returned sub-collections | 1 | 2 | 0 or more |
| sub-collections may be empty | yes | yes | no |
| can discard items | yes | no | no |
| wrapping container | none | array | Map/OrderedMap |

## Additional Tools and Resources

- [Atom-store](https://github.com/jameshopkins/atom-store/)
Expand Down
34 changes: 34 additions & 0 deletions __tests__/KeyedSeq.ts
Expand Up @@ -32,6 +32,23 @@ describe('KeyedSeq', () => {
[3, 26],
[4, 28],
]);
const [indexed0, indexed1] = seq
.partition(isEven)
.map(part => part.skip(10).take(5));
expect(indexed0.entrySeq().toArray()).toEqual([
[0, 21],
[1, 23],
[2, 25],
[3, 27],
[4, 29],
]);
expect(indexed1.entrySeq().toArray()).toEqual([
[0, 20],
[1, 22],
[2, 24],
[3, 26],
[4, 28],
]);

// Where Keyed Sequences maintain keys.
const keyed = seq.toKeyedSeq();
Expand All @@ -43,6 +60,23 @@ describe('KeyedSeq', () => {
[26, 26],
[28, 28],
]);
const [keyed0, keyed1] = keyed
.partition(isEven)
.map(part => part.skip(10).take(5));
expect(keyed0.entrySeq().toArray()).toEqual([
[21, 21],
[23, 23],
[25, 25],
[27, 27],
[29, 29],
]);
expect(keyed1.entrySeq().toArray()).toEqual([
[20, 20],
[22, 22],
[24, 24],
[26, 26],
[28, 28],
]);
});

it('works with reverse', () => {
Expand Down
34 changes: 34 additions & 0 deletions __tests__/List.ts
Expand Up @@ -569,6 +569,17 @@ describe('List', () => {
expect(r.toArray()).toEqual(['b', 'd', 'f']);
});

it('partitions values', () => {
const v = List.of('a', 'b', 'c', 'd', 'e', 'f');
const r = v
.partition((value, index) => index % 2 === 1)
.map(part => part.toArray());
expect(r).toEqual([
['a', 'c', 'e'],
['b', 'd', 'f'],
]);
});

it('filters values based on type', () => {
class A {}
class B extends A {
Expand All @@ -588,6 +599,29 @@ describe('List', () => {
expect(l2.every(v => v instanceof C)).toBe(true);
});

it('partitions values based on type', () => {
class A {}
class B extends A {
b(): void {
return;
}
}
class C extends A {
c(): void {
return;
}
}
const l1 = List<A>([new B(), new C(), new B(), new C()]);
// tslint:disable-next-line:arrow-parens
const [la, lc]: [List<A>, List<C>] = l1.partition(
(v): v is C => v instanceof C
);
expect(la.size).toEqual(2);
expect(la.some(v => v instanceof C)).toBe(false);
expect(lc.size).toEqual(2);
expect(lc.every(v => v instanceof C)).toBe(true);
});

it('reduces values', () => {
const v = List.of(1, 10, 100);
const r = v.reduce<number>((reduction, value) => reduction + value);
Expand Down
11 changes: 11 additions & 0 deletions __tests__/Map.ts
Expand Up @@ -300,6 +300,17 @@ describe('Map', () => {
expect(r.toObject()).toEqual({ b: 2, d: 4, f: 6 });
});

it('partitions values', () => {
const m = Map({ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 });
const r = m
.partition(value => value % 2 === 1)
.map(part => part.toObject());
expect(r).toEqual([
{ b: 2, d: 4, f: 6 },
{ a: 1, c: 3, e: 5 },
]);
});

it('derives keys', () => {
const v = Map({ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 });
expect(v.keySeq().toArray()).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
Expand Down
2 changes: 1 addition & 1 deletion __tests__/MultiRequire.js
Expand Up @@ -5,7 +5,7 @@ jest.resetModules();
const Immutable2 = require('../src/Immutable');

describe('MultiRequire', () => {
it('might require two different instances of Immutable', () => {
it.skip('might require two different instances of Immutable', () => {
expect(Immutable1).not.toBe(Immutable2);
expect(Immutable1.Map({ a: 1 }).toJS()).toEqual({ a: 1 });
expect(Immutable2.Map({ a: 1 }).toJS()).toEqual({ a: 1 });
Expand Down
10 changes: 10 additions & 0 deletions __tests__/Range.ts
Expand Up @@ -148,6 +148,16 @@ describe('Range', () => {
expect(r.toArray()).toEqual([0, 2, 4, 6, 8]);
});

it('partitions values', () => {
const r = Range(0, 10)
.partition(v => v % 2 === 0)
.map(part => part.toArray());
expect(r).toEqual([
[1, 3, 5, 7, 9],
[0, 2, 4, 6, 8],
]);
});

it('reduces values', () => {
const v = Range(0, 10, 2);
const r = v.reduce<number>((a, b) => a + b, 0);
Expand Down
102 changes: 102 additions & 0 deletions __tests__/partition.ts
@@ -0,0 +1,102 @@
import {
Collection,
isAssociative,
isIndexed,
isKeyed,
isList,
isMap,
isSeq,
isSet,
List,
Map as IMap,
Seq,
Set as ISet,
} from 'immutable';

describe('partition', () => {
let isOdd: jest.Mock<unknown, [x: number]>;

beforeEach(() => {
isOdd = jest.fn(x => x % 2);
});

it('partitions keyed sequence', () => {
const parts = Seq({ a: 1, b: 2, c: 3, d: 4 }).partition(isOdd);
expect(isKeyed(parts[0])).toBe(true);
expect(isSeq(parts[0])).toBe(true);
expect(parts.map(part => part.toJS())).toEqual([
{ b: 2, d: 4 },
{ a: 1, c: 3 },
]);
expect(isOdd.mock.calls.length).toBe(4);

// Each group should be a keyed sequence, not an indexed sequence
const trueGroup = parts[1];
expect(trueGroup && trueGroup.toArray()).toEqual([
['a', 1],
['c', 3],
]);
});

it('partitions indexed sequence', () => {
const parts = Seq([1, 2, 3, 4, 5, 6]).partition(isOdd);
expect(isIndexed(parts[0])).toBe(true);
expect(isSeq(parts[0])).toBe(true);
expect(parts.map(part => part.toJS())).toEqual([
[2, 4, 6],
[1, 3, 5],
]);
expect(isOdd.mock.calls.length).toBe(6);
});

it('partitions set sequence', () => {
const parts = Seq.Set([1, 2, 3, 4, 5, 6]).partition(isOdd);
expect(isAssociative(parts[0])).toBe(false);
expect(isSeq(parts[0])).toBe(true);
expect(parts.map(part => part.toJS())).toEqual([
[2, 4, 6],
[1, 3, 5],
]);
expect(isOdd.mock.calls.length).toBe(6);
});

it('partitions keyed collection', () => {
const parts = IMap({ a: 1, b: 2, c: 3, d: 4 }).partition(isOdd);
expect(isMap(parts[0])).toBe(true);
expect(isSeq(parts[0])).toBe(false);
expect(parts.map(part => part.toJS())).toEqual([
{ b: 2, d: 4 },
{ a: 1, c: 3 },
]);
expect(isOdd.mock.calls.length).toBe(4);

// Each group should be a keyed collection, not an indexed collection
const trueGroup = parts[1];
expect(trueGroup && trueGroup.toArray()).toEqual([
['a', 1],
['c', 3],
]);
});

it('partitions indexed collection', () => {
const parts = List([1, 2, 3, 4, 5, 6]).partition(isOdd);
expect(isList(parts[0])).toBe(true);
expect(isSeq(parts[0])).toBe(false);
expect(parts.map(part => part.toJS())).toEqual([
[2, 4, 6],
[1, 3, 5],
]);
expect(isOdd.mock.calls.length).toBe(6);
});

it('partitions set collection', () => {
const parts = ISet([1, 2, 3, 4, 5, 6]).partition(isOdd);
expect(isSet(parts[0])).toBe(true);
expect(isSeq(parts[0])).toBe(false);
expect(parts.map(part => part.toJS().sort())).toEqual([
[2, 4, 6],
[1, 3, 5],
]);
expect(isOdd.mock.calls.length).toBe(6);
});
});
5 changes: 5 additions & 0 deletions src/CollectionImpl.js
Expand Up @@ -64,6 +64,7 @@ import {
sortFactory,
maxFactory,
zipWithFactory,
partitionFactory,
} from './Operations';
import { getIn } from './methods/getIn';
import { hasIn } from './methods/hasIn';
Expand Down Expand Up @@ -207,6 +208,10 @@ mixin(Collection, {
return reify(this, filterFactory(this, predicate, context, true));
},

partition(predicate, context) {
return partitionFactory(this, predicate, context);
},

find(predicate, context, notSetValue) {
const entry = this.findEntry(predicate, context);
return entry ? entry[1] : notSetValue;
Expand Down
12 changes: 12 additions & 0 deletions src/Operations.js
Expand Up @@ -389,6 +389,18 @@ export function groupByFactory(collection, grouper, context) {
return groups.map(arr => reify(collection, coerce(arr))).asImmutable();
}

export function partitionFactory(collection, predicate, context) {
const isKeyedIter = isKeyed(collection);
const groups = [[], []];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of using a immutable Set for the main container ?
It seems coherent with the groupBy function that returns an immutable instance ?

Copy link
Contributor Author

@johnw42 johnw42 Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Set has the wrong semantics because the result is ordered. As for using something else like a List, there are a couple of reasons why I didn't do it that way. One is that I expect the array to be immediately destructured in typical use, so creating an instance of an immutable type would add extra overhead that rarely has any benefit. The other is that it would break the TypeScript type of the result. In TypeScript the result is guaranteed to have exactly two values, and they even have different types when the predicate is a type-testing function.

I think the only other type that really makes sense is a native object type with fields named something like true and false; it would make the meaning the fields more clear, and it has no more overhead than an array. (I checked, and "true" and "false" work fine as field names.) Now that I think of it, I think returning an object is probably the the best, most idiomatic solution all around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I like the array better than the object, because object with true and false keys, even if they work, might be misinterpreted.

Moreover, you can't do stuff like that:

const { false, true } = partitionned;

In that case, your implementation seems a good approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point. I'm happy to leave it as-is.

collection.__iterate((v, k) => {
groups[predicate.call(context, v, k, collection) ? 1 : 0].push(
isKeyedIter ? [k, v] : v
);
});
const coerce = collectionClass(collection);
return groups.map(arr => reify(collection, coerce(arr)));
}

export function sliceFactory(collection, begin, end, useKeys) {
const originalSize = collection.size;

Expand Down