-
Notifications
You must be signed in to change notification settings - Fork 188
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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: | ||||||
Returns a generator of permutations or a single permutation of the specified *source* object (or array). | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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
then later
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"] | ||||||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this link can be moved into the docs? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} | ||
} | ||
} |
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
There was a problem hiding this comment.
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!