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

Add permutations generator #172

Closed
wants to merge 1 commit into from
Closed
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
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,9 +532,31 @@ d3.pairs([1, 2, 3, 4], (a, b) => b - a); // returns [1, 1, 1];

If the specified iterable has fewer than two elements, returns the empty array.

<a name="permute" href="#permute">#</a> d3.<b>permute</b>(<i>source</i>, <i>keys</i>) · [Source](https://github.com/d3/d3-array/blob/master/src/permute.js), [Examples](https://observablehq.com/@d3/d3-permute)
<a name="permute" href="#permute">#</a> d3.<b>permute</b>(<i>source</i>[, <i>keys</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/permute.js), [Examples](https://observablehq.com/@d3/d3-permute)

Returns a permutation of the specified *source* object (or array) using the specified iterable of *keys*. The returned array contains the corresponding property of the source object for each key in *keys*, in order. For example:
Copy link

Choose a reason for hiding this comment

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

This is really good. No detail of the original documentation was lost here. Nice work!

Returns a generator of permutations or a single permutation of the specified *source* object (or array).
Copy link

@curran curran Sep 19, 2020

Choose a reason for hiding this comment

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

Suggested change
Returns a generator of permutations or a single permutation of the specified *source* object (or array).
Returns a single permutation (if *keys* is specified) or a generator of all possible permutations (if *keys* is omitted) of the specified *source* object (or array).

This language "Returns a generator of permutations or a single permutation of the specified source object (or array)." is ambiguous in terms of "a generator of" possibly being associated with "a single permutation". Meaning, one could interpret this sentence as saying that it returns a generator with a single permutation. To solve this, I suggest to reverse the order, so the generator concept is only introduced in association with the notion of "all possible permutations" (another level of detail I've suggested here). I also suggest that this first line of documentation make it clear the conditions with which either case will happen, so I've included those in parentheses.

It might make sense to also flip the more detailed content to match this ordering, so first

If keys is specified then...

then later

If keys is omitted then...

This would have the additional benefit of backwards compatibility at the documentation level. Meaning, if someone is used to seeing the old content, then let's give them the old content first, then the new content after that.


If *keys* is omitted then a [generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) is returned which, when iterated, generates all permutations of the input. For example:

```js
const permutations = permute(["a", "b", "c"]);
permutations.next().value; // returns ["a", "b", "c"]
permutations.next().value; // returns ["a", "c", "b"]
permutations.next().value; // returns ["c", "a", "b"]
permutations.next().value; // returns ["c", "b", "a"]
permutations.next().value; // returns ["b", "c", "a"]
permutations.next().value; // returns ["b", "a", "c"]
```

Generators are iterable and can be used in [`for…of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of):

```js
for (const permutation of permute(["a", "b", "c"])) {
// code looping over each permutation
}
```

If *keys* is specified then a single permutation is returned. The returned array contains the corresponding property of the source object for each key in *keys*, in order. For example:

```js
permute(["a", "b", "c"], [1, 2, 0]); // returns ["b", "c", "a"]
Expand Down
46 changes: 44 additions & 2 deletions src/permute.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
export default function(source, keys) {
return Array.from(keys, key => source[key]);
export default function (source, keys) {
if (keys === undefined) {
return permute(source);
} else {
return Array.from(keys, (key) => source[key]);
}
}

// Based on https://github.com/google/guava/blob/f4b3f611c4e49ecaded58dcb49262f55e56a3322/guava/src/com/google/common/collect/Collections2.java#L622-L683
Copy link

Choose a reason for hiding this comment

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

Maybe this link can be moved into the docs?

Copy link

Choose a reason for hiding this comment

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

Something like.

This implementation was inspired by the permute algorithm in Guava Collections.

// Apache-2.0 License, Copyright (C) 2008 The Guava Authors.
Copy link

Choose a reason for hiding this comment

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

Confusion on licensing here. Not sure how to address, but this comment is super confusing. Does it override the licence for the overall library? Is this code dual licensed with both the library license and this licence here in the comment? I suggest to remove this comment, and somehow come to terms with the fact that you are re-licensing this code/algorithm by submitting this PR.

The original license does not apply to a JavaScript port, I don't think.

export function* permute(source) {
if (source.length <= 1) {
yield source.slice();
} else {
source = source.slice();
const c = Array(source.length).fill(0);
const o = Array(source.length).fill(1);
let j = Infinity;
while (j > 0) {
yield source.slice();
j = source.length - 1;
let s = 0;
while (true) {
const q = c[j] + o[j];
if (q < 0) {
o[j] = -o[j];
j--;
} else if (q === j + 1) {
if (j === 0) {
break;
}
s++;
o[j] = -o[j];
j--;
} else {
const a = j - c[j] + s;
const b = j - q + s;
[source[a], source[b]] = [source[b], source[a]];
c[j] = q;
break;
}
}
}
}
}
107 changes: 107 additions & 0 deletions test/permute-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,104 @@
const tape = require("tape-await");
const d3 = require("../");

//#region Based on https://github.com/google/guava/blob/f4b3f611c4e49ecaded58dcb49262f55e56a3322/guava-tests/test/com/google/common/collect/Collections2Test.java

tape("permute(…) permutes zero values", (test) => {
Copy link

Choose a reason for hiding this comment

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

Very nice tests!

test.deepEqual(Array.from(d3.permute([])), [[]]);
});

tape("permute(…) permutes one value", (test) => {
test.deepEqual(Array.from(d3.permute([1])), [[1]]);
});

tape("permute(…) permutes two values", (test) => {
test.deepEqual(Array.from(d3.permute([1, 2])), [
[1, 2],
[2, 1],
]);
});

tape("permute(…) permutes three values", (test) => {
test.deepEqual(Array.from(d3.permute([1, 2, 3])), [
[1, 2, 3],
[1, 3, 2],
[3, 1, 2],

[3, 2, 1],
[2, 3, 1],
[2, 1, 3],
]);
});

tape("permute(…) permutes three values out of order", (test) => {
test.deepEqual(Array.from(d3.permute([3, 2, 1])), [
[3, 2, 1],
[3, 1, 2],
[1, 3, 2],

[1, 2, 3],
[2, 1, 3],
[2, 3, 1],
]);
});

tape("permute(…) permutes three repeated values", (test) => {
test.deepEqual(Array.from(d3.permute([1, 1, 2])), [
[1, 1, 2],
[1, 2, 1],
[2, 1, 1],

[2, 1, 1],
[1, 2, 1],
[1, 1, 2],
]);
});

tape("permute(…) permutes values", (test) => {
test.deepEqual(Array.from(d3.permute([1, 2, 3, 4])), [
[1, 2, 3, 4],
[1, 2, 4, 3],
[1, 4, 2, 3],
[4, 1, 2, 3],

[4, 1, 3, 2],
[1, 4, 3, 2],
[1, 3, 4, 2],
[1, 3, 2, 4],

[3, 1, 2, 4],
[3, 1, 4, 2],
[3, 4, 1, 2],
[4, 3, 1, 2],

[4, 3, 2, 1],
[3, 4, 2, 1],
[3, 2, 4, 1],
[3, 2, 1, 4],

[2, 3, 1, 4],
[2, 3, 4, 1],
[2, 4, 3, 1],
[4, 2, 3, 1],

[4, 2, 1, 3],
[2, 4, 1, 3],
[2, 1, 4, 3],
[2, 1, 3, 4],
]);
});

tape("permute(…) permutes count of permutations", (test) => {
test.equal(count(d3.permute([])), 1);
test.equal(count(d3.permute([1])), 1);
test.equal(count(d3.permute([1, 2])), 2);
test.equal(count(d3.permute([1, 2, 3])), 6);
test.equal(count(d3.permute([1, 2, 3, 4, 5, 6, 7])), 5040);
test.equal(count(d3.permute([1, 2, 3, 4, 5, 6, 7, 8])), 40320);
});

//#endregion

tape("permute(…) permutes according to the specified index", (test) => {
test.deepEqual(d3.permute([3, 4, 5], [2, 1, 0]), [5, 4, 3]);
test.deepEqual(d3.permute([3, 4, 5], [2, 0, 1]), [5, 3, 4]);
Expand Down Expand Up @@ -48,3 +146,12 @@ tape("permute(…) can take a typed array as the source", (test) => {
tape("permute(…) can take an iterable as the keys", (test) => {
test.deepEqual(d3.permute({foo: 1, bar: 2}, new Set(["bar", "foo"])), [2, 1]);
});

function count(values) {
let count = 0;
// eslint-disable-next-line no-unused-vars
Copy link

Choose a reason for hiding this comment

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

There must be a way to do this without unused vars?

for (const _value of values) {
count++;
}
return count;
}