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 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
- // => "rekātū pe'ide kēpe 'ātēdapidapi kaha"
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
- return pickMeToo(frequencies)(rando !== null && rando !== void 0 ? rando : (() => Math.random()));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pick-me-too",
3
- "version": "1.2.1",
3
+ "version": "2.0.0",
4
4
  "description": "Generate a function that returns random items from a list with specified frequencies.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -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("rekātū pe'ide kēpe 'ātēdapidapi kaha"))
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', 'foo', 'baz', 'baz', 'baz', 'bar', 'baz', 'baz', 'bar', '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('foo'))
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', 'foo', 'baz', 'baz', 'baz', 'bar', 'baz', 'baz', 'bar', '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
- return pickMeToo(frequencies)(rando ?? (() => Math.random()))
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 just returns "foo"
61
+ * // minimum probablility generator; this will always return the same thing, which happens to be "foo"
61
62
  * fooBarBaz(() => 0)()
62
- * // => "foo"
63
+ * // => "bar"
63
64
  *
64
- * // maximum probability generator just returns "baz"
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
- let total = 0,
87
- acc = 0
88
- for (const [, n] of frequencies) total += n
89
- for (let i = 0; i < frequencies.length; i++) {
90
- const [, n] = frequencies[i]
91
- acc += n
92
- frequencies[i][1] = acc / total
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
- const items: T[] = [],
95
- thresholds: number[] = []
96
- for (const [t, n] of frequencies) {
97
- items.push(t)
98
- thresholds.push(n)
141
+ if (n.right) {
142
+ acc += n.interval
143
+ n.rightThreshold = acc
144
+ addThresholds(n.right, acc)
99
145
  }
100
- const f = thresholder(thresholds)
101
- return (rng: Rng) => () => items[f(rng())]!
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)