loaded_die 2.0.1 → 2.0.3

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) 2015-2025 Aaron Beckerman
1
+ Copyright (c) 2015-2026 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 CHANGED
@@ -1,7 +1,8 @@
1
1
  Loaded Die
2
2
 
3
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.
4
+ of options where some options are more likely than others. It's written for
5
+ clarity over performance.
5
6
 
6
7
 
7
8
  Usage
@@ -12,7 +13,7 @@ times as you like.
12
13
 
13
14
  Let's say you want to choose randomly from three strings: "A", "B", and "C".
14
15
  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
+ of them. That means "A" should be 25% likely, "B" should be 25% likely, and "C"
16
17
  should be 50% likely.
17
18
 
18
19
  You can create your sampler like this:
data/lib/loaded_die.rb CHANGED
@@ -28,7 +28,7 @@ module LoadedDie
28
28
  # is an individual that can be chosen and the second element is its weight
29
29
  # -- that is, the likelihood relative to the other weights that the
30
30
  # individual will be chosen. Weights must convert to finite floats that are
31
- # zero or positive.
31
+ # zero or positive. Their sum must also be finite.
32
32
 
33
33
  def initialize population
34
34
  @compiled = population.to_enum.inject [] do |accum, elem|
@@ -45,7 +45,12 @@ module LoadedDie
45
45
  else
46
46
  0.0
47
47
  end
48
- accum << { :maximum => prev_max + weight_f, :individual => individual }
48
+ maximum = prev_max + weight_f
49
+ unless maximum.finite?
50
+ ::Kernel.raise ::ArgumentError, "invalid total weight #{maximum}"
51
+ break
52
+ end
53
+ accum << { :maximum => maximum, :individual => individual }
49
54
  end
50
55
  nil
51
56
  end
@@ -80,12 +85,13 @@ module LoadedDie
80
85
  end
81
86
 
82
87
  ##
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.
88
+ # Returns a randomly-chosen individual. If the total weight is zero, this
89
+ # method returns nil. The argument is a hash, empty by default. If it
90
+ # contains a value for the +:random+ key, that value will be used as the
91
+ # random number generator instead of the default. This RNG will be sent a
92
+ # rand message with one argument, the sum of weights, and should return a
93
+ # number (convertible to a float) greater than or equal to zero and less
94
+ # than the sum of weights.
89
95
 
90
96
  def sample options = {}
91
97
  rng = options.fetch(:random) { ::LoadedDie::Sampler::DEFAULT_RNG }
@@ -26,6 +26,25 @@ fail "" unless LoadedDie::Sampler.equal? LoadedDie::Sampler.new({}).class
26
26
  fail "" unless LoadedDie::Sampler.equal?(
27
27
  LoadedDie::Sampler.new({ :a => 0.0, :b => 1.0 }).class)
28
28
 
29
+ # LoadedDie::Sampler.new should allow population elements to have more than
30
+ # two elements. It should ignore extras.
31
+ fail "" unless 1.0.eql? LoadedDie::Sampler.new([[:a, 1.0, "ignored"]]).length
32
+
33
+ # LoadedDie::Sampler.new should allow the population to be anything that
34
+ # returns an enumerable when sent a to_enum message with no arguments.
35
+ class ::Object
36
+ pop = ::Object.new
37
+ def pop.to_enum
38
+ [[:a, 1.0]]
39
+ end
40
+ fail "" unless 1.0.eql? LoadedDie::Sampler.new(pop).length
41
+ end
42
+
43
+ # LoadedDie::Sampler.new should allow weights to be objects that convert to
44
+ # floats in a conventional way.
45
+ fail "" unless 1.0.eql? LoadedDie::Sampler.new({ :a => 1 }).length
46
+ fail "" unless 1.7.eql? LoadedDie::Sampler.new({ :a => "1.7" }).length
47
+
29
48
  # LoadedDie::Sampler.new with a negative weight should raise an exception.
30
49
  begin
31
50
  LoadedDie::Sampler.new({ :a => -1.0 })
@@ -53,6 +72,16 @@ else
53
72
  fail ""
54
73
  end
55
74
 
75
+ # LoadedDie::Sampler.new with finite weights whose sum is not finite should
76
+ # raise an exception.
77
+ begin
78
+ LoadedDie::Sampler.new({ :a => Float::MAX, :b => Float::MAX })
79
+ rescue ArgumentError
80
+ fail "" unless /\Ainvalid total weight / =~ $!.message
81
+ else
82
+ fail ""
83
+ end
84
+
56
85
  # LoadedDie::Sampler.new with a weight that does not convert to a float should
57
86
  # raise an exception.
58
87
  begin
@@ -61,6 +90,12 @@ rescue TypeError
61
90
  else
62
91
  fail ""
63
92
  end
93
+ begin
94
+ LoadedDie::Sampler.new({ :a => "z" })
95
+ rescue ArgumentError
96
+ else
97
+ fail ""
98
+ end
64
99
 
65
100
  # LoadedDie::Sampler.new with a population that doesn't conform to the
66
101
  # interface should raise an exception.
@@ -101,10 +136,10 @@ class ::Object
101
136
  end
102
137
 
103
138
  # 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
139
+ fail "" unless 0.0.eql? LoadedDie::Sampler.new([]).length
140
+ fail "" unless 2.0.eql? LoadedDie::Sampler.new([[:a, 2.0]]).length
141
+ fail "" unless (Float(2) + 0.0 + 3.1).eql? LoadedDie::Sampler.new(
142
+ [[:a, 2], [:b, 0.0], [:c, 3.1]]).length
108
143
 
109
144
  # Sending length with superfluous non-block arguments to a sampler should raise
110
145
  # an exception.
@@ -172,7 +207,7 @@ class ::Object
172
207
  sampler = LoadedDie::Sampler.new [[:a, 3], [:b, 2.0], ["!", 0.0], [:c, 9001]]
173
208
  rng = Object.new
174
209
  def rng.rand n
175
- fail "" unless ((Float(3) + 2.0) + Float(9001)) == n
210
+ fail "" unless ((Float(3) + 2.0) + Float(9001)).eql? n
176
211
  4.9
177
212
  end
178
213
  fail "" unless :b.equal? sampler.sample({ :random => rng })
metadata CHANGED
@@ -1,13 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loaded_die
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
5
- prerelease:
6
- segments:
7
- - 2
8
- - 0
9
- - 1
10
- version: 2.0.1
4
+ version: 2.0.3
11
5
  platform: ruby
12
6
  authors:
13
7
  - Aaron Beckerman
@@ -15,7 +9,7 @@ autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
11
 
18
- date: 2025-05-16 00:00:00 -07:00
12
+ date: 2026-06-06 00:00:00 -07:00
19
13
  default_executable:
20
14
  dependencies: []
21
15
 
@@ -42,24 +36,14 @@ rdoc_options: []
42
36
  require_paths:
43
37
  - lib
44
38
  required_ruby_version: !ruby/object:Gem::Requirement
45
- none: false
46
39
  requirements:
47
40
  - - ">="
48
41
  - !ruby/object:Gem::Version
49
- hash: 57
50
- segments:
51
- - 1
52
- - 8
53
- - 7
54
42
  version: 1.8.7
55
43
  required_rubygems_version: !ruby/object:Gem::Requirement
56
- none: false
57
44
  requirements:
58
45
  - - ">="
59
46
  - !ruby/object:Gem::Version
60
- hash: 3
61
- segments:
62
- - 0
63
47
  version: "0"
64
48
  requirements: []
65
49