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 +3 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/documentation.test.ts +3 -3
- package/src/index.ts +92 -33
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
package/package.json
CHANGED
|
@@ -92,7 +92,7 @@ describe('README', () => {
|
|
|
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', () => {
|
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
|
|
@@ -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
|
|
61
|
+
* // minimum probablility generator; this will always return the same thing, which happens to be "foo"
|
|
62
62
|
* fooBarBaz(() => 0)()
|
|
63
|
-
* // => "
|
|
63
|
+
* // => "bar"
|
|
64
64
|
*
|
|
65
|
-
* // maximum probability generator
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
thresholds.push(n)
|
|
141
|
+
if (n.right) {
|
|
142
|
+
acc += n.interval
|
|
143
|
+
n.rightThreshold = acc
|
|
144
|
+
addThresholds(n.right, acc)
|
|
100
145
|
}
|
|
101
|
-
|
|
102
|
-
|
|
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
|