pick-me-too 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +24 -0
- package/lib/index.d.ts +55 -19
- package/lib/index.js +91 -41
- package/package.json +1 -1
- package/src/__tests__/documentation.test.ts +10 -2
- package/src/index.ts +79 -25
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -150,6 +150,30 @@ for (let i = 0; i < 10; i++) ar.push(fooBarBaz())
|
|
|
150
150
|
// => [ 'baz', 'foo', 'baz', 'baz', 'baz', 'bar', 'baz', 'baz', 'bar', 'baz' ]
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
### `function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: () => number) => () => T`
|
|
154
|
+
|
|
155
|
+
Generates a function from random number generators to generators of `T`.
|
|
156
|
+
|
|
157
|
+
This is like `pickMe` -- in fact, `pickMe` delegates to this -- but it factors out the random
|
|
158
|
+
number generator. It is useful in a situation where you want to incur the compilation of the
|
|
159
|
+
picker only once but you want to plug in different random number sequences on different
|
|
160
|
+
occasions.
|
|
161
|
+
|
|
162
|
+
@example
|
|
163
|
+
```ts
|
|
164
|
+
import { pickMeToo } from 'pick-me-too'
|
|
165
|
+
|
|
166
|
+
const fooBarBaz = pickMeToo([["foo", 1], ["bar", 1000], ["baz", 1]])
|
|
167
|
+
|
|
168
|
+
// minimum probablility generator just returns "foo"
|
|
169
|
+
fooBarBaz(() => 0)()
|
|
170
|
+
// => "foo"
|
|
171
|
+
|
|
172
|
+
// maximum probability generator just returns "baz"
|
|
173
|
+
fooBarBaz(() => 1)()
|
|
174
|
+
// => "baz"
|
|
175
|
+
```
|
|
176
|
+
|
|
153
177
|
### `function rando(seed: number): () => number`
|
|
154
178
|
|
|
155
179
|
Generates a random number sequence generator.
|
package/lib/index.d.ts
CHANGED
|
@@ -7,37 +7,73 @@
|
|
|
7
7
|
* If the parameters are `[[1, 1], [2, 2]]`, you will get twice as many 2s as 1s. You will get the same
|
|
8
8
|
* results with `[[1, 0.1], [2, 0.2]]`, `[[1, 10], [2, 20]]`, and `[[1, 0.13], [2, 0.26]]`.
|
|
9
9
|
*
|
|
10
|
+
* The second, optional parameter is a random number sequence generator. Provide this if you want a reproducible
|
|
11
|
+
* sequence of selections from your frequency list. If this doesn't concern you, leave it off and take
|
|
12
|
+
* whatever `Math.random()` gives you.
|
|
13
|
+
*
|
|
10
14
|
* If the frequency list is not a list of pairs but just a list of items, this is equivalent to a list of
|
|
11
|
-
* pairs where all the frequencies are the same. Note, this will not work if the items are all pairs whose
|
|
12
|
-
* second value is a number. If you want to pick among such pairs you'll need to wrap them
|
|
13
|
-
* of a frequency pair with the second value being some common positive number.
|
|
15
|
+
* pairs where all the frequencies are the same. (Note, this will not work if the items are all pairs whose
|
|
16
|
+
* second value is a number. If you want to pick among such pairs you'll need to wrap them in new pairs.)
|
|
14
17
|
*
|
|
15
18
|
* An item with
|
|
16
19
|
* a non-positive frequency will be culled from the list. `pickMe` will throw an error if the list is empty.
|
|
17
|
-
* Likewise, it will throw an error if any item appears more than once in the list. `[
|
|
18
|
-
* not good. `[[1, 1], [1,2]]` is no better.
|
|
20
|
+
* Likewise, it will throw an error if any item appears more than once in the list. `[]` is
|
|
21
|
+
* not good. `[[1, 1], [1, 2]]` is no better.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* Note, the item returned by `pickMe` is the item you provided, not a copy. If you are picking from a list of
|
|
24
|
+
* objects and you modify one of them, it will remain modified.
|
|
25
|
+
*
|
|
26
|
+
* Finally, `pickMe` compiles an efficient picker from the parameters provided. As with other compiled things, such
|
|
27
|
+
* as regular expressions, you want to do this one time in an initialization phase and then reuse it in loops.
|
|
28
|
+
* The compilation is not terribly expensive, but skipping it altogether is cheaper.
|
|
23
29
|
*
|
|
24
30
|
* @example
|
|
25
31
|
* ```ts
|
|
26
|
-
* import { rando, pickMe } from 'pick-me'
|
|
32
|
+
* import { rando, pickMe } from 'pick-me'
|
|
27
33
|
*
|
|
28
34
|
* // make a random number generator with a seed of 1
|
|
29
|
-
* const rng = rando(1)
|
|
30
|
-
* const fooBarBaz = pickMe([["foo", 1], ["bar", 2], ["baz", 3]], rng)
|
|
31
|
-
* const ar: string[] = []
|
|
35
|
+
* const rng = rando(1)
|
|
36
|
+
* const fooBarBaz = pickMe([["foo", 1], ["bar", 2], ["baz", 3]], rng)
|
|
37
|
+
* const ar: string[] = []
|
|
32
38
|
* for (let i = 0; i < 10; i++) ar.push(fooBarBaz())
|
|
33
39
|
* // => [ 'baz', 'foo', 'baz', 'baz', 'baz', 'bar', 'baz', 'baz', 'bar', 'baz' ]
|
|
34
40
|
* ```
|
|
35
41
|
*
|
|
36
42
|
* @param {[T, number][] | T[]} frequencies - phrases to match
|
|
37
|
-
* @param {
|
|
43
|
+
* @param {Rng} [rando=() => Math.random()] - random number generator
|
|
38
44
|
* @returns {() => T} a picker of T's
|
|
39
45
|
*/
|
|
40
|
-
export declare function pickMe<T>(frequencies: [T, number][] | T[], rando?:
|
|
46
|
+
export declare function pickMe<T>(frequencies: [T, number][] | T[], rando?: Rng): () => T;
|
|
47
|
+
/**
|
|
48
|
+
* Generates pickers of things. This separates the compilation step from frequencies so you
|
|
49
|
+
* can plug different random number generators in.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { pickMeToo } from 'pick-me'
|
|
54
|
+
*
|
|
55
|
+
* const fooBarBaz = pickMeToo([["foo", 1], ["bar", 1000], ["baz", 1]])
|
|
56
|
+
*
|
|
57
|
+
* // minimum probablility generator just returns "foo"
|
|
58
|
+
* fooBarBaz(() => 0)()
|
|
59
|
+
* // => "foo"
|
|
60
|
+
*
|
|
61
|
+
* // maximum probability generator just returns "baz"
|
|
62
|
+
* fooBarBaz(() => 1)()
|
|
63
|
+
* // => "baz"
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @template T - the sort of thing to pick
|
|
67
|
+
* @param {[T, number][] | T[]} frequencies - things to pick according to their frequencies
|
|
68
|
+
* @returns {(rng: Rng) => () => T} - a function from random number generators to pickers of things in the frequency list
|
|
69
|
+
*/
|
|
70
|
+
export declare function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: Rng) => () => T;
|
|
71
|
+
/**
|
|
72
|
+
* The type of random number generators. Ideally this will return number from a continuous distribution
|
|
73
|
+
* in the interval [0, 1], but for `pickMe` and number less than 0 is the same as 0 and any number
|
|
74
|
+
* greater than 1 is the same as 1.
|
|
75
|
+
*/
|
|
76
|
+
export declare type Rng = () => number;
|
|
41
77
|
/**
|
|
42
78
|
* Generates a random number sequence generator. This is a demonstration generator
|
|
43
79
|
* useful for testing and documentation. Any zero-argument function that returns
|
|
@@ -48,16 +84,16 @@ export declare function pickMe<T>(frequencies: [T, number][] | T[], rando?: () =
|
|
|
48
84
|
*
|
|
49
85
|
* @example
|
|
50
86
|
* ```ts
|
|
51
|
-
* import { rando } from 'pick-me'
|
|
87
|
+
* import { rando } from 'pick-me'
|
|
52
88
|
*
|
|
53
89
|
* // make a random number generator with a seed of 1
|
|
54
|
-
* const rng = rando(1)
|
|
55
|
-
* const ar: number[] = []
|
|
90
|
+
* const rng = rando(1)
|
|
91
|
+
* const ar: number[] = []
|
|
56
92
|
* for (let i = 0; i < 3; i++) ar.push(rng())
|
|
57
93
|
* // => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
|
|
58
94
|
* ```
|
|
59
95
|
*
|
|
60
96
|
* @param {number} seed - the seed for a random number sequence
|
|
61
|
-
* @returns {
|
|
97
|
+
* @returns {Rng} a generator of random numbers
|
|
62
98
|
*/
|
|
63
|
-
export declare function rando(seed: number):
|
|
99
|
+
export declare function rando(seed: number): Rng;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.rando = exports.pickMe = void 0;
|
|
3
|
+
exports.rando = exports.pickMeToo = exports.pickMe = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Generates a zero-argument function that returns an item from the frequency list
|
|
6
6
|
* parameter with a probability concordant with its specified frequency.
|
|
@@ -10,55 +10,82 @@ exports.rando = exports.pickMe = void 0;
|
|
|
10
10
|
* If the parameters are `[[1, 1], [2, 2]]`, you will get twice as many 2s as 1s. You will get the same
|
|
11
11
|
* results with `[[1, 0.1], [2, 0.2]]`, `[[1, 10], [2, 20]]`, and `[[1, 0.13], [2, 0.26]]`.
|
|
12
12
|
*
|
|
13
|
+
* The second, optional parameter is a random number sequence generator. Provide this if you want a reproducible
|
|
14
|
+
* sequence of selections from your frequency list. If this doesn't concern you, leave it off and take
|
|
15
|
+
* whatever `Math.random()` gives you.
|
|
16
|
+
*
|
|
13
17
|
* If the frequency list is not a list of pairs but just a list of items, this is equivalent to a list of
|
|
14
|
-
* pairs where all the frequencies are the same. Note, this will not work if the items are all pairs whose
|
|
15
|
-
* second value is a number. If you want to pick among such pairs you'll need to wrap them
|
|
16
|
-
* of a frequency pair with the second value being some common positive number.
|
|
18
|
+
* pairs where all the frequencies are the same. (Note, this will not work if the items are all pairs whose
|
|
19
|
+
* second value is a number. If you want to pick among such pairs you'll need to wrap them in new pairs.)
|
|
17
20
|
*
|
|
18
21
|
* An item with
|
|
19
22
|
* a non-positive frequency will be culled from the list. `pickMe` will throw an error if the list is empty.
|
|
20
|
-
* Likewise, it will throw an error if any item appears more than once in the list. `[
|
|
21
|
-
* not good. `[[1, 1], [1,2]]` is no better.
|
|
23
|
+
* Likewise, it will throw an error if any item appears more than once in the list. `[]` is
|
|
24
|
+
* not good. `[[1, 1], [1, 2]]` is no better.
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* Note, the item returned by `pickMe` is the item you provided, not a copy. If you are picking from a list of
|
|
27
|
+
* objects and you modify one of them, it will remain modified.
|
|
28
|
+
*
|
|
29
|
+
* Finally, `pickMe` compiles an efficient picker from the parameters provided. As with other compiled things, such
|
|
30
|
+
* as regular expressions, you want to do this one time in an initialization phase and then reuse it in loops.
|
|
31
|
+
* The compilation is not terribly expensive, but skipping it altogether is cheaper.
|
|
26
32
|
*
|
|
27
33
|
* @example
|
|
28
34
|
* ```ts
|
|
29
|
-
* import { rando, pickMe } from 'pick-me'
|
|
35
|
+
* import { rando, pickMe } from 'pick-me'
|
|
30
36
|
*
|
|
31
37
|
* // make a random number generator with a seed of 1
|
|
32
|
-
* const rng = rando(1)
|
|
33
|
-
* const fooBarBaz = pickMe([["foo", 1], ["bar", 2], ["baz", 3]], rng)
|
|
34
|
-
* const ar: string[] = []
|
|
38
|
+
* const rng = rando(1)
|
|
39
|
+
* const fooBarBaz = pickMe([["foo", 1], ["bar", 2], ["baz", 3]], rng)
|
|
40
|
+
* const ar: string[] = []
|
|
35
41
|
* for (let i = 0; i < 10; i++) ar.push(fooBarBaz())
|
|
36
42
|
* // => [ 'baz', 'foo', 'baz', 'baz', 'baz', 'bar', 'baz', 'baz', 'bar', 'baz' ]
|
|
37
43
|
* ```
|
|
38
44
|
*
|
|
39
45
|
* @param {[T, number][] | T[]} frequencies - phrases to match
|
|
40
|
-
* @param {
|
|
46
|
+
* @param {Rng} [rando=() => Math.random()] - random number generator
|
|
41
47
|
* @returns {() => T} a picker of T's
|
|
42
48
|
*/
|
|
43
49
|
function pickMe(frequencies, rando) {
|
|
44
|
-
frequencies
|
|
50
|
+
return pickMeToo(frequencies)(rando !== null && rando !== void 0 ? rando : (() => Math.random()));
|
|
51
|
+
}
|
|
52
|
+
exports.pickMe = pickMe;
|
|
53
|
+
/**
|
|
54
|
+
* Generates pickers of things. This separates the compilation step from frequencies so you
|
|
55
|
+
* can plug different random number generators in.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { pickMeToo } from 'pick-me'
|
|
60
|
+
*
|
|
61
|
+
* const fooBarBaz = pickMeToo([["foo", 1], ["bar", 1000], ["baz", 1]])
|
|
62
|
+
*
|
|
63
|
+
* // minimum probablility generator just returns "foo"
|
|
64
|
+
* fooBarBaz(() => 0)()
|
|
65
|
+
* // => "foo"
|
|
66
|
+
*
|
|
67
|
+
* // maximum probability generator just returns "baz"
|
|
68
|
+
* fooBarBaz(() => 1)()
|
|
69
|
+
* // => "baz"
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @template T - the sort of thing to pick
|
|
73
|
+
* @param {[T, number][] | T[]} frequencies - things to pick according to their frequencies
|
|
74
|
+
* @returns {(rng: Rng) => () => T} - a function from random number generators to pickers of things in the frequency list
|
|
75
|
+
*/
|
|
76
|
+
function pickMeToo(frequencies) {
|
|
77
|
+
const simpleton = simplePicker(frequencies);
|
|
78
|
+
if (simpleton)
|
|
79
|
+
return simpleton;
|
|
80
|
+
frequencies = sanitize(frequencies);
|
|
45
81
|
// short-circuit edge cases
|
|
46
82
|
if (frequencies.length === 0)
|
|
47
83
|
throw new Error(`nothing to select in ${frequencies}`);
|
|
48
84
|
if (frequencies.length === 1) {
|
|
49
85
|
const [t] = frequencies[0];
|
|
50
|
-
return () => t;
|
|
86
|
+
return (_rng) => () => t;
|
|
51
87
|
}
|
|
52
|
-
|
|
53
|
-
// it is difficult to do this efficiently because we allow T to be anything
|
|
54
|
-
for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
|
|
55
|
-
const t = frequencies[i][0];
|
|
56
|
-
for (let j = i + 1; j < frequencies.length; j++) {
|
|
57
|
-
if (t === frequencies[j][0])
|
|
58
|
-
throw new Error(`${t} appears more than once among frequencies`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
rando !== null && rando !== void 0 ? rando : (rando = () => Math.random());
|
|
88
|
+
dupChecker(frequencies, (item) => item[0]);
|
|
62
89
|
let total = 0, acc = 0;
|
|
63
90
|
for (const [, n] of frequencies)
|
|
64
91
|
total += n;
|
|
@@ -73,17 +100,42 @@ function pickMe(frequencies, rando) {
|
|
|
73
100
|
thresholds.push(n);
|
|
74
101
|
}
|
|
75
102
|
const f = thresholder(thresholds);
|
|
76
|
-
return () => items[f(
|
|
103
|
+
return (rng) => () => items[f(rng())];
|
|
77
104
|
}
|
|
78
|
-
exports.
|
|
79
|
-
//
|
|
80
|
-
|
|
105
|
+
exports.pickMeToo = pickMeToo;
|
|
106
|
+
// throw an error in case of duplicates
|
|
107
|
+
// it is difficult to do this efficiently because we allow T to be anything, so we can't for instance use a Set
|
|
108
|
+
function dupChecker(frequencies, toT) {
|
|
109
|
+
for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
|
|
110
|
+
const t = toT(frequencies[i]);
|
|
111
|
+
for (let j = i + 1; j < frequencies.length; j++) {
|
|
112
|
+
if (t === toT(frequencies[j]))
|
|
113
|
+
throw new Error(`${t} appears more than once among frequencies`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// if all frequencies are the same, we can make a simpler picker
|
|
118
|
+
function simplePicker(frequencies) {
|
|
81
119
|
if (frequencies.length === 0)
|
|
82
|
-
return
|
|
83
|
-
if (frequencies.every((s) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number'))
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
return;
|
|
121
|
+
if (frequencies.every((s) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number')) {
|
|
122
|
+
const castFrequencies = frequencies;
|
|
123
|
+
const [, n1] = castFrequencies[0];
|
|
124
|
+
if (n1 > 0 && castFrequencies.every(([, n2]) => n2 === n1)) {
|
|
125
|
+
const items = castFrequencies.map(([t]) => t);
|
|
126
|
+
dupChecker(items, (item) => item);
|
|
127
|
+
return (rng) => () => items[Math.floor(items.length * rng())];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const items = [...frequencies];
|
|
132
|
+
if (items.length === 1) {
|
|
133
|
+
const t = items[0];
|
|
134
|
+
return (_rng) => () => t;
|
|
135
|
+
}
|
|
136
|
+
dupChecker(items, (item) => item);
|
|
137
|
+
return (rng) => () => items[Math.floor(items.length * rng())];
|
|
138
|
+
}
|
|
87
139
|
}
|
|
88
140
|
// remove impossible frequencies
|
|
89
141
|
function sanitize(frequencies) {
|
|
@@ -103,8 +155,6 @@ function portion(thresholds, start, end) {
|
|
|
103
155
|
const i = start + (n % 2 === 0 ? n / 2 - 1 : (n - 1) / 2);
|
|
104
156
|
return `(n > ${thresholds[i]} ? ${portion(thresholds, i + 1, end)} : ${portion(thresholds, start, i + 1)})`;
|
|
105
157
|
}
|
|
106
|
-
// return a generator of a reproducible random number sequence
|
|
107
|
-
// gotten from https://stackoverflow.com/a/47593316/15060051 -- this is Mulberry32
|
|
108
158
|
/**
|
|
109
159
|
* Generates a random number sequence generator. This is a demonstration generator
|
|
110
160
|
* useful for testing and documentation. Any zero-argument function that returns
|
|
@@ -115,17 +165,17 @@ function portion(thresholds, start, end) {
|
|
|
115
165
|
*
|
|
116
166
|
* @example
|
|
117
167
|
* ```ts
|
|
118
|
-
* import { rando } from 'pick-me'
|
|
168
|
+
* import { rando } from 'pick-me'
|
|
119
169
|
*
|
|
120
170
|
* // make a random number generator with a seed of 1
|
|
121
|
-
* const rng = rando(1)
|
|
122
|
-
* const ar: number[] = []
|
|
171
|
+
* const rng = rando(1)
|
|
172
|
+
* const ar: number[] = []
|
|
123
173
|
* for (let i = 0; i < 3; i++) ar.push(rng())
|
|
124
174
|
* // => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
|
|
125
175
|
* ```
|
|
126
176
|
*
|
|
127
177
|
* @param {number} seed - the seed for a random number sequence
|
|
128
|
-
* @returns {
|
|
178
|
+
* @returns {Rng} a generator of random numbers
|
|
129
179
|
*/
|
|
130
180
|
function rando(seed) {
|
|
131
181
|
return () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** tests for all the code shown in documentation */
|
|
2
2
|
|
|
3
|
-
import { rando, pickMe } from '..'
|
|
3
|
+
import { rando, pickMe, pickMeToo } from '..'
|
|
4
4
|
|
|
5
5
|
describe('README', () => {
|
|
6
6
|
describe('Synopsis', () => {
|
|
@@ -92,7 +92,6 @@ describe('README', () => {
|
|
|
92
92
|
return s.join(' ')
|
|
93
93
|
}
|
|
94
94
|
const saying = clause()
|
|
95
|
-
console.log(saying)
|
|
96
95
|
test('polynesianish', () => expect(saying).toEqual("rekātū pe'ide kēpe 'ātēdapidapi kaha"))
|
|
97
96
|
})
|
|
98
97
|
})
|
|
@@ -112,6 +111,15 @@ describe('index.ts', () => {
|
|
|
112
111
|
for (let i = 0; i < 10; i++) ar.push(fooBarBaz())
|
|
113
112
|
test('pickMe', () => expect(ar).toEqual(['baz', 'foo', 'baz', 'baz', 'baz', 'bar', 'baz', 'baz', 'bar', 'baz']))
|
|
114
113
|
})
|
|
114
|
+
describe('pickMeToo', () => {
|
|
115
|
+
const fooBarBaz = pickMeToo([
|
|
116
|
+
['foo', 1],
|
|
117
|
+
['bar', 1000],
|
|
118
|
+
['baz', 1],
|
|
119
|
+
])
|
|
120
|
+
test('low probability', () => expect(fooBarBaz(() => 0)()).toEqual('foo'))
|
|
121
|
+
test('high probability', () => expect(fooBarBaz(() => 1)()).toEqual('baz'))
|
|
122
|
+
})
|
|
115
123
|
describe('rando', () => {
|
|
116
124
|
const rng = rando(1)
|
|
117
125
|
const ar: number[] = []
|
package/src/index.ts
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
* a non-positive frequency will be culled from the list. `pickMe` will throw an error if the list is empty.
|
|
20
20
|
* Likewise, it will throw an error if any item appears more than once in the list. `[]` is
|
|
21
21
|
* not good. `[[1, 1], [1, 2]]` is no better.
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* Note, the item returned by `pickMe` is the item you provided, not a copy. If you are picking from a list of
|
|
24
24
|
* objects and you modify one of them, it will remain modified.
|
|
25
|
-
*
|
|
25
|
+
*
|
|
26
26
|
* Finally, `pickMe` compiles an efficient picker from the parameters provided. As with other compiled things, such
|
|
27
27
|
* as regular expressions, you want to do this one time in an initialization phase and then reuse it in loops.
|
|
28
28
|
* The compilation is not terribly expensive, but skipping it altogether is cheaper.
|
|
@@ -40,26 +40,49 @@
|
|
|
40
40
|
* ```
|
|
41
41
|
*
|
|
42
42
|
* @param {[T, number][] | T[]} frequencies - phrases to match
|
|
43
|
-
* @param {
|
|
43
|
+
* @param {Rng} [rando=() => Math.random()] - random number generator
|
|
44
44
|
* @returns {() => T} a picker of T's
|
|
45
45
|
*/
|
|
46
|
-
export function pickMe<T>(frequencies: [T, number][] | T[], rando?:
|
|
47
|
-
frequencies
|
|
46
|
+
export function pickMe<T>(frequencies: [T, number][] | T[], rando?: Rng): () => T {
|
|
47
|
+
return pickMeToo(frequencies)(rando ?? (() => Math.random()))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generates pickers of things. This separates the compilation step from frequencies so you
|
|
52
|
+
* can plug different random number generators in.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { pickMeToo } from 'pick-me'
|
|
57
|
+
*
|
|
58
|
+
* const fooBarBaz = pickMeToo([["foo", 1], ["bar", 1000], ["baz", 1]])
|
|
59
|
+
*
|
|
60
|
+
* // minimum probablility generator just returns "foo"
|
|
61
|
+
* fooBarBaz(() => 0)()
|
|
62
|
+
* // => "foo"
|
|
63
|
+
*
|
|
64
|
+
* // maximum probability generator just returns "baz"
|
|
65
|
+
* fooBarBaz(() => 1)()
|
|
66
|
+
* // => "baz"
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @template T - the sort of thing to pick
|
|
70
|
+
* @param {[T, number][] | T[]} frequencies - things to pick according to their frequencies
|
|
71
|
+
* @returns {(rng: Rng) => () => T} - a function from random number generators to pickers of things in the frequency list
|
|
72
|
+
*/
|
|
73
|
+
export function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: Rng) => () => T {
|
|
74
|
+
const simpleton = simplePicker(frequencies)
|
|
75
|
+
if (simpleton) return simpleton
|
|
76
|
+
|
|
77
|
+
frequencies = sanitize(frequencies as any as [T, number][])
|
|
78
|
+
|
|
48
79
|
// short-circuit edge cases
|
|
49
80
|
if (frequencies.length === 0) throw new Error(`nothing to select in ${frequencies}`)
|
|
50
81
|
if (frequencies.length === 1) {
|
|
51
82
|
const [t] = frequencies[0]
|
|
52
|
-
return () => t
|
|
83
|
+
return (_rng: Rng) => () => t
|
|
53
84
|
}
|
|
54
|
-
|
|
55
|
-
// it is difficult to do this efficiently because we allow T to be anything
|
|
56
|
-
for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
|
|
57
|
-
const t = frequencies[i][0]
|
|
58
|
-
for (let j = i + 1; j < frequencies.length; j++) {
|
|
59
|
-
if (t === frequencies[j][0]) throw new Error(`${t} appears more than once among frequencies`)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
rando ??= () => Math.random()
|
|
85
|
+
dupChecker(frequencies, (item: [T, number] | T) => (item as [T, number])[0])
|
|
63
86
|
let total = 0,
|
|
64
87
|
acc = 0
|
|
65
88
|
for (const [, n] of frequencies) total += n
|
|
@@ -75,16 +98,47 @@ export function pickMe<T>(frequencies: [T, number][] | T[], rando?: () => number
|
|
|
75
98
|
thresholds.push(n)
|
|
76
99
|
}
|
|
77
100
|
const f = thresholder(thresholds)
|
|
78
|
-
return () => items[f(
|
|
101
|
+
return (rng: Rng) => () => items[f(rng())]!
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The type of random number generators. Ideally this will return number from a continuous distribution
|
|
106
|
+
* in the interval [0, 1], but for `pickMe` and number less than 0 is the same as 0 and any number
|
|
107
|
+
* greater than 1 is the same as 1.
|
|
108
|
+
*/
|
|
109
|
+
export type Rng = () => number
|
|
110
|
+
|
|
111
|
+
// throw an error in case of duplicates
|
|
112
|
+
// it is difficult to do this efficiently because we allow T to be anything, so we can't for instance use a Set
|
|
113
|
+
function dupChecker<T>(frequencies: [T, number][] | T[], toT: (item: [T, number] | T) => T) {
|
|
114
|
+
for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
|
|
115
|
+
const t = toT(frequencies[i])
|
|
116
|
+
for (let j = i + 1; j < frequencies.length; j++) {
|
|
117
|
+
if (t === toT(frequencies[j])) throw new Error(`${t} appears more than once among frequencies`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
79
120
|
}
|
|
80
121
|
|
|
81
|
-
//
|
|
82
|
-
function
|
|
83
|
-
if (frequencies.length === 0) return
|
|
84
|
-
if ((frequencies as any).every((s: any) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number'))
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
// if all frequencies are the same, we can make a simpler picker
|
|
123
|
+
function simplePicker<T>(frequencies: [T, number][] | T[]): undefined | ((rng: Rng) => () => T) {
|
|
124
|
+
if (frequencies.length === 0) return
|
|
125
|
+
if ((frequencies as any).every((s: any) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number')) {
|
|
126
|
+
const castFrequencies: [T, number][] = frequencies as any as [T, number][]
|
|
127
|
+
const [, n1] = castFrequencies[0]
|
|
128
|
+
if (n1 > 0 && castFrequencies.every(([, n2]) => n2 === n1)) {
|
|
129
|
+
const items = castFrequencies.map(([t]) => t)
|
|
130
|
+
dupChecker(items, (item: [T, number] | T) => item as T)
|
|
131
|
+
return (rng: Rng) => () => items[Math.floor(items.length * rng())]
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const items: T[] = [...frequencies] as any as T[]
|
|
135
|
+
if (items.length === 1) {
|
|
136
|
+
const t = items[0]
|
|
137
|
+
return (_rng: Rng) => () => t
|
|
138
|
+
}
|
|
139
|
+
dupChecker(items, (item: [T, number] | T) => item as T)
|
|
140
|
+
return (rng: Rng) => () => items[Math.floor(items.length * rng())]
|
|
141
|
+
}
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
// remove impossible frequencies
|
|
@@ -130,9 +184,9 @@ function portion(thresholds: number[], start: number, end: number): string {
|
|
|
130
184
|
* ```
|
|
131
185
|
*
|
|
132
186
|
* @param {number} seed - the seed for a random number sequence
|
|
133
|
-
* @returns {
|
|
187
|
+
* @returns {Rng} a generator of random numbers
|
|
134
188
|
*/
|
|
135
|
-
export function rando(seed: number):
|
|
189
|
+
export function rando(seed: number): Rng {
|
|
136
190
|
return () => {
|
|
137
191
|
let t = (seed += 0x6d2b79f5)
|
|
138
192
|
t = Math.imul(t ^ (t >>> 15), t | 1)
|