pick-me-too 1.3.0 → 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,8 @@
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
3
6
  ## v1.3.0 *2022-7-23*
4
7
  - made `rando` reseedable
5
8
  ## v1.2.1 *2022-4-30*
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pick-me-too",
3
- "version": "1.3.0",
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",
@@ -92,7 +92,7 @@ describe('README', () => {
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', () => {
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
@@ -58,11 +58,11 @@ export function pickMe<T>(frequencies: [T, number][] | T[], rando?: Rng): () =>
58
58
  *
59
59
  * const fooBarBaz = pickMeToo([["foo", 1], ["bar", 1000], ["baz", 1]])
60
60
  *
61
- * // minimum probablility generator just returns "foo"
61
+ * // minimum probablility generator; this will always return the same thing, which happens to be "foo"
62
62
  * fooBarBaz(() => 0)()
63
- * // => "foo"
63
+ * // => "bar"
64
64
  *
65
- * // maximum probability generator just returns "baz"
65
+ * // maximum probability generator; this will always return the same thing, which happens to be "baz"
66
66
  * fooBarBaz(() => 1)()
67
67
  * // => "baz"
68
68
  * ```
@@ -84,22 +84,96 @@ export function pickMeToo<T>(frequencies: [T, number][] | T[]): (rng: Rng) => ()
84
84
  return (_rng: Rng) => () => t
85
85
  }
86
86
  dupChecker(frequencies, (item: [T, number] | T) => (item as [T, number])[0])
87
- let total = 0,
88
- acc = 0
89
- for (const [, n] of frequencies) total += n
90
- for (let i = 0; i < frequencies.length; i++) {
91
- const [, n] = frequencies[i]
92
- acc += n
93
- 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
94
140
  }
95
- const items: T[] = [],
96
- thresholds: number[] = []
97
- for (const [t, n] of frequencies) {
98
- items.push(t)
99
- thresholds.push(n)
141
+ if (n.right) {
142
+ acc += n.interval
143
+ n.rightThreshold = acc
144
+ addThresholds(n.right, acc)
100
145
  }
101
- const f = thresholder(thresholds)
102
- 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))
103
177
  }
104
178
 
105
179
  /**
@@ -150,21 +224,6 @@ function sanitize<T>(frequencies: [T, number][]): [T, number][] {
150
224
  ][]
151
225
  }
152
226
 
153
- // make a function that converts a random number into an index, preserving the frequencies
154
- // assuming the random number is from an even distribution in the interval [0, 1]
155
- function thresholder(thresholds: number[]): (n: number) => number {
156
- return eval(`(n) => ${portion(thresholds, 0, thresholds.length)}`)
157
- }
158
-
159
- // recursively convert the threshold list into nested ternary operators
160
- // this allows us to find our index in O(log(n)) comparisons
161
- function portion(thresholds: number[], start: number, end: number): string {
162
- const n = end - start
163
- if (n === 1) return start.toString()
164
- const i = start + (n % 2 === 0 ? n / 2 - 1 : (n - 1) / 2)
165
- return `(n > ${thresholds[i]} ? ${portion(thresholds, i + 1, end)} : ${portion(thresholds, start, i + 1)})`
166
- }
167
-
168
227
  /**
169
228
  * Generates a random number sequence generator. This is a demonstration generator
170
229
  * useful for testing and documentation. Any zero-argument function that returns