pick-me-too 1.1.1 → 1.3.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,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.3.0 *2022-7-23*
4
+ - made `rando` reseedable
5
+ ## v1.2.1 *2022-4-30*
6
+ - fixed typo in README
7
+ ## v1.2.0 *2022-4-17*
8
+ - some efficiency improvements in edge cases
3
9
  ## v1.1.1 *2022-4-16*
4
10
  - fixed some project configuration
5
- - ## v1.1.0 *2022-4-16*
11
+ ## v1.1.0 *2022-4-16*
6
12
  - factored `pickMeToo` out of `pickMe`
package/README.md CHANGED
@@ -180,7 +180,7 @@ Generates a random number sequence generator.
180
180
 
181
181
  This is a demonstration generator useful for testing and documentation. Any zero-argument function that returns
182
182
  numbers will suffice for `pickMe`. If it returns 0 or less, the first item
183
- given to `pickMe` will be picked. If it is 1 or less, the last item will be.
183
+ given to `pickMe` will be picked. If it is 1 or more, the last item will be.
184
184
 
185
185
  The implementation of `rando` is taken from [this Stack Overflow comment](https://stackoverflow.com/a/47593316/15060051).
186
186
  There are many others if you don't find this suitable.
@@ -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
  /**
@@ -74,7 +75,10 @@ exports.pickMe = pickMe;
74
75
  * @returns {(rng: Rng) => () => T} - a function from random number generators to pickers of things in the frequency list
75
76
  */
76
77
  function pickMeToo(frequencies) {
77
- frequencies = sanitize(normalize(frequencies));
78
+ const simpleton = simplePicker(frequencies);
79
+ if (simpleton)
80
+ return simpleton;
81
+ frequencies = sanitize(frequencies);
78
82
  // short-circuit edge cases
79
83
  if (frequencies.length === 0)
80
84
  throw new Error(`nothing to select in ${frequencies}`);
@@ -82,15 +86,7 @@ function pickMeToo(frequencies) {
82
86
  const [t] = frequencies[0];
83
87
  return (_rng) => () => t;
84
88
  }
85
- // throw an error in case of duplicates
86
- // it is difficult to do this efficiently because we allow T to be anything
87
- for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
88
- const t = frequencies[i][0];
89
- for (let j = i + 1; j < frequencies.length; j++) {
90
- if (t === frequencies[j][0])
91
- throw new Error(`${t} appears more than once among frequencies`);
92
- }
93
- }
89
+ dupChecker(frequencies, (item) => item[0]);
94
90
  let total = 0, acc = 0;
95
91
  for (const [, n] of frequencies)
96
92
  total += n;
@@ -108,14 +104,39 @@ function pickMeToo(frequencies) {
108
104
  return (rng) => () => items[f(rng())];
109
105
  }
110
106
  exports.pickMeToo = pickMeToo;
111
- // take in an array of ambiguous type and return a list of pairs, T and frequency
112
- function normalize(frequencies) {
107
+ // throw an error in case of duplicates
108
+ // it is difficult to do this efficiently because we allow T to be anything, so we can't for instance use a Set
109
+ function dupChecker(frequencies, toT) {
110
+ for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
111
+ const t = toT(frequencies[i]);
112
+ for (let j = i + 1; j < frequencies.length; j++) {
113
+ if (t === toT(frequencies[j]))
114
+ throw new Error(`${t} appears more than once among frequencies`);
115
+ }
116
+ }
117
+ }
118
+ // if all frequencies are the same, we can make a simpler picker
119
+ function simplePicker(frequencies) {
113
120
  if (frequencies.length === 0)
114
- return frequencies;
115
- if (frequencies.every((s) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number'))
116
- return frequencies;
117
- const ar = frequencies.map((t) => [t, 1]);
118
- return ar;
121
+ return;
122
+ if (frequencies.every((s) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number')) {
123
+ const castFrequencies = frequencies;
124
+ const [, n1] = castFrequencies[0];
125
+ if (n1 > 0 && castFrequencies.every(([, n2]) => n2 === n1)) {
126
+ const items = castFrequencies.map(([t]) => t);
127
+ dupChecker(items, (item) => item);
128
+ return (rng) => () => items[Math.floor(items.length * rng())];
129
+ }
130
+ }
131
+ else {
132
+ const items = [...frequencies];
133
+ if (items.length === 1) {
134
+ const t = items[0];
135
+ return (_rng) => () => t;
136
+ }
137
+ dupChecker(items, (item) => item);
138
+ return (rng) => () => items[Math.floor(items.length * rng())];
139
+ }
119
140
  }
120
141
  // remove impossible frequencies
121
142
  function sanitize(frequencies) {
@@ -143,6 +164,10 @@ function portion(thresholds, start, end) {
143
164
  * The implementation of `rando` is taken from https://stackoverflow.com/a/47593316/15060051.
144
165
  * It is the Mulberry32 algorithm.
145
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
+ *
146
171
  * @example
147
172
  * ```ts
148
173
  * import { rando } from 'pick-me'
@@ -152,13 +177,25 @@ function portion(thresholds, start, end) {
152
177
  * const ar: number[] = []
153
178
  * for (let i = 0; i < 3; i++) ar.push(rng())
154
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
155
190
  * ```
156
191
  *
157
192
  * @param {number} seed - the seed for a random number sequence
158
193
  * @returns {Rng} a generator of random numbers
159
194
  */
160
195
  function rando(seed) {
161
- return () => {
196
+ return (reseed) => {
197
+ if (reseed !== undefined)
198
+ seed = reseed;
162
199
  let t = (seed += 0x6d2b79f5);
163
200
  t = Math.imul(t ^ (t >>> 15), t | 1);
164
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.1.1",
3
+ "version": "1.3.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,11 @@ 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
- console.log(saying)
96
95
  test('polynesianish', () => expect(saying).toEqual("rekātū pe'ide kēpe 'ātēdapidapi kaha"))
97
96
  })
98
97
  })
@@ -128,5 +127,7 @@ describe('index.ts', () => {
128
127
  test('rando', () => {
129
128
  expect(ar).toEqual([0.6270739405881613, 0.002735721180215478, 0.5274470399599522])
130
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))
131
132
  })
132
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
@@ -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
  /**
@@ -71,21 +72,18 @@ export function pickMe<T>(frequencies: [T, number][] | T[], rando?: Rng): () =>
71
72
  * @returns {(rng: Rng) => () => T} - a function from random number generators to pickers of things in the frequency list
72
73
  */
73
74
  export function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: Rng) => () => T {
74
- frequencies = sanitize(normalize(frequencies))
75
+ const simpleton = simplePicker(frequencies)
76
+ if (simpleton) return simpleton
77
+
78
+ frequencies = sanitize(frequencies as any as [T, number][])
79
+
75
80
  // short-circuit edge cases
76
81
  if (frequencies.length === 0) throw new Error(`nothing to select in ${frequencies}`)
77
82
  if (frequencies.length === 1) {
78
83
  const [t] = frequencies[0]
79
84
  return (_rng: Rng) => () => t
80
85
  }
81
- // throw an error in case of duplicates
82
- // it is difficult to do this efficiently because we allow T to be anything
83
- for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
84
- const t = frequencies[i][0]
85
- for (let j = i + 1; j < frequencies.length; j++) {
86
- if (t === frequencies[j][0]) throw new Error(`${t} appears more than once among frequencies`)
87
- }
88
- }
86
+ dupChecker(frequencies, (item: [T, number] | T) => (item as [T, number])[0])
89
87
  let total = 0,
90
88
  acc = 0
91
89
  for (const [, n] of frequencies) total += n
@@ -111,13 +109,37 @@ export function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: Rng) => ()
111
109
  */
112
110
  export type Rng = () => number
113
111
 
114
- // take in an array of ambiguous type and return a list of pairs, T and frequency
115
- function normalize<T>(frequencies: [T, number][] | T[]): [T, number][] {
116
- if (frequencies.length === 0) return frequencies as any as [T, number][]
117
- if ((frequencies as any).every((s: any) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number'))
118
- return frequencies as any as [T, number][]
119
- const ar = frequencies.map((t) => [t, 1])
120
- return ar as any as [T, number][]
112
+ // throw an error in case of duplicates
113
+ // it is difficult to do this efficiently because we allow T to be anything, so we can't for instance use a Set
114
+ function dupChecker<T>(frequencies: [T, number][] | T[], toT: (item: [T, number] | T) => T) {
115
+ for (let i = 0, lim = frequencies.length - 1; i < lim; i++) {
116
+ const t = toT(frequencies[i])
117
+ for (let j = i + 1; j < frequencies.length; j++) {
118
+ if (t === toT(frequencies[j])) throw new Error(`${t} appears more than once among frequencies`)
119
+ }
120
+ }
121
+ }
122
+
123
+ // if all frequencies are the same, we can make a simpler picker
124
+ function simplePicker<T>(frequencies: [T, number][] | T[]): undefined | ((rng: Rng) => () => T) {
125
+ if (frequencies.length === 0) return
126
+ if ((frequencies as any).every((s: any) => Array.isArray(s) && s.length === 2 && typeof s[1] === 'number')) {
127
+ const castFrequencies: [T, number][] = frequencies as any as [T, number][]
128
+ const [, n1] = castFrequencies[0]
129
+ if (n1 > 0 && castFrequencies.every(([, n2]) => n2 === n1)) {
130
+ const items = castFrequencies.map(([t]) => t)
131
+ dupChecker(items, (item: [T, number] | T) => item as T)
132
+ return (rng: Rng) => () => items[Math.floor(items.length * rng())]
133
+ }
134
+ } else {
135
+ const items: T[] = [...frequencies] as any as T[]
136
+ if (items.length === 1) {
137
+ const t = items[0]
138
+ return (_rng: Rng) => () => t
139
+ }
140
+ dupChecker(items, (item: [T, number] | T) => item as T)
141
+ return (rng: Rng) => () => items[Math.floor(items.length * rng())]
142
+ }
121
143
  }
122
144
 
123
145
  // remove impossible frequencies
@@ -150,6 +172,10 @@ function portion(thresholds: number[], start: number, end: number): string {
150
172
  *
151
173
  * The implementation of `rando` is taken from https://stackoverflow.com/a/47593316/15060051.
152
174
  * It is the Mulberry32 algorithm.
175
+ *
176
+ * Note, `rando` can be re-seeded by passing an optional number argument as the fresh seed (see
177
+ * example below). This can be useful if you are sharing a random number generator among
178
+ * several objects and you need to change random number sequences.
153
179
  *
154
180
  * @example
155
181
  * ```ts
@@ -160,13 +186,24 @@ function portion(thresholds: number[], start: number, end: number): string {
160
186
  * const ar: number[] = []
161
187
  * for (let i = 0; i < 3; i++) ar.push(rng())
162
188
  * // => [ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522 ]
189
+ *
190
+ * // the same seed will produce the same random number sequence
191
+ * rando(1)()
192
+ * // => 0.6270739405881613
193
+ * rando(1)()
194
+ * // => 0.6270739405881613
195
+ *
196
+ * // but you can provide a fresh seed to change the sequence
197
+ * (rando(1) as any)(2)
198
+ * // => 0.7342509443406016
163
199
  * ```
164
200
  *
165
201
  * @param {number} seed - the seed for a random number sequence
166
202
  * @returns {Rng} a generator of random numbers
167
203
  */
168
204
  export function rando(seed: number): Rng {
169
- return () => {
205
+ return (reseed?: number) => {
206
+ if (reseed !== undefined) seed = reseed
170
207
  let t = (seed += 0x6d2b79f5)
171
208
  t = Math.imul(t ^ (t >>> 15), t | 1)
172
209
  t ^= t + Math.imul(t ^ (t >>> 7), t | 61)