simple-random 1.0.2 → 1.0.4
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 +5 -5
- data/.travis.yml +20 -6
- data/Gemfile +8 -12
- data/LICENSE +1 -1
- data/README.md +20 -11
- data/VERSION +1 -1
- data/lib/simple-random/multi_threaded_simple_random.rb +2 -3
- data/lib/simple-random/simple_random.rb +148 -108
- data/lib/simple-random.rb +2 -2
- data/simple-random.gemspec +2 -9
- data/test/helper.rb +2 -1
- data/test/test_simple_random.rb +83 -99
- metadata +3 -33
- data/Gemfile.lock +0 -81
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2b758006bb39a31380c6505ff4e9ecd52c67ef9b62ad952d63adeb7a2d49411c
|
|
4
|
+
data.tar.gz: 1ddd254e9a7cfce4b5e7d76dd19f58658501f537f4d46f2c025300d28a0aa471
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89da26472354aca342ab707908b2c519389ea92d38b72157c18fcaa71f2911312e46a37f5aaa821a4562a2171ae7c220bcdc4935de8afd2a4676279ff0624ebd
|
|
7
|
+
data.tar.gz: fcd901b6f50d9925d820f4ced57687716363ea12be33bac361f8734c54b27a6005d8077009814fa7cafd9edc88fa919eb84d41611dd20e6daf7ba43ae1f51c29
|
data/.travis.yml
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
# single test suite, non-parallel build.
|
|
2
|
+
|
|
3
|
+
env:
|
|
4
|
+
global:
|
|
5
|
+
- CC_TEST_REPORTER_ID=fc1330064eb82caf0f39ecd509e3e1c5b4fc5ca8b02fb9fa63d6e8a92da5ad18
|
|
1
6
|
language: ruby
|
|
2
7
|
rvm:
|
|
3
|
-
- 2.
|
|
4
|
-
- 2.
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
|
|
8
|
+
- 2.6
|
|
9
|
+
- 2.5
|
|
10
|
+
- 2.4
|
|
11
|
+
- 2.3.6
|
|
12
|
+
- 2.2.0
|
|
13
|
+
before_install:
|
|
14
|
+
- gem install bundler --version 1.17.3
|
|
15
|
+
before_script:
|
|
16
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
|
17
|
+
- chmod +x ./cc-test-reporter
|
|
18
|
+
- ./cc-test-reporter before-build
|
|
19
|
+
script:
|
|
20
|
+
- bundle exec rake test
|
|
21
|
+
after_script:
|
|
22
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/Gemfile
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
source
|
|
2
|
-
# Add dependencies required to use your gem here.
|
|
3
|
-
# Example:
|
|
4
|
-
# gem "activesupport", ">= 2.3.5"
|
|
1
|
+
source 'https://rubygems.org'
|
|
5
2
|
|
|
6
|
-
# Add dependencies to develop your gem here.
|
|
7
|
-
# Include everything needed to run rake, tests, features, etc.
|
|
8
3
|
group :development do
|
|
9
|
-
gem
|
|
10
|
-
gem
|
|
11
|
-
gem
|
|
12
|
-
gem
|
|
13
|
-
gem
|
|
14
|
-
gem
|
|
4
|
+
gem 'awesome_print', '>= 1.6'
|
|
5
|
+
gem 'minitest'
|
|
6
|
+
gem 'shoulda'
|
|
7
|
+
gem 'jeweler', '~> 2.0.1'
|
|
8
|
+
gem 'simplecov'
|
|
9
|
+
gem 'nokogiri', '>= 1.8.5'
|
|
10
|
+
gem 'codeclimate-test-reporter', require: nil
|
|
15
11
|
end
|
data/LICENSE
CHANGED
|
@@ -127,4 +127,4 @@ This License represents the complete agreement concerning subject matter hereof.
|
|
|
127
127
|
|
|
128
128
|
10. RESPONSIBILITY FOR CLAIMS.
|
|
129
129
|
|
|
130
|
-
As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
|
|
130
|
+
As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# simple-random 
|
|
2
2
|
|
|
3
3
|
Generate random numbers sampled from the following distributions:
|
|
4
4
|
|
|
@@ -34,16 +34,16 @@ Add `gem 'simple-random', '~> 1.0.0'` to your Gemfile and run `bundle install`.
|
|
|
34
34
|
Some of the methods available:
|
|
35
35
|
|
|
36
36
|
``` ruby
|
|
37
|
-
>
|
|
37
|
+
> r = SimpleRandom.new # Initialize a SimpleRandom instance
|
|
38
38
|
=> #<SimpleRandom:0x007f9e3ad58010 @m_w=521288629, @m_z=362436069>
|
|
39
|
-
>
|
|
40
|
-
>
|
|
39
|
+
> r.set_seed # By default the same random seed is used, so we change it
|
|
40
|
+
> r.uniform(0, 5) # Produce a uniform random sample from the open interval (lower, upper).
|
|
41
41
|
=> 0.6353204359766096
|
|
42
|
-
>
|
|
42
|
+
> r.normal(1000, 200) # Sample normal distribution with given mean and standard deviation
|
|
43
43
|
=> 862.5447157384566
|
|
44
|
-
>
|
|
44
|
+
> r.exponential(2) # Get exponential random sample with specified mean
|
|
45
45
|
=> 0.9386480625062965
|
|
46
|
-
>
|
|
46
|
+
> r.triangular(0, 2.5, 10) # Get triangular random sample with specified lower limit, mode, upper limit
|
|
47
47
|
=> 3.1083306054169277
|
|
48
48
|
```
|
|
49
49
|
|
|
@@ -56,11 +56,12 @@ See [lib/simple-random.rb](lib/simple-random/simple_random.rb) for all available
|
|
|
56
56
|
|
|
57
57
|
* Fork the project.
|
|
58
58
|
* Make your feature addition or bug fix.
|
|
59
|
-
* Add tests for it. This is important so I don't break it in a
|
|
60
|
-
|
|
61
|
-
* Commit, do not mess with rakefile, version, or history.
|
|
59
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
|
60
|
+
* Commit, but please do not mess with the gemspec, `Rakefile`, `VERSION`, `LICENSE`, or `.travis.yml`.
|
|
62
61
|
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
63
|
-
* Send me a pull request.
|
|
62
|
+
* Send me a pull request.
|
|
63
|
+
|
|
64
|
+
HT: This list was modified from the default instructions that ship with [jeweler](https://github.com/technicalpickles/jeweler).
|
|
64
65
|
|
|
65
66
|
## Copyright
|
|
66
67
|
|
|
@@ -68,6 +69,14 @@ Distributed under the Code Project Open License, which is similar to MIT or BSD.
|
|
|
68
69
|
|
|
69
70
|
## History
|
|
70
71
|
|
|
72
|
+
### 1.0.4 - 2026-02-18
|
|
73
|
+
* Remove vulnerable development dependency declarations for `bundler` and `rdoc`
|
|
74
|
+
* Use `https://rubygems.org` as the gem source in development
|
|
75
|
+
|
|
76
|
+
### 1.0.3 - 2015-11-25
|
|
77
|
+
* Attempt to reduce code complexity and improve readability
|
|
78
|
+
* Change error handling somewhat to throw specific errors and improve messages
|
|
79
|
+
|
|
71
80
|
### 1.0.2 - 2015-11-24
|
|
72
81
|
* Merge pull request from [cunchem](https://github.com/cunchem) to fix Laplace method
|
|
73
82
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.4
|
|
@@ -5,8 +5,7 @@ class MultiThreadedSimpleRandom < SimpleRandom
|
|
|
5
5
|
@instances = nil
|
|
6
6
|
|
|
7
7
|
def instance
|
|
8
|
-
|
|
9
|
-
unless @instances
|
|
8
|
+
unless instance_variable_defined?('@instances') && @instances
|
|
10
9
|
extend MonitorMixin
|
|
11
10
|
|
|
12
11
|
self.synchronize do
|
|
@@ -27,4 +26,4 @@ class MultiThreadedSimpleRandom < SimpleRandom
|
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
private_class_method :new
|
|
30
|
-
end
|
|
29
|
+
end
|
|
@@ -1,58 +1,67 @@
|
|
|
1
1
|
class SimpleRandom
|
|
2
|
-
|
|
3
|
-
@m_w = 521288629
|
|
4
|
-
@m_z = 362436069
|
|
5
|
-
end
|
|
2
|
+
class InvalidSeedArgument < StandardError; end
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
x = (args.first.to_f * 1000000).to_i
|
|
15
|
-
@m_w = x >> 16
|
|
16
|
-
@m_z = x % 4294967296 # 2 ** 32
|
|
4
|
+
I_32_BIT = 4294967296
|
|
5
|
+
F_32_BIT = 4294967296.0
|
|
6
|
+
DEFAULT_SEEDS = [521288629, 362436069]
|
|
7
|
+
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
if args.empty?
|
|
10
|
+
set_seed(*DEFAULT_SEEDS)
|
|
17
11
|
else
|
|
18
|
-
|
|
19
|
-
@m_w = x >> 16
|
|
20
|
-
@m_z = x % 4294967296 # 2 ** 32
|
|
12
|
+
set_seed(*args)
|
|
21
13
|
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_seed(*args)
|
|
17
|
+
validate_seeds!(*args)
|
|
22
18
|
|
|
23
|
-
@m_w
|
|
24
|
-
|
|
19
|
+
@m_w, @m_z = determine_seeds(*args)
|
|
20
|
+
|
|
21
|
+
ensure_32bit_seeds!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def seeds=(value)
|
|
25
|
+
set_seed(*[value].flatten.compact)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def seeds
|
|
29
|
+
[@m_w, @m_z]
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
# Produce a uniform random sample from the open interval (lower, upper).
|
|
28
|
-
# The method will not return either end point.
|
|
29
33
|
def uniform(lower = 0, upper = 1)
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
fail ArgumentError, 'Upper bound must be greater than lower bound.' unless lower < upper
|
|
35
|
+
|
|
36
|
+
((get_unsigned_int + 1) * (upper - lower) / F_32_BIT) + lower
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
# Sample normal distribution with given mean and standard deviation
|
|
35
40
|
def normal(mean = 0.0, standard_deviation = 1.0)
|
|
36
|
-
|
|
41
|
+
fail ArgumentError, 'Standard deviation must be strictly positive' unless standard_deviation > 0
|
|
42
|
+
|
|
37
43
|
mean + standard_deviation * ((-2.0 * Math.log(uniform)) ** 0.5) * Math.sin(2.0 * Math::PI * uniform)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# Get exponential random sample with specified mean
|
|
41
47
|
def exponential(mean = 1)
|
|
42
|
-
|
|
48
|
+
fail ArgumentError, "Mean must be strictly positive" unless mean > 0
|
|
49
|
+
|
|
43
50
|
-1.0 * mean * Math.log(uniform)
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
# Get triangular random sample with specified lower limit, mode, upper limit
|
|
47
54
|
def triangular(lower, mode, upper)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
fail ArgumentError, 'Upper bound must be greater than lower bound.' unless lower < upper
|
|
56
|
+
fail ArgumentError, 'Mode must lie between the upper and lower limits' if mode > upper || mode < lower
|
|
57
|
+
|
|
58
|
+
r = (upper - lower).to_f
|
|
59
|
+
u = uniform
|
|
60
|
+
|
|
61
|
+
if u < ((mode - lower) / r)
|
|
62
|
+
lower + Math.sqrt(u * r * (mode - lower))
|
|
54
63
|
else
|
|
55
|
-
upper - Math.sqrt((1 -
|
|
64
|
+
upper - Math.sqrt((1.0 - u) * r * (upper - mode))
|
|
56
65
|
end
|
|
57
66
|
end
|
|
58
67
|
|
|
@@ -60,28 +69,26 @@ class SimpleRandom
|
|
|
60
69
|
# by George Marsaglia and Wai Wan Tsang. ACM Transactions on Mathematical Software
|
|
61
70
|
# Vol 26, No 3, September 2000, pages 363-372.
|
|
62
71
|
def gamma(shape, scale)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
fail ArgumentError, 'Shape must be strictly positive' unless shape > 0
|
|
73
|
+
return scale * gamma(shape + 1.0, 1.0) * uniform ** -shape if shape < 1
|
|
74
|
+
|
|
75
|
+
d = shape - 1 / 3.0
|
|
76
|
+
c = (9 * d) ** -0.5
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
z = normal
|
|
80
|
+
|
|
81
|
+
condition1 = z > (-1.0 / c)
|
|
82
|
+
condition2 = false
|
|
83
|
+
|
|
84
|
+
if condition1
|
|
73
85
|
u = uniform
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
86
|
+
v = (1 + c * z) ** 3
|
|
87
|
+
condition2 = Math.log(u) < (0.5 * (z ** 2) + d * (1.0 - v + Math.log(v)))
|
|
77
88
|
end
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
g = gamma(shape + 1.0, 1.0)
|
|
82
|
-
w = uniform
|
|
83
|
-
return scale * g * (w ** (1.0 / shape))
|
|
84
|
-
end
|
|
89
|
+
end while !condition2
|
|
90
|
+
|
|
91
|
+
scale * d * v
|
|
85
92
|
end
|
|
86
93
|
|
|
87
94
|
def chi_square(degrees_of_freedom)
|
|
@@ -93,34 +100,36 @@ class SimpleRandom
|
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
def beta(a, b)
|
|
96
|
-
|
|
103
|
+
fail ArgumentError, "Parameters must be strictly positive" unless a > 0 && b > 0
|
|
97
104
|
u = gamma(a, 1)
|
|
98
105
|
v = gamma(b, 1)
|
|
99
106
|
u / (u + v)
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
def weibull(shape, scale)
|
|
103
|
-
|
|
110
|
+
fail ArgumentError, 'Shape and scale must be positive' unless shape > 0 && scale > 0
|
|
104
111
|
|
|
105
112
|
scale * ((-Math.log(uniform)) ** (1.0 / shape))
|
|
106
113
|
end
|
|
107
114
|
|
|
108
115
|
def cauchy(median, scale)
|
|
109
|
-
|
|
116
|
+
fail ArgumentError, 'Scale must be positive' unless scale > 0
|
|
110
117
|
|
|
111
118
|
median + scale * Math.tan(Math::PI * (uniform - 0.5))
|
|
112
119
|
end
|
|
113
120
|
|
|
114
121
|
def student_t(degrees_of_freedom)
|
|
115
|
-
|
|
122
|
+
fail ArgumentError, 'Degrees of freedom must be strictly positive' unless degrees_of_freedom > 0
|
|
116
123
|
|
|
117
124
|
normal / ((chi_square(degrees_of_freedom) / degrees_of_freedom) ** 0.5)
|
|
118
125
|
end
|
|
119
126
|
|
|
120
127
|
def laplace(mean, scale)
|
|
121
|
-
u_1 = uniform
|
|
128
|
+
u_1 = uniform(-0.5, 0.5)
|
|
122
129
|
u_2 = uniform
|
|
123
|
-
|
|
130
|
+
|
|
131
|
+
sign = u_1 / u_1.abs
|
|
132
|
+
mean + sign * scale * Math.log(1 - u_2)
|
|
124
133
|
end
|
|
125
134
|
|
|
126
135
|
def log_normal(mu, sigma)
|
|
@@ -144,65 +153,96 @@ class SimpleRandom
|
|
|
144
153
|
((@m_z << 16) + (@m_w & 65535)) % 4294967296
|
|
145
154
|
end
|
|
146
155
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
-0.11651675918591e-2,
|
|
158
|
-
-0.2152416741149e-3,
|
|
159
|
-
0.1280502823882e-3,
|
|
160
|
-
-0.201348547807e-4,
|
|
161
|
-
-0.12504934821e-5,
|
|
162
|
-
0.1133027232e-5,
|
|
163
|
-
-0.2056338417e-6,
|
|
164
|
-
0.6116095e-8,
|
|
165
|
-
0.50020075e-8,
|
|
166
|
-
-0.11812746e-8,
|
|
167
|
-
0.1043427e-9,
|
|
168
|
-
0.77823e-11,
|
|
169
|
-
-0.36968e-11,
|
|
170
|
-
0.51e-12,
|
|
171
|
-
-0.206e-13,
|
|
172
|
-
-0.54e-14,
|
|
173
|
-
0.14e-14
|
|
174
|
-
]
|
|
175
|
-
|
|
176
|
-
r = 1.0
|
|
156
|
+
def validate_seeds!(*args)
|
|
157
|
+
return true if args.compact.empty?
|
|
158
|
+
|
|
159
|
+
unless args[0].to_f > 0
|
|
160
|
+
fail InvalidSeedArgument, 'Seeds must be strictly positive'
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
unless args[1].nil? || args[1].to_f > 0
|
|
164
|
+
fail InvalidSeedArgument, 'Seeds must be strictly positive'
|
|
165
|
+
end
|
|
177
166
|
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def generate_temporal_seed(timestamp = Time.now)
|
|
171
|
+
x = (timestamp.to_f * 1000000).to_i
|
|
172
|
+
|
|
173
|
+
[x >> 16, x % 4294967296]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def gamma_function(x)
|
|
178
177
|
return 1e308 if x > 171.0
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
178
|
+
|
|
179
|
+
if x.to_f == x.to_i
|
|
180
|
+
return unless x > 0
|
|
181
|
+
return 1 if x.to_i == 1
|
|
182
|
+
|
|
183
|
+
(1...x).inject(&:*)
|
|
185
184
|
else
|
|
186
|
-
if x.abs > 1.0
|
|
187
|
-
|
|
188
|
-
z = x.abs - x.abs.to_i
|
|
185
|
+
z = if x.abs > 1.0
|
|
186
|
+
x.abs - x.abs.to_i
|
|
189
187
|
else
|
|
190
|
-
|
|
188
|
+
x
|
|
191
189
|
end
|
|
192
190
|
|
|
193
|
-
gr = g
|
|
194
|
-
|
|
195
|
-
gr = gr * z + g[i]
|
|
191
|
+
gr = GAMMA_VALUES.inject(GAMMA_NAUGHT) do |sum, g|
|
|
192
|
+
sum * z + g
|
|
196
193
|
end
|
|
197
|
-
|
|
198
|
-
if x.abs > 1
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
end
|
|
194
|
+
|
|
195
|
+
r = if x.abs > 1
|
|
196
|
+
(1..(x.abs.to_i)).inject(1.0) { |prod, i| prod * (x.abs - i) }
|
|
197
|
+
else
|
|
198
|
+
1.0
|
|
203
199
|
end
|
|
204
|
-
end
|
|
205
200
|
|
|
206
|
-
|
|
201
|
+
if x < 0 && x.abs > 1
|
|
202
|
+
-Math::PI * gr * z / (x * r * Math.sin(Math::PI * x))
|
|
203
|
+
else
|
|
204
|
+
r / (gr * z)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
207
|
end
|
|
208
|
+
|
|
209
|
+
def ensure_32bit_seeds!
|
|
210
|
+
@m_w = @m_w.to_i % I_32_BIT
|
|
211
|
+
@m_z = @m_z.to_i % I_32_BIT
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def determine_seeds(*args)
|
|
215
|
+
return generate_temporal_seed(args.first || Time.now) if args.empty? || args.first.respond_to?(:iso8601)
|
|
216
|
+
return [DEFAULT_SEEDS.first, args.first] if args.size < 2
|
|
217
|
+
args[0..1]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
GAMMA_NAUGHT = 0.14e-14
|
|
221
|
+
|
|
222
|
+
GAMMA_VALUES = [
|
|
223
|
+
-5.4e-15,
|
|
224
|
+
-2.06e-14,
|
|
225
|
+
5.1e-13,
|
|
226
|
+
-3.6968e-12,
|
|
227
|
+
7.7823e-12,
|
|
228
|
+
1.043427e-10,
|
|
229
|
+
-1.1812746e-09,
|
|
230
|
+
5.0020075e-09,
|
|
231
|
+
6.116095e-09,
|
|
232
|
+
-2.056338417e-07,
|
|
233
|
+
1.133027232e-06,
|
|
234
|
+
-1.2504934821e-06,
|
|
235
|
+
-2.01348547807e-05,
|
|
236
|
+
0.0001280502823882,
|
|
237
|
+
-0.0002152416741149,
|
|
238
|
+
-0.0011651675918591,
|
|
239
|
+
0.007218943246663,
|
|
240
|
+
-0.009621971527877,
|
|
241
|
+
-0.0421977345555443,
|
|
242
|
+
0.1665386113822915,
|
|
243
|
+
-0.0420026350340952,
|
|
244
|
+
-0.6558780715202538,
|
|
245
|
+
0.5772156649015329,
|
|
246
|
+
1.0
|
|
247
|
+
]
|
|
208
248
|
end
|
data/lib/simple-random.rb
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
require 'simple-random
|
|
2
|
-
require 'simple-random
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'simple-random', 'simple_random')
|
|
2
|
+
require File.join(File.dirname(__FILE__), 'simple-random', 'multi_threaded_simple_random')
|
data/simple-random.gemspec
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "simple-random"
|
|
9
|
-
s.version = "1.0.
|
|
9
|
+
s.version = "1.0.4"
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib"]
|
|
13
13
|
s.authors = ["John D. Cook", "Jason Adams"]
|
|
14
|
-
s.date = "
|
|
14
|
+
s.date = "2026-02-18"
|
|
15
15
|
s.description = "Simple Random Number Generator including Beta, Cauchy, Chi square, Exponential, Gamma, Inverse Gamma, Laplace (double exponential), Normal, Student t, Uniform, and Weibull. Ported from John D. Cook's C# Code."
|
|
16
16
|
s.email = "jasonmadams@gmail.com"
|
|
17
17
|
s.extra_rdoc_files = [
|
|
@@ -22,7 +22,6 @@ Gem::Specification.new do |s|
|
|
|
22
22
|
".document",
|
|
23
23
|
".travis.yml",
|
|
24
24
|
"Gemfile",
|
|
25
|
-
"Gemfile.lock",
|
|
26
25
|
"LICENSE",
|
|
27
26
|
"README.md",
|
|
28
27
|
"Rakefile",
|
|
@@ -45,23 +44,17 @@ Gem::Specification.new do |s|
|
|
|
45
44
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
|
46
45
|
s.add_development_dependency(%q<minitest>, [">= 0"])
|
|
47
46
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
|
48
|
-
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
|
49
|
-
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
|
50
47
|
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
|
51
48
|
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
|
52
49
|
else
|
|
53
50
|
s.add_dependency(%q<minitest>, [">= 0"])
|
|
54
51
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
|
55
|
-
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
|
56
|
-
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
|
57
52
|
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
|
58
53
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
|
59
54
|
end
|
|
60
55
|
else
|
|
61
56
|
s.add_dependency(%q<minitest>, [">= 0"])
|
|
62
57
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
|
63
|
-
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
|
64
|
-
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
|
65
58
|
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
|
66
59
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
|
67
60
|
end
|
data/test/helper.rb
CHANGED
data/test/test_simple_random.rb
CHANGED
|
@@ -12,7 +12,7 @@ end
|
|
|
12
12
|
class Array
|
|
13
13
|
def mean
|
|
14
14
|
if size > 0
|
|
15
|
-
inject(
|
|
15
|
+
inject(&:+) / size.to_f
|
|
16
16
|
else
|
|
17
17
|
0.0
|
|
18
18
|
end
|
|
@@ -28,102 +28,72 @@ class Array
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@r = SimpleRandom.new
|
|
35
|
-
end
|
|
31
|
+
def Time.now
|
|
32
|
+
new(2015, 11, 26, 12, 1, 15, '-05:00')
|
|
33
|
+
end
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
class TestSimpleRandom < MiniTest::Test
|
|
36
|
+
context "Setting the seeds for a simple random number generator" do
|
|
37
|
+
context "on initialization" do
|
|
38
|
+
should "assign default seeds when none are specified" do
|
|
39
|
+
r = SimpleRandom.new
|
|
40
|
+
assert r.seeds == SimpleRandom::DEFAULT_SEEDS
|
|
42
41
|
end
|
|
43
|
-
end
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
assert epsilon < MAXIMUM_EPSILON
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
should "generate random numbers from a normal distribution with mean approximately 0" do
|
|
53
|
-
numbers = generate_numbers(@r, :normal)
|
|
54
|
-
epsilon = (0.0 - numbers.mean).abs
|
|
55
|
-
|
|
56
|
-
assert epsilon < MAXIMUM_EPSILON
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
should "generate random numbers from a normal distribution with sample standard deviation approximately 1" do
|
|
60
|
-
numbers = generate_numbers(@r, :normal)
|
|
61
|
-
epsilon = (1.0 - numbers.standard_deviation).abs
|
|
62
|
-
|
|
63
|
-
assert epsilon < MAXIMUM_EPSILON
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
should "generate random numbers from an exponential distribution with mean approximately 1" do
|
|
67
|
-
numbers = generate_numbers(@r, :exponential)
|
|
68
|
-
epsilon = (1.0 - numbers.mean).abs
|
|
69
|
-
|
|
70
|
-
assert epsilon < MAXIMUM_EPSILON
|
|
71
|
-
end
|
|
43
|
+
should "assign the seeds specified in the initializer" do
|
|
44
|
+
r = SimpleRandom.new(1, 2)
|
|
45
|
+
assert r.seeds == [1, 2]
|
|
46
|
+
end
|
|
72
47
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
assert t >= 0.0
|
|
48
|
+
should "reject negative seed values" do
|
|
49
|
+
assert_raises SimpleRandom::InvalidSeedArgument do
|
|
50
|
+
SimpleRandom.new(-1, 3)
|
|
51
|
+
end
|
|
78
52
|
end
|
|
79
53
|
end
|
|
80
54
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
numbers = generate_numbers(@r, :triangular, a, c, b)
|
|
86
|
-
mean = (a + b + c) / 3
|
|
87
|
-
epsilon = (mean - numbers.mean).abs
|
|
88
|
-
|
|
89
|
-
assert epsilon < MAXIMUM_EPSILON
|
|
90
|
-
end
|
|
55
|
+
context 'after initialization' do
|
|
56
|
+
setup do
|
|
57
|
+
@r = SimpleRandom.new
|
|
58
|
+
end
|
|
91
59
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
numbers = generate_numbers(@r, :triangular, a, c, b)
|
|
97
|
-
std_dev = Math.sqrt((a**2 + b**2 + c**2 - a*b - a*c - b*c) / 18)
|
|
98
|
-
epsilon = (std_dev - numbers.standard_deviation).abs
|
|
60
|
+
should "accept a single value and leave the first seed the same" do
|
|
61
|
+
@r.seeds = 1
|
|
62
|
+
assert @r.seeds == [521288629, 1]
|
|
63
|
+
end
|
|
99
64
|
|
|
100
|
-
|
|
101
|
-
|
|
65
|
+
should "update the seeds when given an array of values" do
|
|
66
|
+
@r.seeds = [1, 2]
|
|
67
|
+
assert @r.seeds == [1, 2]
|
|
68
|
+
end
|
|
102
69
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
70
|
+
should "accept a timestamp instead of an numeric value" do
|
|
71
|
+
@r.seeds = Time.parse('2015-01-01T00:00:00.000-0500')
|
|
72
|
+
assert @r.seeds == [193992865, 413250560]
|
|
73
|
+
end
|
|
106
74
|
|
|
107
|
-
|
|
108
|
-
|
|
75
|
+
should "use the current timestamp when nothing is specified" do
|
|
76
|
+
@r.seeds = nil
|
|
77
|
+
assert @r.seeds == [628393424, 2245012672]
|
|
78
|
+
end
|
|
109
79
|
end
|
|
110
80
|
|
|
111
|
-
should "
|
|
112
|
-
|
|
113
|
-
|
|
81
|
+
should "provide different results with different integer seeds" do
|
|
82
|
+
r1 = SimpleRandom.new
|
|
83
|
+
r1.set_seed(2)
|
|
84
|
+
r2 = SimpleRandom.new
|
|
85
|
+
r2.set_seed(1234512343214134)
|
|
114
86
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
end
|
|
87
|
+
r1_randoms = 100.times.map { r1.uniform(0, 10).floor }
|
|
88
|
+
r2_randoms = 100.times.map { r2.uniform(0, 10).floor }
|
|
118
89
|
|
|
119
|
-
|
|
120
|
-
assert @r.weibull(5, 2.3)
|
|
90
|
+
assert r1_randoms != r2_randoms
|
|
121
91
|
end
|
|
122
92
|
end
|
|
123
93
|
|
|
124
|
-
context "A
|
|
94
|
+
context "A simple random number generator" do
|
|
125
95
|
setup do
|
|
126
|
-
@r =
|
|
96
|
+
@r = SimpleRandom.new
|
|
127
97
|
end
|
|
128
98
|
|
|
129
99
|
should "generate random numbers from a uniform distribution in the interval (0, 1)" do
|
|
@@ -159,7 +129,7 @@ class TestSimpleRandom < MiniTest::Test
|
|
|
159
129
|
numbers = generate_numbers(@r, :exponential)
|
|
160
130
|
epsilon = (1.0 - numbers.mean).abs
|
|
161
131
|
|
|
162
|
-
assert epsilon < MAXIMUM_EPSILON
|
|
132
|
+
assert epsilon < MAXIMUM_EPSILON
|
|
163
133
|
end
|
|
164
134
|
|
|
165
135
|
should "generate random numbers from triangular(0, 1, 1) in the range [0, 1]" do
|
|
@@ -183,30 +153,37 @@ class TestSimpleRandom < MiniTest::Test
|
|
|
183
153
|
|
|
184
154
|
should "generate random numbers from triangular(0, 1, 1) with standard deviation approximately 0.23" do
|
|
185
155
|
a = 0.0
|
|
186
|
-
c = 1.0
|
|
187
156
|
b = 1.0
|
|
188
|
-
|
|
189
|
-
|
|
157
|
+
c = 1.0
|
|
158
|
+
numbers = generate_numbers(@r, :triangular, a, b, c)
|
|
159
|
+
std_dev = Math.sqrt((a ** 2 + b ** 2 + c ** 2 - a * b - a * c - b * c) / 18)
|
|
190
160
|
epsilon = (std_dev - numbers.standard_deviation).abs
|
|
191
161
|
|
|
192
162
|
assert epsilon < MAXIMUM_EPSILON
|
|
193
163
|
end
|
|
194
164
|
|
|
195
|
-
should "generate random numbers from
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
165
|
+
should "generate random numbers from triangular(0, 0.5, 1) with mean approximately 0.5" do
|
|
166
|
+
a = 0.0
|
|
167
|
+
b = 0.5
|
|
168
|
+
c = 1.0
|
|
169
|
+
|
|
170
|
+
numbers = generate_numbers(@r, :triangular, a, b, c)
|
|
171
|
+
mean = (a + b + c) / 3
|
|
199
172
|
epsilon = (mean - numbers.mean).abs
|
|
200
173
|
|
|
201
174
|
assert epsilon < MAXIMUM_EPSILON
|
|
202
175
|
end
|
|
203
|
-
|
|
176
|
+
|
|
204
177
|
should "generate a random number sampled from a gamma distribution" do
|
|
205
178
|
assert @r.gamma(5, 2.3)
|
|
179
|
+
assert @r.gamma(5.3, 2.7)
|
|
180
|
+
assert @r.gamma(2.3, 2)
|
|
206
181
|
end
|
|
207
182
|
|
|
208
183
|
should "generate a random number sampled from an inverse gamma distribution" do
|
|
209
184
|
assert @r.inverse_gamma(5, 2.3)
|
|
185
|
+
assert @r.inverse_gamma(5.7, 2.8)
|
|
186
|
+
assert @r.inverse_gamma(3.2, 2)
|
|
210
187
|
end
|
|
211
188
|
|
|
212
189
|
should "generate a random number sampled from a beta distribution" do
|
|
@@ -221,6 +198,25 @@ class TestSimpleRandom < MiniTest::Test
|
|
|
221
198
|
assert @r.weibull(5, 2.3)
|
|
222
199
|
end
|
|
223
200
|
|
|
201
|
+
should "generate random number from a dirichlet distribution" do
|
|
202
|
+
assert @r.dirichlet(5.3, 2.7)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
should "generate random numbers from laplace(0, 1) with mean approximately 0" do
|
|
206
|
+
mean = 0.0
|
|
207
|
+
scale = 0.1
|
|
208
|
+
numbers = generate_numbers(@r, :laplace, mean, scale)
|
|
209
|
+
epsilon = (mean - numbers.mean).abs
|
|
210
|
+
|
|
211
|
+
assert epsilon < MAXIMUM_EPSILON
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
context "A multi-threaded simple random number generator" do
|
|
216
|
+
setup do
|
|
217
|
+
@r = MultiThreadedSimpleRandom.instance
|
|
218
|
+
end
|
|
219
|
+
|
|
224
220
|
should "work independently in every thread" do
|
|
225
221
|
sample_count = 10
|
|
226
222
|
thread_count = 10
|
|
@@ -241,17 +237,5 @@ class TestSimpleRandom < MiniTest::Test
|
|
|
241
237
|
assert samples.size == thread_count
|
|
242
238
|
assert samples.uniq.size == 1
|
|
243
239
|
end
|
|
244
|
-
|
|
245
|
-
should "provide different results with different integer seeds" do
|
|
246
|
-
r1 = SimpleRandom.new
|
|
247
|
-
r1.set_seed(2)
|
|
248
|
-
r2 = SimpleRandom.new
|
|
249
|
-
r2.set_seed(1234512343214134)
|
|
250
|
-
|
|
251
|
-
r1_randoms = 100.times.map { r1.uniform(0, 10).floor }
|
|
252
|
-
r2_randoms = 100.times.map { r2.uniform(0, 10).floor }
|
|
253
|
-
|
|
254
|
-
assert r1_randoms != r2_randoms
|
|
255
|
-
end
|
|
256
240
|
end
|
|
257
241
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simple-random
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John D. Cook
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2026-02-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: minitest
|
|
@@ -39,34 +39,6 @@ dependencies:
|
|
|
39
39
|
- - ">="
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
41
|
version: '0'
|
|
42
|
-
- !ruby/object:Gem::Dependency
|
|
43
|
-
name: rdoc
|
|
44
|
-
requirement: !ruby/object:Gem::Requirement
|
|
45
|
-
requirements:
|
|
46
|
-
- - "~>"
|
|
47
|
-
- !ruby/object:Gem::Version
|
|
48
|
-
version: '3.12'
|
|
49
|
-
type: :development
|
|
50
|
-
prerelease: false
|
|
51
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
-
requirements:
|
|
53
|
-
- - "~>"
|
|
54
|
-
- !ruby/object:Gem::Version
|
|
55
|
-
version: '3.12'
|
|
56
|
-
- !ruby/object:Gem::Dependency
|
|
57
|
-
name: bundler
|
|
58
|
-
requirement: !ruby/object:Gem::Requirement
|
|
59
|
-
requirements:
|
|
60
|
-
- - "~>"
|
|
61
|
-
- !ruby/object:Gem::Version
|
|
62
|
-
version: '1.0'
|
|
63
|
-
type: :development
|
|
64
|
-
prerelease: false
|
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
-
requirements:
|
|
67
|
-
- - "~>"
|
|
68
|
-
- !ruby/object:Gem::Version
|
|
69
|
-
version: '1.0'
|
|
70
42
|
- !ruby/object:Gem::Dependency
|
|
71
43
|
name: jeweler
|
|
72
44
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,7 +80,6 @@ files:
|
|
|
108
80
|
- ".document"
|
|
109
81
|
- ".travis.yml"
|
|
110
82
|
- Gemfile
|
|
111
|
-
- Gemfile.lock
|
|
112
83
|
- LICENSE
|
|
113
84
|
- README.md
|
|
114
85
|
- Rakefile
|
|
@@ -138,8 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
109
|
- !ruby/object:Gem::Version
|
|
139
110
|
version: '0'
|
|
140
111
|
requirements: []
|
|
141
|
-
|
|
142
|
-
rubygems_version: 2.4.5.1
|
|
112
|
+
rubygems_version: 3.0.3.1
|
|
143
113
|
signing_key:
|
|
144
114
|
specification_version: 4
|
|
145
115
|
summary: Simple Random Number Generator
|
data/Gemfile.lock
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
GEM
|
|
2
|
-
remote: http://rubygems.org/
|
|
3
|
-
specs:
|
|
4
|
-
activesupport (4.1.4)
|
|
5
|
-
i18n (~> 0.6, >= 0.6.9)
|
|
6
|
-
json (~> 1.7, >= 1.7.7)
|
|
7
|
-
minitest (~> 5.1)
|
|
8
|
-
thread_safe (~> 0.1)
|
|
9
|
-
tzinfo (~> 1.1)
|
|
10
|
-
addressable (2.3.6)
|
|
11
|
-
builder (3.2.2)
|
|
12
|
-
descendants_tracker (0.0.4)
|
|
13
|
-
thread_safe (~> 0.3, >= 0.3.1)
|
|
14
|
-
docile (1.1.5)
|
|
15
|
-
faraday (0.9.0)
|
|
16
|
-
multipart-post (>= 1.2, < 3)
|
|
17
|
-
git (1.2.7)
|
|
18
|
-
github_api (0.11.3)
|
|
19
|
-
addressable (~> 2.3)
|
|
20
|
-
descendants_tracker (~> 0.0.1)
|
|
21
|
-
faraday (~> 0.8, < 0.10)
|
|
22
|
-
hashie (>= 1.2)
|
|
23
|
-
multi_json (>= 1.7.5, < 2.0)
|
|
24
|
-
nokogiri (~> 1.6.0)
|
|
25
|
-
oauth2
|
|
26
|
-
hashie (3.1.0)
|
|
27
|
-
highline (1.6.21)
|
|
28
|
-
i18n (0.6.9)
|
|
29
|
-
jeweler (2.0.1)
|
|
30
|
-
builder
|
|
31
|
-
bundler (>= 1.0)
|
|
32
|
-
git (>= 1.2.5)
|
|
33
|
-
github_api
|
|
34
|
-
highline (>= 1.6.15)
|
|
35
|
-
nokogiri (>= 1.5.10)
|
|
36
|
-
rake
|
|
37
|
-
rdoc
|
|
38
|
-
json (1.8.1)
|
|
39
|
-
jwt (1.0.0)
|
|
40
|
-
mini_portile (0.6.0)
|
|
41
|
-
minitest (5.4.0)
|
|
42
|
-
multi_json (1.10.1)
|
|
43
|
-
multi_xml (0.5.5)
|
|
44
|
-
multipart-post (2.0.0)
|
|
45
|
-
nokogiri (1.6.2.1)
|
|
46
|
-
mini_portile (= 0.6.0)
|
|
47
|
-
oauth2 (0.9.4)
|
|
48
|
-
faraday (>= 0.8, < 0.10)
|
|
49
|
-
jwt (~> 1.0)
|
|
50
|
-
multi_json (~> 1.3)
|
|
51
|
-
multi_xml (~> 0.5)
|
|
52
|
-
rack (~> 1.2)
|
|
53
|
-
rack (1.5.2)
|
|
54
|
-
rake (10.3.2)
|
|
55
|
-
rdoc (3.12.2)
|
|
56
|
-
json (~> 1.4)
|
|
57
|
-
shoulda (3.5.0)
|
|
58
|
-
shoulda-context (~> 1.0, >= 1.0.1)
|
|
59
|
-
shoulda-matchers (>= 1.4.1, < 3.0)
|
|
60
|
-
shoulda-context (1.2.1)
|
|
61
|
-
shoulda-matchers (2.6.1)
|
|
62
|
-
activesupport (>= 3.0.0)
|
|
63
|
-
simplecov (0.8.2)
|
|
64
|
-
docile (~> 1.1.0)
|
|
65
|
-
multi_json
|
|
66
|
-
simplecov-html (~> 0.8.0)
|
|
67
|
-
simplecov-html (0.8.0)
|
|
68
|
-
thread_safe (0.3.4)
|
|
69
|
-
tzinfo (1.2.1)
|
|
70
|
-
thread_safe (~> 0.1)
|
|
71
|
-
|
|
72
|
-
PLATFORMS
|
|
73
|
-
ruby
|
|
74
|
-
|
|
75
|
-
DEPENDENCIES
|
|
76
|
-
bundler (~> 1.0)
|
|
77
|
-
jeweler (~> 2.0.1)
|
|
78
|
-
minitest
|
|
79
|
-
rdoc (~> 3.12)
|
|
80
|
-
shoulda
|
|
81
|
-
simplecov
|