diff --git a/README.md b/README.md index 31d2a39..d5b3b9e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ Randomness algorithms for JavaScript. See [docs](https://aureooms.github.io/js-random). Parent is [@aureooms/js-algorithms](https://aureooms.github.io/js-algorithms). +> :warning: Depending on your environment, the code may require +> `regeneratorRuntime` to be defined, for instance by importing +> [regenerator-runtime/runtime](https://www.npmjs.com/package/regenerator-runtime). + ```js import { randint , // randint(i, j) -> [i, j[ \cap ZZ diff --git a/doc/manual/usage.md b/doc/manual/usage.md index 22435ca..7c2a9ef 100644 --- a/doc/manual/usage.md +++ b/doc/manual/usage.md @@ -1,6 +1,9 @@ # Usage -The code needs a ES2015+ polyfill to work, for example -[regenerator-runtime/runtime](https://babeljs.io/docs/usage/polyfill). + +> :warning: Depending on your environment, the code may require +> `regeneratorRuntime` to be defined, for instance by importing +> [regenerator-runtime/runtime](https://www.npmjs.com/package/regenerator-runtime). + ```js require( 'regenerator-runtime/runtime' ) ; // or @@ -9,7 +12,7 @@ import 'regenerator-runtime/runtime.js' ; Then ```js -const random = require( '@aureooms/js-random' ) ; +const { ... } = require( '@aureooms/js-random' ) ; // or -import * as random from '@aureooms/js-random' ; +import { ... } from '@aureooms/js-random' ; ``` diff --git a/package.json b/package.json index be3fe94..001d098 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@aureooms/js-random", "description": "Randomness algorithms for JavaScript", - "version": "3.2.1", + "version": "3.3.0", "license": "AGPL-3.0", "author": "Aurélien Ooms ", "homepage": "https://aureooms.github.io/js-random", @@ -67,12 +67,12 @@ "@aureooms/js-array": "4.0.0", "@aureooms/js-compare": "^2.0.1", "@aureooms/js-functools": "2.0.3", - "@aureooms/js-itertools": "5.0.1", + "@aureooms/js-itertools": "5.1.0", "@aureooms/js-memory": "4.0.0", "@aureooms/js-type": "1.0.4", - "@babel/core": "7.13.10", - "@babel/preset-env": "7.13.10", - "@babel/register": "7.13.8", + "@babel/core": "7.13.14", + "@babel/preset-env": "7.13.12", + "@babel/register": "7.13.14", "@commitlint/cli": "12.1.1", "@js-library/commitlint-config": "0.0.4", "ava": "3.15.0", diff --git a/src/api/randrange.js b/src/api/randrange.js index 30c2d98..9304066 100644 --- a/src/api/randrange.js +++ b/src/api/randrange.js @@ -1,12 +1,24 @@ import randint from './randint.js'; /** - * Return a randomly selected element from range(start, stop, step). + * Pick an element from range(start, stop, step) uniformly at random. + * + * Return an element from range(start, stop, step) selected uniformly at random. + * If step is positive, this set corresponds to + * {x: x in [start, stop[ AND x % step = 0}. + * If step is negative, the range has to be given in reverse order, that is, + * largest value first, smallest value second. Both the starting value and the + * step value are optional. By default the starting value is 0. + * The default for the step value is 1. + * + * TODO: Handle empty ranges. + * + * @param {number} [start=0] - The starting value. + * @param {number} stop - The stopping value. + * @param {number} [step=1] - The step value. + * @return {number} The picked element. */ - const randrange = (start, stop, step) => { - // TODO handle empty ranges - if (stop === undefined) return randint(0, start); if (step === undefined) step = 1; diff --git a/src/api/sample.js b/src/api/sample.js index 2a59cdc..7716668 100644 --- a/src/api/sample.js +++ b/src/api/sample.js @@ -2,7 +2,7 @@ import _fisheryates from '../kernel/_fisheryates.js'; import randint from './randint.js'; /** - * Take a sample of size n (without repetitions) from the items i through + * Take a sample of size n (without replacement) from the items i through * j-1 of the input array. This is done in-place. The sample can be retrieved * from position i to i+n. * diff --git a/src/api/shuffled.js b/src/api/shuffled.js new file mode 100644 index 0000000..0ade2ab --- /dev/null +++ b/src/api/shuffled.js @@ -0,0 +1,13 @@ +import _fisheryates_inside_out from '../kernel/_fisheryates_inside_out.js'; +import randint from './randint.js'; + +/** + * Given an input iterable, constructs an array containing the elements of the + * input shuffled uniformly at random. + * + * @function + * @param {Iterable} iterable The input iterable. + * @return {Array} The constructed array. + */ +const shuffled = _fisheryates_inside_out(randint); +export default shuffled; diff --git a/src/index.js b/src/index.js index fe17915..7365016 100644 --- a/src/index.js +++ b/src/index.js @@ -5,8 +5,10 @@ export {default as random} from './api/random.js'; export {default as randrange} from './api/randrange.js'; export {default as sample} from './api/sample.js'; export {default as shuffle} from './api/shuffle.js'; +export {default as shuffled} from './api/shuffled.js'; export {default as _choice} from './kernel/_choice.js'; export {default as _fisheryates} from './kernel/_fisheryates.js'; +export {default as _fisheryates_inside_out} from './kernel/_fisheryates_inside_out.js'; export {default as _randfloat} from './kernel/_randfloat.js'; export {default as _randint} from './kernel/_randint.js'; export {default as _shuffle} from './kernel/_shuffle.js'; diff --git a/src/kernel/_fisheryates.js b/src/kernel/_fisheryates.js index b981728..18640c4 100644 --- a/src/kernel/_fisheryates.js +++ b/src/kernel/_fisheryates.js @@ -1,5 +1,5 @@ /** - * Sample element from an array using Fisher-Yates method. + * Sample elements from an array using Fisher-Yates method. * * NOTE: The original description of the algorithm by Fisher and Yates [1] had * unnecessary bookkeeping which made the algorithm run in O(n * (j-i)) time. @@ -24,7 +24,7 @@ */ const _fisheryates = (randint) => { /** - * Take a sample of size n (without repetitions) from the items i through + * Take a sample of size n (without replacement) from the items i through * j-1 of the input array. This is done in-place. The sample can be * retrieved from position i to i+n. * @@ -42,11 +42,9 @@ const _fisheryates = (randint) => { for (; i < k; ++i) { // Choose any index p in the remaining array - const p = randint(i, j); - // Swap element at index p with first element in the array - + // Swap selected element with the first remaining element. const tmp = a[i]; a[i] = a[p]; a[p] = tmp; diff --git a/src/kernel/_fisheryates_inside_out.js b/src/kernel/_fisheryates_inside_out.js new file mode 100644 index 0000000..e834696 --- /dev/null +++ b/src/kernel/_fisheryates_inside_out.js @@ -0,0 +1,62 @@ +/** + * Shuffle elements of an iterable using an inside-out implementation of the + * Fisher-Yates method. + * + * One can observe that if the input contains n elements, the loop has exactly + * n! possible outcomes: one for the first iteration, two for the second, three + * for the third, etc., the number of outcomes of a loop being the product of + * the number of outcomes for each iteration. Given a perfect randint function, + * each iteration's outcomes are equally likely, and independent of other + * iterations outcomes. The proof below shows that these outcomes are + * distinct. + * + * To see that this method yields the correct result (assume perfect randint): + * 1. Observe that it is correct when the input is empty. + * 2. By induction: + * - Induction hypothesis: assume it is correct when the input consists of + * n elements. + * - We almost insert the (n+1)th element at one of the n+1 possible + * insertion position in the output array. Almost because we move the + * element that is at the insertion position at the end instead of + * shifting the elements right of the insertion position to make room for + * the inserted element. + * - Ideally, since we inserted the last element at one of the n+1 + * positions, we would like that the elements inserted earlier form one + * of n! permutations uniformly at random after moving the element under + * the insertion position. This is true because the permutations that we + * obtain after this move are in one-to-one correspondance with the n! + * distinct permutations that can be obtained before the move. These are + * equally likely to be produced by the induction hypothesis. + * + * @param {Function} randint The randint function. + * @return {Function} The sampling function. + */ +const _fisheryates_inside_out = (randint) => { + /** + * Given an input iterable, constructs an array containing the elements of + * the input shuffled uniformly at random. + * + * @param {Iterable} iterable The input iterable. + * @param {Array} [output=[]] The constructed array. + * @return {Array} The constructed array. + */ + const shuffled = (iterable, output = []) => { + let n = 0; + for (const item of iterable) { + const i = randint(-1, n); + if (i === -1) output.push(item); + else { + output.push(output[i]); + output[i] = item; + } + + ++n; + } + + return output; + }; + + return shuffled; +}; + +export default _fisheryates_inside_out; diff --git a/test/src/shuffled.js b/test/src/shuffled.js new file mode 100644 index 0000000..d7e297a --- /dev/null +++ b/test/src/shuffled.js @@ -0,0 +1,35 @@ +import test from 'ava'; +import {shuffled, _fisheryates_inside_out, randint} from '../../src/index.js'; + +import {list, range, sorted} from '@aureooms/js-itertools'; +import {increasing} from '@aureooms/js-compare'; + +const macro = (t, _, shuffle, i, j) => { + const input = list(range(i, j)); + const output = shuffled(range(i, j)); + t.is(output.length, input.length); + t.deepEqual(sorted(increasing, output), sorted(increasing, input)); +}; + +macro.title = (title, shuffle_name, _, i, j) => + title || `[${n}] shuffled ( ${shuffle_name}, ${i}, ${j} )`; + +const n = 100; + +const params = [ + [0, n], + [20, n], + [0, n - 20], + [10, n - 10], +]; + +const algorithms = [ + ['inside-out Fisher-Yates', _fisheryates_inside_out(randint)], + ['API', shuffled], +]; + +for (const [name, algorithm] of algorithms) { + for (const [i, j] of params) { + test(macro, name, algorithm, i, j); + } +} diff --git a/yarn.lock b/yarn.lock index 1ddc104..8a87c42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,10 +29,10 @@ resolved "https://registry.yarnpkg.com/@aureooms/js-functools/-/js-functools-2.0.3.tgz#5139d6245278193da4cf21c44666797bf40a1d6e" integrity sha1-UTnWJFJ4GT2kzyHERmZ5e/QKHW4= -"@aureooms/js-itertools@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@aureooms/js-itertools/-/js-itertools-5.0.1.tgz#73af47b0a710073730fccde6c57431a9178d1bea" - integrity sha512-EBmA0EiY49MSu7M6eCZvUyEhV79bvIQDg/LL3bfUG8fxhL4IeBLbgYc41a4/LunD5mLFG8ZQna/Z/oUjr4fUuQ== +"@aureooms/js-itertools@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@aureooms/js-itertools/-/js-itertools-5.1.0.tgz#bb90a237baab78eeaef177376ac762fc43b9d485" + integrity sha512-HC5Rl89Vc99lcp2HBtZVYpmr1Esjm4gDyBD4V8ATNB5zBMtJ2+Jd145UsvgIqHPg7z4RbX1G0XNW6pM4rB6Uuw== dependencies: "@aureooms/js-collections-deque" "^6.0.1" "@aureooms/js-error" "^5.0.2" @@ -83,29 +83,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== -"@babel/core@7.13.10": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559" - integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.13.9" - "@babel/helper-compilation-targets" "^7.13.10" - "@babel/helper-module-transforms" "^7.13.0" - "@babel/helpers" "^7.13.10" - "@babel/parser" "^7.13.10" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - lodash "^4.17.19" - semver "^6.3.0" - source-map "^0.5.0" - -"@babel/core@^7.12.10", "@babel/core@^7.12.16": +"@babel/core@7.13.14", "@babel/core@^7.12.10", "@babel/core@^7.12.16": version "7.13.14" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06" integrity sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA== @@ -536,11 +514,6 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.0.tgz#49b9b6ee213e5634fa80361dae139effef893f78" integrity sha512-w80kxEMFhE3wjMOQkfdTvv0CSdRSJZptIlLhU4eU/coNJeWjduspUFz+IRnBbAq6m5XYBFMoT1TNkk9K9yf10g== -"@babel/parser@^7.13.10": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" - integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== - "@babel/parser@^7.13.13", "@babel/parser@^7.3.3": version "7.13.13" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df" @@ -656,15 +629,6 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-proposal-optional-chaining@^7.13.8": - version "7.13.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.8.tgz#e39df93efe7e7e621841babc197982e140e90756" - integrity sha512-hpbBwbTgd7Cz1QryvwJZRo1U0k1q8uyBmeXOSQUjdg/A2TASkhR/rz7AyqZ/kS8kbpsNA80rOYbxySBJAqmhhQ== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-proposal-private-methods@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz#04bd4c6d40f6e6bbfa2f57e2d8094bad900ef787" @@ -1091,81 +1055,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13" -"@babel/preset-env@7.13.10": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252" - integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ== - dependencies: - "@babel/compat-data" "^7.13.8" - "@babel/helper-compilation-targets" "^7.13.10" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-validator-option" "^7.12.17" - "@babel/plugin-proposal-async-generator-functions" "^7.13.8" - "@babel/plugin-proposal-class-properties" "^7.13.0" - "@babel/plugin-proposal-dynamic-import" "^7.13.8" - "@babel/plugin-proposal-export-namespace-from" "^7.12.13" - "@babel/plugin-proposal-json-strings" "^7.13.8" - "@babel/plugin-proposal-logical-assignment-operators" "^7.13.8" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.13.8" - "@babel/plugin-proposal-numeric-separator" "^7.12.13" - "@babel/plugin-proposal-object-rest-spread" "^7.13.8" - "@babel/plugin-proposal-optional-catch-binding" "^7.13.8" - "@babel/plugin-proposal-optional-chaining" "^7.13.8" - "@babel/plugin-proposal-private-methods" "^7.13.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.12.13" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.12.13" - "@babel/plugin-transform-arrow-functions" "^7.13.0" - "@babel/plugin-transform-async-to-generator" "^7.13.0" - "@babel/plugin-transform-block-scoped-functions" "^7.12.13" - "@babel/plugin-transform-block-scoping" "^7.12.13" - "@babel/plugin-transform-classes" "^7.13.0" - "@babel/plugin-transform-computed-properties" "^7.13.0" - "@babel/plugin-transform-destructuring" "^7.13.0" - "@babel/plugin-transform-dotall-regex" "^7.12.13" - "@babel/plugin-transform-duplicate-keys" "^7.12.13" - "@babel/plugin-transform-exponentiation-operator" "^7.12.13" - "@babel/plugin-transform-for-of" "^7.13.0" - "@babel/plugin-transform-function-name" "^7.12.13" - "@babel/plugin-transform-literals" "^7.12.13" - "@babel/plugin-transform-member-expression-literals" "^7.12.13" - "@babel/plugin-transform-modules-amd" "^7.13.0" - "@babel/plugin-transform-modules-commonjs" "^7.13.8" - "@babel/plugin-transform-modules-systemjs" "^7.13.8" - "@babel/plugin-transform-modules-umd" "^7.13.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.13" - "@babel/plugin-transform-new-target" "^7.12.13" - "@babel/plugin-transform-object-super" "^7.12.13" - "@babel/plugin-transform-parameters" "^7.13.0" - "@babel/plugin-transform-property-literals" "^7.12.13" - "@babel/plugin-transform-regenerator" "^7.12.13" - "@babel/plugin-transform-reserved-words" "^7.12.13" - "@babel/plugin-transform-shorthand-properties" "^7.12.13" - "@babel/plugin-transform-spread" "^7.13.0" - "@babel/plugin-transform-sticky-regex" "^7.12.13" - "@babel/plugin-transform-template-literals" "^7.13.0" - "@babel/plugin-transform-typeof-symbol" "^7.12.13" - "@babel/plugin-transform-unicode-escapes" "^7.12.13" - "@babel/plugin-transform-unicode-regex" "^7.12.13" - "@babel/preset-modules" "^0.1.4" - "@babel/types" "^7.13.0" - babel-plugin-polyfill-corejs2 "^0.1.4" - babel-plugin-polyfill-corejs3 "^0.1.3" - babel-plugin-polyfill-regenerator "^0.1.2" - core-js-compat "^3.9.0" - semver "^6.3.0" - -"@babel/preset-env@^7.12.11": +"@babel/preset-env@7.13.12", "@babel/preset-env@^7.12.11": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.12.tgz#6dff470478290582ac282fb77780eadf32480237" integrity sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA== @@ -1272,10 +1162,10 @@ "@babel/plugin-transform-react-jsx-development" "^7.12.17" "@babel/plugin-transform-react-pure-annotations" "^7.12.1" -"@babel/register@7.13.8": - version "7.13.8" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.13.8.tgz#d9051dc6820cb4e86375cc0e2d55a4862b31184f" - integrity sha512-yCVtABcmvQjRsX2elcZFUV5Q5kDDpHdtXKKku22hNDma60lYuhKmtp1ykZ/okRCPLT2bR5S+cA1kvtBdAFlDTQ== +"@babel/register@7.13.14": + version "7.13.14" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.13.14.tgz#bbfa8f4f027c2ebc432e8e69e078b632605f2d9b" + integrity sha512-iyw0hUwjh/fzN8qklVqZodbyWjEBOG0KdDnBOpv3zzIgK3NmuRXBmIXH39ZBdspkn8LTHvSboN+oYb4MT43+9Q== dependencies: find-cache-dir "^2.0.0" lodash "^4.17.19"