loaded_die 1.0.1 → 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.
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2018 Aaron Beckerman
1
+ Copyright (c) 2024 Aaron Beckerman
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
data/README.txt ADDED
@@ -0,0 +1,47 @@
1
+ Loaded Die
2
+
3
+ Loaded Die is a Ruby library that makes it easy to randomly choose from a set
4
+ of options where some options are more likely than others.
5
+
6
+
7
+ Usage
8
+
9
+ This is the basic pattern: First, you load the library. Then you create an
10
+ instance of LoadedDie::Sampler and send the sample message to it as many
11
+ times as you like.
12
+
13
+ Let's say you want to choose randomly from three strings: "A", "B", and "C".
14
+ You want "A" and "B" to be equally likely, and "C" to be twice as likely as one
15
+ of them. That means "A" should be 25% likely, B should be 25% likely, and C
16
+ should be 50% likely.
17
+
18
+ You can create your sampler like this:
19
+
20
+ require "loaded_die"
21
+ sampler = LoadedDie::Sampler.new({ "A" => 0.25, "B" => 0.25, "C" => 0.5 })
22
+
23
+ And then sample from it like this:
24
+
25
+ sampler.sample
26
+
27
+ But you don't need to be fussy about how you represent probabilities. That is,
28
+ you don't need to represent them as numbers between zero and one such that they
29
+ sum to one. Their relative values are what matter. Furthermore, they don't need
30
+ to be floats, just objects that convert to floats. So you could also create
31
+ your sampler like this:
32
+
33
+ sampler = LoadedDie::Sampler.new({ "A" => 1, "B" => 1, "C" => 2 })
34
+
35
+ You can specify the random number generator that sample uses. To do this,
36
+ supply as the argument a hash with your random number generator under the
37
+ :random key. (This is based on the behavior of Ruby's Array#sample.) Your
38
+ random number generator will be sent a rand message with one argument, the sum
39
+ of the weights you specified when creating the sampler. The returned object
40
+ must be a float greater than or equal to zero and less than the sum of the
41
+ weights.
42
+
43
+ rng = Object.new
44
+ def rng.rand(n)
45
+ 0.0
46
+ end
47
+ sampler.sample({ :random => rng })
data/lib/loaded_die.rb CHANGED
@@ -1,52 +1,98 @@
1
+ ##
2
+ # This module allows you to choose randomly from a set of options where some
3
+ # options are more likely than others.
4
+
1
5
  module LoadedDie
2
- VERSION = '1.0.1'
6
+
7
+ ##
8
+ # Instances of this class can choose randomly from a set of options (called
9
+ # individuals here). The options can have different probabilities of being
10
+ # chosen.
3
11
 
4
12
  class Sampler
13
+
14
+ ##
15
+ # The default random number generator. It samples from a uniform
16
+ # distribution.
17
+
5
18
  DEFAULT_RNG = ::Object.new
6
- def DEFAULT_RNG.rand(n)
19
+ def DEFAULT_RNG.rand n
7
20
  ::Kernel.rand * n
8
21
  end
9
22
 
10
- Segment = ::Struct.new(:maximum, :individual)
23
+ ##
24
+ # Creates a sampler from a population. The argument should return an
25
+ # enumerable (conventionally an instance of Enumerator) of arrays when sent
26
+ # a to_enum message with no arguments. (A normal instance of Hash
27
+ # qualifies.) Each array must have at least two elements: the first element
28
+ # is an individual that can be chosen and the second element is its weight
29
+ # -- that is, the likelihood relative to the other weights that the
30
+ # individual will be chosen. Weights must convert to finite floats that are
31
+ # zero or positive.
11
32
 
12
- def initialize(population)
13
- @compiled = population.inject [] do |accum, (individual, weight)|
14
- if weight <= 0
15
- raise ::ArgumentError, "non-positive weight #{weight}"
33
+ def initialize population
34
+ @compiled = population.to_enum.inject [] do |accum, elem|
35
+ individual = elem.fetch 0
36
+ weight = elem.fetch 1
37
+ weight_f = ::Kernel.Float weight
38
+ unless 0.0 <= weight_f && weight_f.finite?
39
+ ::Kernel.raise ::ArgumentError, "invalid weight #{weight_f}"
40
+ break
16
41
  end
17
42
  prev_max =
18
43
  if last = accum.last
19
- last.maximum
44
+ last.fetch :maximum
20
45
  else
21
- 0
46
+ 0.0
22
47
  end
23
- accum << Segment.new(prev_max + weight, individual)
48
+ accum << { :maximum => prev_max + weight_f, :individual => individual }
24
49
  end
25
50
  nil
26
51
  end
27
52
 
53
+ ##
54
+ # Returns the sum of weights.
55
+
28
56
  def length
29
57
  if last = @compiled.last
30
- last.maximum
58
+ last.fetch :maximum
31
59
  else
32
- 0
60
+ 0.0
33
61
  end
34
62
  end
35
63
 
36
- def [](point)
37
- if point < 0
64
+ ##
65
+ # Returns the individual associated with the given point on a line. The
66
+ # point should convert to a float. If it is greater than or equal to zero
67
+ # and less than the sum of weights, this returns the corresponding
68
+ # individual. If it is outside those bounds, this returns nil.
69
+
70
+ def [] point
71
+ point_f = ::Kernel.Float point
72
+ if 0.0 > point_f
38
73
  nil
39
- elsif choice = @compiled.detect { |segment| point < segment.maximum }
40
- choice.individual
74
+ elsif choice = @compiled.detect { |segment|
75
+ segment.fetch(:maximum) > point_f }
76
+ choice.fetch :individual
41
77
  else
42
78
  nil
43
79
  end
44
80
  end
45
81
 
46
- def sample(options = {})
47
- rng = options.fetch(:random) { DEFAULT_RNG }
48
- point = rng.rand(length)
82
+ ##
83
+ # Returns a randomly-chosen individual. The argument is a hash, empty by
84
+ # default. If it contains a value for the +:random+ key, that value will be
85
+ # used as the random number generator instead of the default. This RNG will
86
+ # be sent a rand message with one argument, the sum of weights, and should
87
+ # return a number (convertible to a float) greater than or equal to zero
88
+ # and less than the sum of weights.
89
+
90
+ def sample options = {}
91
+ rng = options.fetch(:random) { ::LoadedDie::Sampler::DEFAULT_RNG }
92
+ point = rng.rand length
49
93
  self[point]
50
94
  end
95
+
51
96
  end
97
+
52
98
  end
data/loaded_die.gemspec CHANGED
@@ -1,19 +1,12 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "loaded_die"
5
-
6
1
  Gem::Specification.new do |s|
7
2
  s.name = "loaded_die"
8
- s.version = LoadedDie::VERSION
9
- s.author = "Aaron Beckerman"
10
- s.summary = %q{A library for choosing randomly when some options are more likely than others.}
11
- s.description = %q{Loaded Die makes it easy to choose randomly when some options are more likely than others.}
12
- s.license = "MIT"
13
- s.platform = Gem::Platform::RUBY
3
+ s.version = "2.0.0"
4
+ s.authors = ["Aaron Beckerman"]
5
+ s.summary = "A library for choosing randomly where some options are more" \
6
+ " likely than others."
7
+ s.licenses = ["MIT"]
14
8
  s.required_ruby_version = ">= 1.8.7"
15
- s.files = ["LICENSE.txt", "README.rdoc", "loaded_die.gemspec", "lib/loaded_die.rb", "test/loaded_die_test.rb"]
9
+ s.files = ["LICENSE.txt", "README.txt", "loaded_die.gemspec",
10
+ "lib/loaded_die.rb", "test/loaded_die_test.rb"]
16
11
  s.test_files = ["test/loaded_die_test.rb"]
17
- s.executables = []
18
- s.require_paths = ["lib"]
19
12
  end
@@ -1,71 +1,195 @@
1
- require 'loaded_die'
1
+ # Sections that create local variables are in class definitions so that later
2
+ # sections cannot accidentally reference those local variables.
2
3
 
3
- String == LoadedDie::VERSION.class or fail
4
+ require "loaded_die"
4
5
 
5
- Module == LoadedDie.class or fail
6
+ # LoadedDie should be an instance of Module.
7
+ fail "" unless Module.equal? LoadedDie.class
6
8
 
7
- Class == LoadedDie::Sampler.class or fail
9
+ # LoadedDie::Sampler should be a class.
10
+ fail "" unless Class.equal? LoadedDie::Sampler.class
8
11
 
12
+ # LoadedDie::Sampler::DEFAULT_RNG.rand should return a float in the expected
13
+ # range.
14
+ class ::Object
15
+ n = LoadedDie::Sampler::DEFAULT_RNG.rand 0.5
16
+ fail "" unless Float.equal? n.class
17
+ fail "" unless 0.0 <= n && 0.5 > n
18
+ end
19
+
20
+ # LoadedDie::Sampler.new with an empty population should return an object of
21
+ # class LoadedDie::Sampler.
22
+ fail "" unless LoadedDie::Sampler.equal? LoadedDie::Sampler.new({}).class
23
+
24
+ # LoadedDie::Sampler.new with only zero or positive finite weights should
25
+ # return an instance of LoadedDie::Sampler.
26
+ fail "" unless LoadedDie::Sampler.equal?(
27
+ LoadedDie::Sampler.new({ :a => 0.0, :b => 1.0 }).class)
28
+
29
+ # LoadedDie::Sampler.new with a negative weight should raise an exception.
30
+ begin
31
+ LoadedDie::Sampler.new({ :a => -1.0 })
32
+ rescue ArgumentError
33
+ fail "" unless /\Ainvalid weight / =~ $!.message
34
+ else
35
+ fail ""
36
+ end
37
+
38
+ # LoadedDie::Sampler.new with a NaN weight should raise an exception.
39
+ begin
40
+ LoadedDie::Sampler.new({ :a => 0.0 / 0.0 })
41
+ rescue ArgumentError
42
+ fail "" unless /\Ainvalid weight / =~ $!.message
43
+ else
44
+ fail ""
45
+ end
46
+
47
+ # LoadedDie::Sampler.new with an infinite weight should raise an exception.
9
48
  begin
10
- LoadedDie::Sampler.new(:a => 0)
49
+ LoadedDie::Sampler.new({ :a => 1.0 / 0.0 })
11
50
  rescue ArgumentError
51
+ fail "" unless /\Ainvalid weight / =~ $!.message
52
+ else
53
+ fail ""
54
+ end
55
+
56
+ # LoadedDie::Sampler.new with a weight that does not convert to a float should
57
+ # raise an exception.
58
+ begin
59
+ LoadedDie::Sampler.new({ :a => nil })
60
+ rescue TypeError
61
+ else
62
+ fail ""
63
+ end
64
+
65
+ # LoadedDie::Sampler.new with a population that doesn't conform to the
66
+ # interface should raise an exception.
67
+ begin
68
+ LoadedDie::Sampler.new nil
69
+ rescue NoMethodError
70
+ else
71
+ fail ""
72
+ end
73
+ begin
74
+ LoadedDie::Sampler.new [[]]
75
+ rescue IndexError
76
+ else
77
+ fail ""
78
+ end
79
+ begin
80
+ LoadedDie::Sampler.new [[1.0]]
81
+ rescue IndexError
12
82
  else
13
- fail
83
+ fail ""
14
84
  end
15
85
 
86
+ # Sending new with superfluous non-block arguments to LoadedDie::Sampler should
87
+ # raise an exception.
16
88
  begin
17
- LoadedDie::Sampler.new(:a => -1)
89
+ LoadedDie::Sampler.new({}, nil)
18
90
  rescue ArgumentError
19
91
  else
20
- fail
92
+ fail ""
21
93
  end
22
94
 
23
- lambda do
95
+ # A sampler should say it responds to the expected messages.
96
+ class ::Object
24
97
  sampler = LoadedDie::Sampler.new({})
25
- sampler.respond_to?(:length) or fail
26
- sampler.respond_to?(:[]) or fail
27
- sampler.respond_to?(:sample) or fail
28
- end.call
29
-
30
- lambda do
31
- 0 == LoadedDie::Sampler.new({}).length or fail
32
- 2 == LoadedDie::Sampler.new({ :a => 2 }).length or fail
33
- (2 + 3.1) == LoadedDie::Sampler.new({ :a => 2, :b => 3.1 }).length or fail
34
- end.call
35
-
36
- lambda do
98
+ fail "" unless sampler.respond_to? :length
99
+ fail "" unless sampler.respond_to? :[]
100
+ fail "" unless sampler.respond_to? :sample
101
+ end
102
+
103
+ # Sampler length should be the sum of weights converted to floats.
104
+ fail "" unless 0.0 == LoadedDie::Sampler.new([]).length
105
+ fail "" unless 2.0 == LoadedDie::Sampler.new([[:a, 2.0]]).length
106
+ fail "" unless (Float(2) + 0.0 + 3.1) ==
107
+ LoadedDie::Sampler.new([[:a, 2], [:b, 0.0], [:c, 3.1]]).length
108
+
109
+ # Sending length with superfluous non-block arguments to a sampler should raise
110
+ # an exception.
111
+ begin
112
+ LoadedDie::Sampler.new({}).length nil
113
+ rescue ArgumentError
114
+ else
115
+ fail ""
116
+ end
117
+
118
+ # An empty sampler should always find nil when queried.
119
+ class ::Object
37
120
  sampler = LoadedDie::Sampler.new({})
38
- nil == sampler[0.42] or fail
39
- end.call
40
-
41
- lambda do
42
- sampler = LoadedDie::Sampler.new([[:a, 3], [:b, 2.0], [:c, 5]])
43
- nil == sampler[-1.0] or fail
44
- :a == sampler[0.0] or fail
45
- :a == sampler[2.99] or fail
46
- :b == sampler[3] or fail
47
- :b == sampler[3.0] or fail
48
- :b == sampler[4.99] or fail
49
- :c == sampler[5.0] or fail
50
- :c == sampler[9.99] or fail
51
- nil == sampler[10.0] or fail
52
- nil == sampler[11.0] or fail
53
- end.call
54
-
55
- lambda do
121
+ fail "" unless nil.equal? sampler[0.42]
122
+ end
123
+
124
+ # A sampler should find the expected segment or nil when queried.
125
+ class ::Object
126
+ sampler = LoadedDie::Sampler.new [[:a, 3], ["!", 0.0], [:b, 2.0], [:c, 5]]
127
+ fail "" unless nil.equal? sampler[-1.0]
128
+ fail "" unless :a.equal? sampler[0.0]
129
+ fail "" unless :a.equal? sampler[2.9]
130
+ fail "" unless :b.equal? sampler[3.0]
131
+ fail "" unless :b.equal? sampler[4.9]
132
+ fail "" unless :c.equal? sampler[5.0]
133
+ fail "" unless :c.equal? sampler[9.9]
134
+ fail "" unless nil.equal? sampler[10.0]
135
+ fail "" unless nil.equal? sampler[11.0]
136
+ fail "" unless nil.equal? sampler[0.0 / 0.0] # NaN
137
+ fail "" unless nil.equal? sampler[1.0 / 0.0] # infinity
138
+
139
+ # This non-float should be converted to a float.
140
+ fail "" unless :b.equal? sampler[4]
141
+
142
+ # An exception should be raised when the argument cannot be converted to a
143
+ # float.
144
+ begin
145
+ sampler[nil]
146
+ rescue TypeError
147
+ else
148
+ fail ""
149
+ end
150
+ end
151
+
152
+ # Sending [] with superfluous non-block arguments to a sampler should raise an
153
+ # exception.
154
+ begin
155
+ LoadedDie::Sampler.new({})[0.0, nil]
156
+ rescue ArgumentError
157
+ else
158
+ fail ""
159
+ end
160
+
161
+ # An empty sampler should return nil when sampled with the default
162
+ # random number generator.
163
+ class ::Object
56
164
  sampler = LoadedDie::Sampler.new({})
57
- nil == sampler.sample or fail
58
- end.call
165
+ fail "" unless nil.equal? sampler.sample
166
+ end
59
167
 
60
- lambda do
61
- sampler = LoadedDie::Sampler.new([[:a, 3], [:b, 2.0], [:c, 9001]])
168
+ # A sampler should return the expected individual when the random number
169
+ # generator is faked and it should return something reasonable when sampled
170
+ # with the default random number generator.
171
+ class ::Object
172
+ sampler = LoadedDie::Sampler.new [[:a, 3], [:b, 2.0], ["!", 0.0], [:c, 9001]]
62
173
  rng = Object.new
63
- def rng.rand(n)
64
- (3 + 2.0 + 9001) == n or fail
65
- 4.99
174
+ def rng.rand n
175
+ fail "" unless ((Float(3) + 2.0) + Float(9001)) == n
176
+ 4.9
66
177
  end
67
- :b == sampler.sample(:random => rng) or fail
68
- [:a, :b, :c].include?(sampler.sample) or fail
69
- end.call
178
+ fail "" unless :b.equal? sampler.sample({ :random => rng })
179
+ fail "" unless [:a, :b, :c].include? sampler.sample
180
+ # Unknown options should be ignored.
181
+ fail "" unless :b.equal? sampler.sample({ :random => rng, "?" => "?" })
182
+ fail "" unless [:a, :b, :c].include? sampler.sample({ "?" => "?" })
183
+ end
184
+
185
+ # Sending sample with superfluous non-block arguments to a sampler should raise
186
+ # an exception.
187
+ begin
188
+ LoadedDie::Sampler.new({}).sample({}, nil)
189
+ rescue ArgumentError
190
+ else
191
+ fail ""
192
+ end
70
193
 
71
- puts 'Test finished.'
194
+ # If this message doesn't get written, there was a problem.
195
+ puts "Test finished: #{__FILE__}"
metadata CHANGED
@@ -1,50 +1,73 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: loaded_die
3
- version: !ruby/object:Gem::Version
4
- version: 1.0.1
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease:
6
+ segments:
7
+ - 2
8
+ - 0
9
+ - 0
10
+ version: 2.0.0
5
11
  platform: ruby
6
- authors:
12
+ authors:
7
13
  - Aaron Beckerman
8
14
  autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
- date: 2018-05-31 00:00:00.000000000 Z
17
+
18
+ date: 2024-03-12 00:00:00 -07:00
19
+ default_executable:
12
20
  dependencies: []
13
- description: Loaded Die makes it easy to choose randomly when some options are more
14
- likely than others.
21
+
22
+ description:
15
23
  email:
16
24
  executables: []
25
+
17
26
  extensions: []
27
+
18
28
  extra_rdoc_files: []
19
- files:
29
+
30
+ files:
20
31
  - LICENSE.txt
21
- - README.rdoc
22
- - lib/loaded_die.rb
32
+ - README.txt
23
33
  - loaded_die.gemspec
34
+ - lib/loaded_die.rb
24
35
  - test/loaded_die_test.rb
36
+ has_rdoc: true
25
37
  homepage:
26
- licenses:
38
+ licenses:
27
39
  - MIT
28
- metadata: {}
29
40
  post_install_message:
30
41
  rdoc_options: []
31
- require_paths:
42
+
43
+ require_paths:
32
44
  - lib
33
- required_ruby_version: !ruby/object:Gem::Requirement
34
- requirements:
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
35
48
  - - ">="
36
- - !ruby/object:Gem::Version
49
+ - !ruby/object:Gem::Version
50
+ hash: 57
51
+ segments:
52
+ - 1
53
+ - 8
54
+ - 7
37
55
  version: 1.8.7
38
- required_rubygems_version: !ruby/object:Gem::Requirement
39
- requirements:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
40
59
  - - ">="
41
- - !ruby/object:Gem::Version
42
- version: '0'
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
43
65
  requirements: []
66
+
44
67
  rubyforge_project:
45
- rubygems_version: 2.7.6
68
+ rubygems_version: 1.6.2
46
69
  signing_key:
47
- specification_version: 4
48
- summary: A library for choosing randomly when some options are more likely than others.
49
- test_files:
70
+ specification_version: 3
71
+ summary: A library for choosing randomly where some options are more likely than others.
72
+ test_files:
50
73
  - test/loaded_die_test.rb
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA256:
3
- metadata.gz: 362b5a5dc03dcbf3580aeb5501c964cb273a183ce9ab3763bc9840288f4e4b95
4
- data.tar.gz: e5e901d9a0611c2579d33595569658d112736f89714655db4b161349c5f00d24
5
- SHA512:
6
- metadata.gz: 5510423a7c21354514d3a035f77b7c518d2ed563ac315dc1d1ebba6f10459201416a5ed427cadb9c78c957a94a591a9766c75dabe8b15ca96911e8aed413350b
7
- data.tar.gz: 5d8ccf6c0f9713cf6e1d3bf1b45ba63d7c026d7e41523e0c61211163815173cdeff20341b5c4518350674ecc0f67565de16f29dd1f063a7dc2d347e9084c01f7
data/README.rdoc DELETED
@@ -1,57 +0,0 @@
1
- = Loaded Die
2
-
3
- Loaded Die is a Ruby library that makes it easy to randomly choose from a set
4
- of options when some options are more likely than others.
5
-
6
-
7
- == Usage
8
-
9
- This is the basic pattern: First, you load the library. Then you create an
10
- instance of LoadedDie::Sampler and send the "sample" message to it as many
11
- times as you like.
12
-
13
- Let's say you want to choose randomly from three strings: "A", "B", and "C".
14
- You want "A" and "B" to be equally likely, and "C" to be twice as likely as one
15
- of them. That means "A" should be 25% likely, B should be 25% likely, and C
16
- should be 50% likely.
17
-
18
- You can create your sampler like this:
19
-
20
- require 'loaded_die'
21
- sampler = LoadedDie::Sampler.new('A' => 0.25, 'B' => 0.25, 'C' => 0.5)
22
-
23
- And then sample from it like this:
24
-
25
- sampler.sample
26
-
27
- But you don't have to be fussy about how you represent probabilities. That is,
28
- you don't need to represent them as numbers between 0 and 1 such that they sum
29
- up to 1. Their relative values are what matter. So you could also create your
30
- sampler like this:
31
-
32
- sampler = LoadedDie::Sampler.new('A' => 1, 'B' => 1, 'C' => 2)
33
-
34
- You can specify the random number generator that sample uses. To do this,
35
- supply as the argument a hash with your random number generator under the
36
- :random key. (This is based on the behavior of Ruby's Array#sample.) Your
37
- random number generator will be sent the "rand" message with one argument, the
38
- sum of the weights you specified when creating the sampler. The return value
39
- must be a number greater than or equal to zero and less than the sum of the
40
- weights.
41
-
42
- rng = Object.new
43
- def rng.rand(n)
44
- 0
45
- end
46
- sampler.sample(:random => rng)
47
-
48
-
49
- == Author
50
-
51
- This was written by Aaron Beckerman.
52
-
53
-
54
- == Copyright
55
-
56
- This code is distributed under the MIT License (also known as the Expat
57
- License). See the LICENSE.txt file for details.