pick-me-too 1.2.1 → 2.0.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 +5 -0
- package/README.md +5 -1
- package/lib/index.d.ts +14 -0
- package/lib/index.js +19 -2
- package/package.json +1 -1
- package/src/__tests__/documentation.test.ts +6 -4
- package/src/__tests__/functionality.test.ts +13 -0
- package/src/index.ts +110 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.0.0
|
|
4
|
+
- improved efficiency of compiled picker
|
|
5
|
+
- ***BREAKING CHANGE***: with this improvement the order of picked items is no longer preserved, so a pseudo random number generator that return 0 or less or 1 or more is no longer necessarily returns the first or last item in the list, respectively
|
|
6
|
+
## v1.3.0 *2022-7-23*
|
|
7
|
+
- made `rando` reseedable
|
|
3
8
|
## v1.2.1 *2022-4-30*
|
|
4
9
|
- fixed typo in README
|
|
5
10
|
## v1.2.0 *2022-4-17*
|
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ const clause = () => {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const saying = clause()
|
|
106
|
-
// => "
|
|
106
|
+
// => "'ātika kādepāgū bīpīpe pōhopōhopunā'ī bi'a'anū'ā'idi'idi'ōkē'ōkēpetē"
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
## API
|
|
@@ -194,6 +194,10 @@ const rng = rando(1)
|
|
|
194
194
|
const ar: number[] = []
|
|
195
195
|
for (let i = 0; i < 3; i++) ar.push(rng())
|
|
196
196
|
// => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
|
|
197
|
+
|
|
198
|
+
// reseed the generator
|
|
199
|
+
(rng as any)(Math.random())
|
|
200
|
+
// => ??? the random number sequence is still deterministic, but at a completely different place
|
|
197
201
|
```
|
|
198
202
|
|
|
199
203
|
## Acknowledgements
|
package/lib/index.d.ts
CHANGED
|
@@ -82,6 +82,10 @@ export declare type Rng = () => number;
|
|
|
82
82
|
* The implementation of `rando` is taken from https://stackoverflow.com/a/47593316/15060051.
|
|
83
83
|
* It is the Mulberry32 algorithm.
|
|
84
84
|
*
|
|
85
|
+
* Note, `rando` can be re-seeded by passing an optional number argument as the fresh seed (see
|
|
86
|
+
* example below). This can be useful if you are sharing a random number generator among
|
|
87
|
+
* several objects and you need to change random number sequences.
|
|
88
|
+
*
|
|
85
89
|
* @example
|
|
86
90
|
* ```ts
|
|
87
91
|
* import { rando } from 'pick-me'
|
|
@@ -91,6 +95,16 @@ export declare type Rng = () => number;
|
|
|
91
95
|
* const ar: number[] = []
|
|
92
96
|
* for (let i = 0; i < 3; i++) ar.push(rng())
|
|
93
97
|
* // => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
|
|
98
|
+
*
|
|
99
|
+
* // the same seed will produce the same random number sequence
|
|
100
|
+
* rando(1)()
|
|
101
|
+
* // => 0.6270739405881613
|
|
102
|
+
* rando(1)()
|
|
103
|
+
* // => 0.6270739405881613
|
|
104
|
+
*
|
|
105
|
+
* // but you can provide a fresh seed to change the sequence
|
|
106
|
+
* (rando(1) as any)(2)
|
|
107
|
+
* // => 0.7342509443406016
|
|
94
108
|
* ```
|
|
95
109
|
*
|
|
96
110
|
* @param {number} seed - the seed for a random number sequence
|
package/lib/index.js
CHANGED
|
@@ -47,7 +47,8 @@ exports.rando = exports.pickMeToo = exports.pickMe = void 0;
|
|
|
47
47
|
* @returns {() => T} a picker of T's
|
|
48
48
|
*/
|
|
49
49
|
function pickMe(frequencies, rando) {
|
|
50
|
-
|
|
50
|
+
const rng = rando !== null && rando !== void 0 ? rando : (() => Math.random());
|
|
51
|
+
return pickMeToo(frequencies)(rng);
|
|
51
52
|
}
|
|
52
53
|
exports.pickMe = pickMe;
|
|
53
54
|
/**
|
|
@@ -163,6 +164,10 @@ function portion(thresholds, start, end) {
|
|
|
163
164
|
* The implementation of `rando` is taken from https://stackoverflow.com/a/47593316/15060051.
|
|
164
165
|
* It is the Mulberry32 algorithm.
|
|
165
166
|
*
|
|
167
|
+
* Note, `rando` can be re-seeded by passing an optional number argument as the fresh seed (see
|
|
168
|
+
* example below). This can be useful if you are sharing a random number generator among
|
|
169
|
+
* several objects and you need to change random number sequences.
|
|
170
|
+
*
|
|
166
171
|
* @example
|
|
167
172
|
* ```ts
|
|
168
173
|
* import { rando } from 'pick-me'
|
|
@@ -172,13 +177,25 @@ function portion(thresholds, start, end) {
|
|
|
172
177
|
* const ar: number[] = []
|
|
173
178
|
* for (let i = 0; i < 3; i++) ar.push(rng())
|
|
174
179
|
* // => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
|
|
180
|
+
*
|
|
181
|
+
* // the same seed will produce the same random number sequence
|
|
182
|
+
* rando(1)()
|
|
183
|
+
* // => 0.6270739405881613
|
|
184
|
+
* rando(1)()
|
|
185
|
+
* // => 0.6270739405881613
|
|
186
|
+
*
|
|
187
|
+
* // but you can provide a fresh seed to change the sequence
|
|
188
|
+
* (rando(1) as any)(2)
|
|
189
|
+
* // => 0.7342509443406016
|
|
175
190
|
* ```
|
|
176
191
|
*
|
|
177
192
|
* @param {number} seed - the seed for a random number sequence
|
|
178
193
|
* @returns {Rng} a generator of random numbers
|
|
179
194
|
*/
|
|
180
195
|
function rando(seed) {
|
|
181
|
-
return () => {
|
|
196
|
+
return (reseed) => {
|
|
197
|
+
if (reseed !== undefined)
|
|
198
|
+
seed = reseed;
|
|
182
199
|
let t = (seed += 0x6d2b79f5);
|
|
183
200
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
184
201
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
package/package.json
CHANGED
|
@@ -87,12 +87,12 @@ describe('README', () => {
|
|
|
87
87
|
rng,
|
|
88
88
|
)
|
|
89
89
|
const clause = () => {
|
|
90
|
-
let s = []
|
|
90
|
+
let s: string[] = []
|
|
91
91
|
for (let i = 0, lim = clauseLength(); i < lim; i++) s.push(word())
|
|
92
92
|
return s.join(' ')
|
|
93
93
|
}
|
|
94
94
|
const saying = clause()
|
|
95
|
-
test('polynesianish', () => expect(saying).toEqual("
|
|
95
|
+
test('polynesianish', () => expect(saying).toEqual("'ātika kādepāgū bīpīpe pōhopōhopunā'ī bi'a'anū'ā'idi'idi'ōkē'ōkēpetē"))
|
|
96
96
|
})
|
|
97
97
|
})
|
|
98
98
|
|
|
@@ -109,7 +109,7 @@ describe('index.ts', () => {
|
|
|
109
109
|
)
|
|
110
110
|
const ar: string[] = []
|
|
111
111
|
for (let i = 0; i < 10; i++) ar.push(fooBarBaz())
|
|
112
|
-
test('pickMe', () => expect(ar).toEqual(['baz', '
|
|
112
|
+
test('pickMe', () => expect(ar).toEqual(['bar', 'baz', 'bar', 'foo', 'foo', 'baz', 'bar', 'bar', 'baz', 'foo']))
|
|
113
113
|
})
|
|
114
114
|
describe('pickMeToo', () => {
|
|
115
115
|
const fooBarBaz = pickMeToo([
|
|
@@ -117,7 +117,7 @@ describe('index.ts', () => {
|
|
|
117
117
|
['bar', 1000],
|
|
118
118
|
['baz', 1],
|
|
119
119
|
])
|
|
120
|
-
test('low probability', () => expect(fooBarBaz(() => 0)()).toEqual('
|
|
120
|
+
test('low probability', () => expect(fooBarBaz(() => 0)()).toEqual('bar'))
|
|
121
121
|
test('high probability', () => expect(fooBarBaz(() => 1)()).toEqual('baz'))
|
|
122
122
|
})
|
|
123
123
|
describe('rando', () => {
|
|
@@ -127,5 +127,7 @@ describe('index.ts', () => {
|
|
|
127
127
|
test('rando', () => {
|
|
128
128
|
expect(ar).toEqual([0.6270739405881613, 0.002735721180215478, 0.5274470399599522])
|
|
129
129
|
})
|
|
130
|
+
test('rando(1)()', () => expect(rando(1)()).toEqual(0.6270739405881613))
|
|
131
|
+
test('(rando(1) as any)(2)', () => expect((rando(1) as any)(2)).toEqual(0.7342509443406016))
|
|
130
132
|
})
|
|
131
133
|
})
|
|
@@ -16,6 +16,19 @@ describe('rando', () => {
|
|
|
16
16
|
for (let i = 0; i < bins.length; i++) {
|
|
17
17
|
test('even distribution', () => expect(Math.round(bins[i] / 1000)).toEqual(1))
|
|
18
18
|
}
|
|
19
|
+
for (let i = 0; i < 10; i++) {
|
|
20
|
+
const seed = Math.random()
|
|
21
|
+
const rng1 = rando(seed),
|
|
22
|
+
rng2 = rando(seed)
|
|
23
|
+
const ar1: number[] = [],
|
|
24
|
+
ar2: number[] = []
|
|
25
|
+
for (let j = 0; j < 10; j++) {
|
|
26
|
+
ar1.push(rng1())
|
|
27
|
+
ar2.push(rng2())
|
|
28
|
+
}
|
|
29
|
+
test(`two randos with seed ${seed} produce the same sequence of random values`, () => expect(ar1).toEqual(ar2))
|
|
30
|
+
test(`randos can be reseeded`, () => expect(rng1()).not.toEqual((rng2 as any)(Math.random())))
|
|
31
|
+
}
|
|
19
32
|
})
|
|
20
33
|
|
|
21
34
|
describe('pickMe', () => {
|
package/src/index.ts
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* const fooBarBaz = pickMe([["foo", 1], ["bar", 2], ["baz", 3]], rng)
|
|
37
37
|
* const ar: string[] = []
|
|
38
38
|
* for (let i = 0; i < 10; i++) ar.push(fooBarBaz())
|
|
39
|
-
* // => [ 'baz', '
|
|
39
|
+
* // => ['bar', 'baz', 'bar', 'foo', 'foo', 'baz', 'bar', 'bar', 'baz', 'foo']
|
|
40
40
|
* ```
|
|
41
41
|
*
|
|
42
42
|
* @param {[T, number][] | T[]} frequencies - phrases to match
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
* @returns {() => T} a picker of T's
|
|
45
45
|
*/
|
|
46
46
|
export function pickMe<T>(frequencies: [T, number][] | T[], rando?: Rng): () => T {
|
|
47
|
-
|
|
47
|
+
const rng = rando ?? (() => Math.random())
|
|
48
|
+
return pickMeToo(frequencies)(rng)
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -57,11 +58,11 @@ export function pickMe<T>(frequencies: [T, number][] | T[], rando?: Rng): () =>
|
|
|
57
58
|
*
|
|
58
59
|
* const fooBarBaz = pickMeToo([["foo", 1], ["bar", 1000], ["baz", 1]])
|
|
59
60
|
*
|
|
60
|
-
* // minimum probablility generator
|
|
61
|
+
* // minimum probablility generator; this will always return the same thing, which happens to be "foo"
|
|
61
62
|
* fooBarBaz(() => 0)()
|
|
62
|
-
* // => "
|
|
63
|
+
* // => "bar"
|
|
63
64
|
*
|
|
64
|
-
* // maximum probability generator
|
|
65
|
+
* // maximum probability generator; this will always return the same thing, which happens to be "baz"
|
|
65
66
|
* fooBarBaz(() => 1)()
|
|
66
67
|
* // => "baz"
|
|
67
68
|
* ```
|
|
@@ -83,22 +84,96 @@ export function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: Rng) => ()
|
|
|
83
84
|
return (_rng: Rng) => () => t
|
|
84
85
|
}
|
|
85
86
|
dupChecker(frequencies, (item: [T, number] | T) => (item as [T, number])[0])
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const [objects, nodes] = prepare(frequencies)
|
|
88
|
+
const root = buildBranch(nodes)
|
|
89
|
+
addThresholds(root, 0)
|
|
90
|
+
const exp = ternarize(root)
|
|
91
|
+
const picker: (n: number) => number = eval(`(p) => ${exp}`)
|
|
92
|
+
return (rng: Rng) => () => objects[picker(rng())]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// convert a Node search tree into a nested ternary expression
|
|
96
|
+
function ternarize(n: Node): string {
|
|
97
|
+
if (n.left && n.right) {
|
|
98
|
+
return `(p < ${n.leftThreshold} ? ${ternarize(n.left)} : (p < ${n.rightThreshold} ? ${n.index} : ${ternarize(n.right)}))`
|
|
99
|
+
} else if (n.left) {
|
|
100
|
+
return `(p < ${n.leftThreshold} ? ${ternarize(n.left)} : ${n.index})`
|
|
101
|
+
} else if (n.right) {
|
|
102
|
+
return `(p < ${n.rightThreshold} ? ${n.index} : ${ternarize(n.right)})`
|
|
103
|
+
} else {
|
|
104
|
+
return n.index.toString()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// convert the list of frequencies into Nodes (see below) and a list
|
|
109
|
+
// of things to pick
|
|
110
|
+
function prepare<T>(frequencies: [T, number][]): [T[], Node[]] {
|
|
111
|
+
frequencies.sort(([, na], [,nb]) => nb - na)
|
|
112
|
+
const sum = frequencies.reduce((acc, [, n]) => acc + n, 0)
|
|
113
|
+
const objects: T[] = []
|
|
114
|
+
const nodes: Node[] = []
|
|
115
|
+
frequencies.forEach(([t, n], i) => {
|
|
116
|
+
objects.push(t)
|
|
117
|
+
nodes.push({interval: n / sum, index: i})
|
|
118
|
+
})
|
|
119
|
+
return [objects, nodes]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// a temporary data structure used for converting the list of frequencies
|
|
123
|
+
// into a tree structure we can search efficiently
|
|
124
|
+
type Node = {
|
|
125
|
+
index: number,
|
|
126
|
+
interval: number,
|
|
127
|
+
sum?: number,
|
|
128
|
+
leftThreshold?: number
|
|
129
|
+
rightThreshold?: number
|
|
130
|
+
left?: Node
|
|
131
|
+
right?: Node
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// given a search tree, add the probability thresholds for each node
|
|
135
|
+
function addThresholds(n: Node, acc: number) {
|
|
136
|
+
if (n.left) {
|
|
137
|
+
addThresholds(n.left, acc)
|
|
138
|
+
acc += n.left.sum!
|
|
139
|
+
n.leftThreshold = acc
|
|
93
140
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
thresholds.push(n)
|
|
141
|
+
if (n.right) {
|
|
142
|
+
acc += n.interval
|
|
143
|
+
n.rightThreshold = acc
|
|
144
|
+
addThresholds(n.right, acc)
|
|
99
145
|
}
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// convert a list of sorted nodes into an efficiently searchable tree
|
|
149
|
+
function buildBranch(nodes: Node[]): Node {
|
|
150
|
+
const sum = nodes.reduce((acc, n) => acc + n.interval, 0)
|
|
151
|
+
const n = nodes.shift()!
|
|
152
|
+
n.sum = sum
|
|
153
|
+
if (nodes.length === 0) return n
|
|
154
|
+
if (n.interval * 3 >= sum) {
|
|
155
|
+
// the frequencies are so skewed a linear search is more efficient than a binary search
|
|
156
|
+
n.right = buildBranch(nodes)
|
|
157
|
+
} else {
|
|
158
|
+
// perform a binary search, considering wide intervals before narrow ones
|
|
159
|
+
const [left, right] = nodes.reduce(([left, right, idx], n) => {
|
|
160
|
+
if (belongsToLeftBranch(idx)) left.push(n)
|
|
161
|
+
else right.push(n)
|
|
162
|
+
return [left, right, idx + 1]
|
|
163
|
+
}, [[], [], 1] as [Node[], Node[], number])
|
|
164
|
+
n.left = buildBranch(left)
|
|
165
|
+
if (right.length > 0) n.right = buildBranch(right)
|
|
166
|
+
}
|
|
167
|
+
return n
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// treating our sorted list of intervals as a heap, use the heap parent identification rules
|
|
171
|
+
// to determine whether the index in question is under the left or the right branch under the
|
|
172
|
+
// root node
|
|
173
|
+
function belongsToLeftBranch(i: number): boolean {
|
|
174
|
+
if (i === 0) return true
|
|
175
|
+
if (i < 0) return false
|
|
176
|
+
return belongsToLeftBranch(Math.floor((i - 1) / 2))
|
|
102
177
|
}
|
|
103
178
|
|
|
104
179
|
/**
|
|
@@ -149,21 +224,6 @@ function sanitize<T>(frequencies: [T, number][]): [T, number][] {
|
|
|
149
224
|
][]
|
|
150
225
|
}
|
|
151
226
|
|
|
152
|
-
// make a function that converts a random number into an index, preserving the frequencies
|
|
153
|
-
// assuming the random number is from an even distribution in the interval [0, 1]
|
|
154
|
-
function thresholder(thresholds: number[]): (n: number) => number {
|
|
155
|
-
return eval(`(n) => ${portion(thresholds, 0, thresholds.length)}`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// recursively convert the threshold list into nested ternary operators
|
|
159
|
-
// this allows us to find our index in O(log(n)) comparisons
|
|
160
|
-
function portion(thresholds: number[], start: number, end: number): string {
|
|
161
|
-
const n = end - start
|
|
162
|
-
if (n === 1) return start.toString()
|
|
163
|
-
const i = start + (n % 2 === 0 ? n / 2 - 1 : (n - 1) / 2)
|
|
164
|
-
return `(n > ${thresholds[i]} ? ${portion(thresholds, i + 1, end)} : ${portion(thresholds, start, i + 1)})`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
227
|
/**
|
|
168
228
|
* Generates a random number sequence generator. This is a demonstration generator
|
|
169
229
|
* useful for testing and documentation. Any zero-argument function that returns
|
|
@@ -171,6 +231,10 @@ function portion(thresholds: number[], start: number, end: number): string {
|
|
|
171
231
|
*
|
|
172
232
|
* The implementation of `rando` is taken from https://stackoverflow.com/a/47593316/15060051.
|
|
173
233
|
* It is the Mulberry32 algorithm.
|
|
234
|
+
*
|
|
235
|
+
* Note, `rando` can be re-seeded by passing an optional number argument as the fresh seed (see
|
|
236
|
+
* example below). This can be useful if you are sharing a random number generator among
|
|
237
|
+
* several objects and you need to change random number sequences.
|
|
174
238
|
*
|
|
175
239
|
* @example
|
|
176
240
|
* ```ts
|
|
@@ -181,13 +245,24 @@ function portion(thresholds: number[], start: number, end: number): string {
|
|
|
181
245
|
* const ar: number[] = []
|
|
182
246
|
* for (let i = 0; i < 3; i++) ar.push(rng())
|
|
183
247
|
* // => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
|
|
248
|
+
*
|
|
249
|
+
* // the same seed will produce the same random number sequence
|
|
250
|
+
* rando(1)()
|
|
251
|
+
* // => 0.6270739405881613
|
|
252
|
+
* rando(1)()
|
|
253
|
+
* // => 0.6270739405881613
|
|
254
|
+
*
|
|
255
|
+
* // but you can provide a fresh seed to change the sequence
|
|
256
|
+
* (rando(1) as any)(2)
|
|
257
|
+
* // => 0.7342509443406016
|
|
184
258
|
* ```
|
|
185
259
|
*
|
|
186
260
|
* @param {number} seed - the seed for a random number sequence
|
|
187
261
|
* @returns {Rng} a generator of random numbers
|
|
188
262
|
*/
|
|
189
263
|
export function rando(seed: number): Rng {
|
|
190
|
-
return () => {
|
|
264
|
+
return (reseed?: number) => {
|
|
265
|
+
if (reseed !== undefined) seed = reseed
|
|
191
266
|
let t = (seed += 0x6d2b79f5)
|
|
192
267
|
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
193
268
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|