pick_me_too 1.0.0 → 1.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac4906dd27b93c363b8e9af9e82ffe636c3149519d79654a7b8a2f0a2bdfdf25
4
- data.tar.gz: a902f070e98dc4fab62695fe7e556e499ad33c3d1dd2b0dc21764923b37f310f
3
+ metadata.gz: 0b52966a03f64a0e479d224c5c93abed4ca6e3316ae05b88a86f1c9742da799c
4
+ data.tar.gz: 912417eb4f4c70b4d5211ed0e706f6fbd5be43b518ae4f3be8b198e52d9072b5
5
5
  SHA512:
6
- metadata.gz: 73f4816247afc780c1dae9c5399fbc1280db8795d191f575fd4829d39ea8b5f39f61974d9953a0883f25160dcd256feabefe0e75ddc323728523c9fa3a2c997c
7
- data.tar.gz: ef409f69716637d562c45b52d2ea71843e2bfb3fe568d2dfd82447084a27cd93cae9fdf99f1a4decbe37036db1639d2332fe6debfb13ed5f268c5dffae632456
6
+ metadata.gz: f9391158418a645aa5886a86934d695251e22168870c7df05946dfbc9d4a2a64253f8476574dbe44e3bf40fc6f9f9b4e6c9e1d8a466586baef29c4c729c41a03
7
+ data.tar.gz: 3e77cab8f0ce92f2ef30434ad5d45eb99003eae157aad21be287636b71ec16a1f373e3018941c8a8094882053806a1d5749667fec9ae9734a137392806a9075d
data/CHANGES.md CHANGED
@@ -1,4 +1,8 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.1.1 *2022-10-30*
4
+ * simplified, and perhaps improved, the code to compile the frequency list into a nested ternary expression; ***NOTE***: I am not considering this a breaking change, though it may change the sequence of items picked by a fixed random number sequence
5
+ ## 1.1.0 *2022-8-21*
6
+ * added the `randomize!` method
3
7
  ## 1.0.0 *2022-8-14*
4
8
  * initial release
data/README.md CHANGED
@@ -29,7 +29,7 @@ picker = PickMeToo.new({foo: 1, bar: 2, baz: 0.5}, -> { rng.rand })
29
29
  counter = Hash.new 0
30
30
  32.times { counter[picker.pick] += 1 }
31
31
  counter
32
- # => {:foo=>13, :bar=>12, :baz=>7}
32
+ # => {:bar=>15, :foo=>12, :baz=>5}
33
33
 
34
34
  # you don't need to provide your own random number sequence
35
35
  picker = PickMeToo({a: 1, b: 2, c: 3})
@@ -47,7 +47,7 @@ of frequencies:
47
47
 
48
48
  What you need is something that will randomly pick these things for you with the frequencies you specify.
49
49
 
50
- One way to do this would be to make an array, filling it with the items according to the frequencies specified
50
+ One way to do this would be to make an array, fill it with the items according to the frequencies specified,
51
51
  and then pick randomly from the array:
52
52
 
53
53
  ```ruby
@@ -72,7 +72,7 @@ This is the "[urn](https://en.wikipedia.org/wiki/Urn_problem)" containing the it
72
72
 
73
73
  "Fill" the urn.
74
74
 
75
- The required `frequencies` parameter must be something that is effectivly a list of pairs:
75
+ The required `frequencies` parameter must be something that is effectively a list of pairs:
76
76
  things to pick paired with their frequency. The "frequency" is just any positive number.
77
77
 
78
78
  The optional `rnd` parameter is a `Proc` that when called returns a number, ideally in the interval
@@ -88,6 +88,18 @@ This constructor method will raise a `PickMeToo::Error` if
88
88
 
89
89
  Draw an item from the urn.
90
90
 
91
+ ## `PickMeToo#randomize!([rnd])`
92
+
93
+ Replace the random number generator.
94
+ If the optional argument is omitted, the replacement is just
95
+
96
+ ```ruby
97
+ -> { rand }
98
+ ```
99
+
100
+ This is useful if you want to switch from a seeded random number generator
101
+ to something more truly random.
102
+
91
103
  # Installation
92
104
 
93
105
  `pick_me_too` is available as a gem, so one installs it as one does gems.
data/lib/pick_me_too.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ##
4
4
  # An "urn" from which you can pick things with specified frequencies.
5
- #
5
+ #
6
6
  # require 'pick_me_too'
7
7
  #
8
8
  # wandering_monsters = PickMeToo.new({goblin: 10, bugbear: 2, orc: 5, spider: 3, troll: 1})
@@ -17,14 +17,14 @@
17
17
  # probability the next thing you pick is also a cat, and the urn will never be picked empty. (And of course
18
18
  # this is all a metaphor.)
19
19
  class PickMeToo
20
- VERSION = '1.0.0'
20
+ VERSION = '1.1.1'
21
21
 
22
22
  class Error < StandardError; end
23
23
 
24
24
  ##
25
25
  # "Fill" the urn.
26
26
  #
27
- # The required frequencies parameter must be something that is effectivly a list of pairs:
27
+ # The required frequencies parameter must be something that is effectively a list of pairs:
28
28
  # things to pick paired with their frequency. The "frequency" is just any positive number.
29
29
  #
30
30
  # The optional rnd parameter is a Proc that when called returns a number, ideally in the interval
@@ -42,11 +42,9 @@ class PickMeToo
42
42
  if @objects.length == 1
43
43
  @picker = ->(_p) { 0 }
44
44
  else
45
- frequencies = frequencies.map(&:last)
46
- balanced_binary_tree = bifurcate(frequencies.dup)
47
- probability_tree = probabilities(frequencies, balanced_binary_tree)
45
+ root = balanced_binary_tree(frequencies)
48
46
  # compile everything into a nested ternary expression
49
- @picker = eval "->(p) { #{ternerize(probability_tree)} }"
47
+ @picker = eval "->(p) { #{ternarize(root, 0)} }"
50
48
  end
51
49
  end
52
50
 
@@ -56,6 +54,19 @@ class PickMeToo
56
54
  @objects[@picker.call(@rnd.call)]
57
55
  end
58
56
 
57
+ ##
58
+ # Replace the random number generator.
59
+ #
60
+ # If the optional argument is omitted, the replacement is just
61
+ #
62
+ # -> { rand }
63
+ #
64
+ # This is useful if you want to switch from a seeded random number generator
65
+ # to something more truly random.
66
+ def randomize!(rnd = -> { rand })
67
+ @rnd = rnd
68
+ end
69
+
59
70
  private
60
71
 
61
72
  # sanity check and normalization of frequencies
@@ -69,94 +80,49 @@ class PickMeToo
69
80
  raise Error, "the following have non-positive frequencies: #{bad.inspect}" if bad.any?
70
81
 
71
82
  total = good.map(&:last).sum.to_f
72
- good.map { |o, n| [o, n / total] }
73
- end
74
-
75
- # reduce the probability tree to nested ternary expressions
76
- def ternerize(ptree)
77
- p, left, right = ptree.values_at :p, :left, :right
78
- left = left.is_a?(Numeric) ? left : ternerize(left)
79
- right = right.is_a?(Numeric) ? right : ternerize(right)
80
- "(p > #{p} ? #{right} : #{left})"
83
+ # sort by size of probability interval -- optimization step
84
+ good.sort_by(&:last).reverse.map { |o, n| [o, n / total] }
81
85
  end
82
86
 
83
- def probabilities(frequencies, tree)
84
- tree = sum_probabilities(tree, 0)
85
- replace_frequencies_with_indices(tree, frequencies.each_with_index.to_a)
86
- tree
87
- end
87
+ # treat the frequencies as a heap
88
+ # returns the root of this binary tree
89
+ def balanced_binary_tree(frequencies)
90
+ frequencies = frequencies.each_with_index.map { |(*, i), idx| { interval: i, index: idx } }
91
+ frequencies.each do |obj|
92
+ left_idx = obj[:index] * 2 + 1
93
+ next unless (left = frequencies[left_idx])
88
94
 
89
- def replace_frequencies_with_indices(tree, frequencies)
90
- left, right = tree.values_at :left, :right
91
- if left.is_a?(Numeric)
92
- i = frequencies.index { |v,| v == left }
93
- *, i = frequencies.slice!(i)
94
- tree[:left] = i
95
- else
96
- replace_frequencies_with_indices(left, frequencies)
97
- end
98
- if right.is_a?(Numeric)
99
- i = frequencies.index { |v,| v == right }
100
- *, i = frequencies.slice!(i)
101
- tree[:right] = i
102
- else
103
- replace_frequencies_with_indices(right, frequencies)
95
+ obj[:left] = left
96
+ right_idx = left_idx + 1
97
+ if (right = frequencies[right_idx])
98
+ obj[:right] = right
99
+ end
104
100
  end
101
+ frequencies[0]
105
102
  end
106
103
 
107
- # convert the frequency numbers to probabilities
108
- def sum_probabilities(tree, base)
109
- left, right = tree
110
- p = left.flatten.sum + base
111
- left = left.length == 1 ? left.first : sum_probabilities(left, base)
112
- right = right.length == 1 ? right.first : sum_probabilities(right, p)
113
- { p: p, left: left, right: right }
114
- end
104
+ # what is the sum of all intervals under this node?
105
+ def sum(obj)
106
+ return 0 unless obj
115
107
 
116
- # distribute the frequencies so their as balanced as possible
117
- # the better to reduce expected length of the binary search
118
- def bifurcate(nums)
119
- return nums if nums.length < 2
120
-
121
- max = total = 0
122
- max_index = -1
123
- # make one loop find all these things
124
- nums.each_with_index do |n, i|
125
- total += n
126
- if n > max
127
- max = n
128
- max_index = i
129
- end
130
- end
131
- half = total / 2.0
132
- right = [nums.slice!(max_index)]
133
- if max >= half
134
- [bifurcate(nums), right]
135
- else
136
- gap = half - max
137
- while rv = fit_gap(gap, nums)
138
- removed, remaining_gap = rv
139
- right << removed
140
- break unless gap = remaining_gap
141
- end
142
- [bifurcate(nums), bifurcate(right)]
108
+ obj[:sum] ||= begin
109
+ left = sum(obj[:left])
110
+ right = sum(obj[:right])
111
+ left + right + obj[:interval]
143
112
  end
144
113
  end
145
114
 
146
- # look for the frequency best suited to balance the two branches
147
- def fit_gap(gap, nums)
148
- best_index = 0
149
- best_fit = (gap - nums[0]).abs
150
- nums.each_with_index.drop(1).each do |n, i|
151
- fit = (gap - n).abs
152
- if fit < best_fit
153
- best_index = i
154
- best_fit = fit
155
- end
156
- end
157
- if nums[best_index] < gap * 2
158
- n = nums.slice!(best_index)
159
- [n, n < gap ? gap - n : nil]
115
+ # reduce the probability tree to nested ternary expressions
116
+ def ternarize(node, acc)
117
+ left = sum(node[:left])
118
+ return node[:index] if left == 0 # this is a leaf
119
+
120
+ right = if (r = node[:right])
121
+ increment = acc + left + node[:interval]
122
+ "(p < #{increment} ? #{node[:index]} : #{ternarize(r, increment)})"
123
+ else
124
+ node[:index]
160
125
  end
126
+ "(p < #{left + acc} ? #{ternarize(node[:left], acc)} : #{right})"
161
127
  end
162
128
  end
data/pick_me_too.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.require_paths = ['lib']
26
26
 
27
27
  s.add_development_dependency 'bundler', '~> 1.7'
28
- s.add_development_dependency 'byebug', '~> 9.1.0'
28
+ s.add_development_dependency 'byebug', '~> 9.1', '>= 9.1.0'
29
29
  s.add_development_dependency 'json', '~> 2'
30
30
  s.add_development_dependency 'minitest', '~> 5'
31
31
  s.add_development_dependency 'rake', '~> 10.0'
data/test/basic_test.rb CHANGED
@@ -19,7 +19,7 @@ class BasicTest < Minitest::Test
19
19
 
20
20
  def test_hash
21
21
  rnd = Random.new 1
22
- picker = PickMeToo.new({'cat' => 2, 'dog' => 1}, -> { rnd.rand })
22
+ picker = PickMeToo.new({ 'cat' => 2, 'dog' => 1 }, -> { rnd.rand })
23
23
  counter = Hash.new(0)
24
24
  3000.times { counter[picker.pick] += 1 }
25
25
  assert_equal 2, (counter['cat'] / 1000.0).round, 'right number of cats'
@@ -70,4 +70,19 @@ class BasicTest < Minitest::Test
70
70
  PickMeToo.new([['foo', nil]])
71
71
  end
72
72
  end
73
+
74
+ def test_randomize
75
+ rnd1 = Random.new 1
76
+ rnd2 = Random.new 1
77
+ rnd3 = Random.new 2
78
+ picker1 = PickMeToo.new({ foo: 1, bar: 2, baz: 3 }, -> { rnd1.rand })
79
+ picker2 = PickMeToo.new({ foo: 1, bar: 2, baz: 3 }, -> { rnd2.rand })
80
+ ar1 = Array.new(100) { picker1.pick }
81
+ ar2 = Array.new(100) { picker2.pick }
82
+ assert_equal ar1, ar2, 'with the same seeds we get the same sequences'
83
+ picker2.randomize! -> { rnd3.rand }
84
+ ar1 = Array.new(100) { picker1.pick }
85
+ ar2 = Array.new(100) { picker2.pick }
86
+ refute_equal ar1, ar2, 'if we randomize a picker, we get a new sequence'
87
+ end
73
88
  end
@@ -21,6 +21,6 @@ class BasicTest < Minitest::Test
21
21
  picker = PickMeToo.new({ foo: 1, bar: 2, baz: 0.5 }, -> { rng.rand })
22
22
  counter = Hash.new 0
23
23
  32.times { counter[picker.pick] += 1 }
24
- assert_equal({ foo: 13, bar: 12, baz: 7 }, counter)
24
+ assert_equal({:bar=>15, :foo=>12, :baz=>5}, counter)
25
25
  end
26
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pick_me_too
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David F. Houghton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-14 00:00:00.000000000 Z
11
+ date: 2022-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -29,6 +29,9 @@ dependencies:
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '9.1'
34
+ - - ">="
32
35
  - !ruby/object:Gem::Version
33
36
  version: 9.1.0
34
37
  type: :development
@@ -36,6 +39,9 @@ dependencies:
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
41
  - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '9.1'
44
+ - - ">="
39
45
  - !ruby/object:Gem::Version
40
46
  version: 9.1.0
41
47
  - !ruby/object:Gem::Dependency