pick_me_too 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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