games_dice 0.3.9 → 0.4.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +9 -12
- data/CHANGELOG.md +29 -13
- data/Gemfile +2 -0
- data/README.md +5 -5
- data/Rakefile +14 -11
- data/ext/games_dice/extconf.rb +4 -22
- data/ext/games_dice/probabilities.c +1 -1
- data/games_dice.gemspec +26 -28
- data/lib/games_dice/bunch.rb +241 -247
- data/lib/games_dice/complex_die.rb +287 -303
- data/lib/games_dice/complex_die_helpers.rb +68 -0
- data/lib/games_dice/constants.rb +10 -10
- data/lib/games_dice/dice.rb +146 -143
- data/lib/games_dice/die.rb +101 -97
- data/lib/games_dice/die_result.rb +193 -189
- data/lib/games_dice/map_rule.rb +72 -70
- data/lib/games_dice/marshal.rb +18 -13
- data/lib/games_dice/parser.rb +219 -218
- data/lib/games_dice/reroll_rule.rb +76 -77
- data/lib/games_dice/version.rb +3 -1
- data/lib/games_dice.rb +19 -16
- data/spec/bunch_spec.rb +399 -421
- data/spec/complex_die_spec.rb +314 -306
- data/spec/dice_spec.rb +33 -34
- data/spec/die_result_spec.rb +163 -170
- data/spec/die_spec.rb +81 -82
- data/spec/helpers.rb +26 -22
- data/spec/map_rule_spec.rb +40 -44
- data/spec/parser_spec.rb +106 -82
- data/spec/probability_spec.rb +530 -527
- data/spec/readme_spec.rb +404 -384
- data/spec/reroll_rule_spec.rb +40 -44
- metadata +63 -74
- data/lib/games_dice/probabilities.rb +0 -445
@@ -1,445 +0,0 @@
|
|
1
|
-
# This class models probability distributions for dice systems.
|
2
|
-
#
|
3
|
-
# An object of this class represents a single distribution, which might be the result of a complex
|
4
|
-
# combination of dice.
|
5
|
-
#
|
6
|
-
# @example Distribution for a six-sided die
|
7
|
-
# probs = GamesDice::Probabilities.for_fair_die( 6 )
|
8
|
-
# probs.min # => 1
|
9
|
-
# probs.max # => 6
|
10
|
-
# probs.expected # => 3.5
|
11
|
-
# probs.p_ge( 4 ) # => 0.5
|
12
|
-
#
|
13
|
-
# @example Adding two distributions
|
14
|
-
# pd6 = GamesDice::Probabilities.for_fair_die( 6 )
|
15
|
-
# probs = GamesDice::Probabilities.add_distributions( pd6, pd6 )
|
16
|
-
# probs.min # => 2
|
17
|
-
# probs.max # => 12
|
18
|
-
# probs.expected # => 7.0
|
19
|
-
# probs.p_ge( 10 ) # => 0.16666666666666669
|
20
|
-
#
|
21
|
-
class GamesDice::Probabilities
|
22
|
-
|
23
|
-
# Creates new instance of GamesDice::Probabilities.
|
24
|
-
# @param [Array<Float>] probs Each entry in the array is the probability of getting a result
|
25
|
-
# @param [Integer] offset The result associated with index of 0 in the array
|
26
|
-
# @return [GamesDice::Probabilities]
|
27
|
-
def initialize( probs = [1.0], offset = 0 )
|
28
|
-
# This should *probably* be validated in future, but that would impact performance
|
29
|
-
@probs = check_probs_array probs.clone
|
30
|
-
@offset = Integer(offset)
|
31
|
-
end
|
32
|
-
|
33
|
-
# @!visibility private
|
34
|
-
# the Array, Offset representation of probabilities.
|
35
|
-
def to_ao
|
36
|
-
[ @probs, @offset ]
|
37
|
-
end
|
38
|
-
|
39
|
-
# Iterates through value, probability pairs
|
40
|
-
# @yieldparam [Integer] result A result that may be possible in the dice scheme
|
41
|
-
# @yieldparam [Float] probability Probability of result, in range 0.0..1.0
|
42
|
-
# @return [GamesDice::Probabilities] this object
|
43
|
-
def each
|
44
|
-
@probs.each_with_index { |p,i| yield( i+@offset, p ) if p > 0.0 }
|
45
|
-
return self
|
46
|
-
end
|
47
|
-
|
48
|
-
# A hash representation of the distribution. Each key is an integer result,
|
49
|
-
# and the matching value is probability of getting that result. A new hash is generated on each
|
50
|
-
# call to this method.
|
51
|
-
# @return [Hash]
|
52
|
-
def to_h
|
53
|
-
GamesDice::Probabilities.prob_ao_to_h( @probs, @offset )
|
54
|
-
end
|
55
|
-
|
56
|
-
# @!attribute [r] min
|
57
|
-
# Minimum result in the distribution
|
58
|
-
# @return [Integer]
|
59
|
-
def min
|
60
|
-
@offset
|
61
|
-
end
|
62
|
-
|
63
|
-
# @!attribute [r] max
|
64
|
-
# Maximum result in the distribution
|
65
|
-
# @return [Integer]
|
66
|
-
def max
|
67
|
-
@offset + @probs.count() - 1
|
68
|
-
end
|
69
|
-
|
70
|
-
# @!attribute [r] expected
|
71
|
-
# Expected value of distribution.
|
72
|
-
# @return [Float]
|
73
|
-
def expected
|
74
|
-
@expected ||= calc_expected
|
75
|
-
end
|
76
|
-
|
77
|
-
# Probability of result equalling specific target
|
78
|
-
# @param [Integer] target
|
79
|
-
# @return [Float] in range (0.0..1.0)
|
80
|
-
def p_eql target
|
81
|
-
i = Integer(target) - @offset
|
82
|
-
return 0.0 if i < 0 || i >= @probs.count
|
83
|
-
@probs[ i ]
|
84
|
-
end
|
85
|
-
|
86
|
-
# Probability of result being greater than specific target
|
87
|
-
# @param [Integer] target
|
88
|
-
# @return [Float] in range (0.0..1.0)
|
89
|
-
def p_gt target
|
90
|
-
p_ge( Integer(target) + 1 )
|
91
|
-
end
|
92
|
-
|
93
|
-
# Probability of result being equal to or greater than specific target
|
94
|
-
# @param [Integer] target
|
95
|
-
# @return [Float] in range (0.0..1.0)
|
96
|
-
def p_ge target
|
97
|
-
target = Integer(target)
|
98
|
-
return @prob_ge[target] if @prob_ge && @prob_ge[target]
|
99
|
-
@prob_ge = {} unless @prob_ge
|
100
|
-
|
101
|
-
return 1.0 if target <= min
|
102
|
-
return 0.0 if target > max
|
103
|
-
@prob_ge[target] = @probs[target-@offset,@probs.count-1].inject(0.0) {|so_far,p| so_far + p }
|
104
|
-
end
|
105
|
-
|
106
|
-
# Probability of result being equal to or less than specific target
|
107
|
-
# @param [Integer] target
|
108
|
-
# @return [Float] in range (0.0..1.0)
|
109
|
-
def p_le target
|
110
|
-
target = Integer(target)
|
111
|
-
return @prob_le[target] if @prob_le && @prob_le[target]
|
112
|
-
@prob_le = {} unless @prob_le
|
113
|
-
|
114
|
-
return 1.0 if target >= max
|
115
|
-
return 0.0 if target < min
|
116
|
-
@prob_le[target] = @probs[0,1+target-@offset].inject(0.0) {|so_far,p| so_far + p }
|
117
|
-
end
|
118
|
-
|
119
|
-
# Probability of result being less than specific target
|
120
|
-
# @param [Integer] target
|
121
|
-
# @return [Float] in range (0.0..1.0)
|
122
|
-
def p_lt target
|
123
|
-
p_le( Integer(target) - 1 )
|
124
|
-
end
|
125
|
-
|
126
|
-
# Probability distribution derived from this one, where we know (or are only interested in
|
127
|
-
# situations where) the result is greater than or equal to target.
|
128
|
-
# @param [Integer] target
|
129
|
-
# @return [GamesDice::Probabilities] new distribution.
|
130
|
-
def given_ge target
|
131
|
-
target = Integer(target)
|
132
|
-
target = min if min > target
|
133
|
-
p = p_ge(target)
|
134
|
-
raise "There is no valid distribution given a result >= #{target}" unless p > 0.0
|
135
|
-
mult = 1.0/p
|
136
|
-
new_probs = @probs[target-@offset,@probs.count-1].map { |x| x * mult }
|
137
|
-
GamesDice::Probabilities.new( new_probs, target )
|
138
|
-
end
|
139
|
-
|
140
|
-
# Probability distribution derived from this one, where we know (or are only interested in
|
141
|
-
# situations where) the result is less than or equal to target.
|
142
|
-
# @param [Integer] target
|
143
|
-
# @return [GamesDice::Probabilities] new distribution.
|
144
|
-
def given_le target
|
145
|
-
target = Integer(target)
|
146
|
-
target = max if max < target
|
147
|
-
p = p_le(target)
|
148
|
-
raise "There is no valid distribution given a result <= #{target}" unless p > 0.0
|
149
|
-
mult = 1.0/p
|
150
|
-
new_probs = @probs[0..target-@offset].map { |x| x * mult }
|
151
|
-
GamesDice::Probabilities.new( new_probs, @offset )
|
152
|
-
end
|
153
|
-
|
154
|
-
# Creates new instance of GamesDice::Probabilities.
|
155
|
-
# @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
|
156
|
-
# and the matching value is probability of getting that result
|
157
|
-
# @return [GamesDice::Probabilities]
|
158
|
-
def self.from_h prob_hash
|
159
|
-
raise TypeError, "from_h expected a Hash" unless prob_hash.is_a? Hash
|
160
|
-
probs, offset = prob_h_to_ao( prob_hash )
|
161
|
-
GamesDice::Probabilities.new( probs, offset )
|
162
|
-
end
|
163
|
-
|
164
|
-
# Distribution for a die with equal chance of rolling 1..N
|
165
|
-
# @param [Integer] sides Number of sides on die
|
166
|
-
# @return [GamesDice::Probabilities]
|
167
|
-
def self.for_fair_die sides
|
168
|
-
sides = Integer(sides)
|
169
|
-
raise ArgumentError, "sides must be at least 1" unless sides > 0
|
170
|
-
raise ArgumentError, "sides can be at most 100000" if sides > 100000
|
171
|
-
GamesDice::Probabilities.new( Array.new( sides, 1.0/sides ), 1 )
|
172
|
-
end
|
173
|
-
|
174
|
-
# Combines two distributions to create a third, that represents the distribution created when adding
|
175
|
-
# results together.
|
176
|
-
# @param [GamesDice::Probabilities] pd_a First distribution
|
177
|
-
# @param [GamesDice::Probabilities] pd_b Second distribution
|
178
|
-
# @return [GamesDice::Probabilities]
|
179
|
-
def self.add_distributions pd_a, pd_b
|
180
|
-
unless pd_a.is_a?( GamesDice::Probabilities ) && pd_b.is_a?( GamesDice::Probabilities )
|
181
|
-
raise TypeError, "parameter to add_distributions is not a GamesDice::Probabilities"
|
182
|
-
end
|
183
|
-
|
184
|
-
combined_min = pd_a.min + pd_b.min
|
185
|
-
combined_max = pd_a.max + pd_b.max
|
186
|
-
|
187
|
-
add_distributions_internal combined_min, combined_max, 1, pd_a, 1, pd_b
|
188
|
-
end
|
189
|
-
|
190
|
-
# Combines two distributions with multipliers to create a third, that represents the distribution
|
191
|
-
# created when adding weighted results together.
|
192
|
-
# @param [Integer] m_a Weighting for first distribution
|
193
|
-
# @param [GamesDice::Probabilities] pd_a First distribution
|
194
|
-
# @param [Integer] m_b Weighting for second distribution
|
195
|
-
# @param [GamesDice::Probabilities] pd_b Second distribution
|
196
|
-
# @return [GamesDice::Probabilities]
|
197
|
-
def self.add_distributions_mult m_a, pd_a, m_b, pd_b
|
198
|
-
unless pd_a.is_a?( GamesDice::Probabilities ) && pd_b.is_a?( GamesDice::Probabilities )
|
199
|
-
raise TypeError, "parameter to add_distributions_mult is not a GamesDice::Probabilities"
|
200
|
-
end
|
201
|
-
|
202
|
-
m_a = Integer(m_a)
|
203
|
-
m_b = Integer(m_b)
|
204
|
-
|
205
|
-
combined_min, combined_max = [
|
206
|
-
m_a * pd_a.min + m_b * pd_b.min, m_a * pd_a.max + m_b * pd_b.min,
|
207
|
-
m_a * pd_a.min + m_b * pd_b.max, m_a * pd_a.max + m_b * pd_b.max,
|
208
|
-
].minmax
|
209
|
-
|
210
|
-
add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
|
211
|
-
end
|
212
|
-
|
213
|
-
# Returns a symbol for the language name that this class is implemented in. The C version of the
|
214
|
-
# code is noticeably faster when dealing with larger numbers of possible results.
|
215
|
-
# @return [Symbol] Either :c or :ruby
|
216
|
-
def self.implemented_in
|
217
|
-
:ruby
|
218
|
-
end
|
219
|
-
|
220
|
-
# Adds a distribution to itself repeatedly, to simulate a number of dice
|
221
|
-
# results being summed.
|
222
|
-
# @param [Integer] n Number of repetitions, must be at least 1
|
223
|
-
# @return [GamesDice::Probabilities] new distribution
|
224
|
-
def repeat_sum n
|
225
|
-
n = Integer( n )
|
226
|
-
raise "Cannot combine probabilities less than once" if n < 1
|
227
|
-
raise "Probability distribution too large" if ( n * @probs.count ) > 1000000
|
228
|
-
pd_power = self
|
229
|
-
pd_result = nil
|
230
|
-
|
231
|
-
use_power = 1
|
232
|
-
loop do
|
233
|
-
if ( use_power & n ) > 0
|
234
|
-
if pd_result
|
235
|
-
pd_result = GamesDice::Probabilities.add_distributions( pd_result, pd_power )
|
236
|
-
else
|
237
|
-
pd_result = pd_power
|
238
|
-
end
|
239
|
-
end
|
240
|
-
use_power = use_power << 1
|
241
|
-
break if use_power > n
|
242
|
-
pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power )
|
243
|
-
end
|
244
|
-
pd_result
|
245
|
-
end
|
246
|
-
|
247
|
-
# Calculates distribution generated by summing best k results of n iterations
|
248
|
-
# of the distribution.
|
249
|
-
# @param [Integer] n Number of repetitions, must be at least 1
|
250
|
-
# @param [Integer] k Number of best results to keep and sum
|
251
|
-
# @return [GamesDice::Probabilities] new distribution
|
252
|
-
def repeat_n_sum_k n, k, kmode = :keep_best
|
253
|
-
n = Integer( n )
|
254
|
-
k = Integer( k )
|
255
|
-
raise "Cannot combine probabilities less than once" if n < 1
|
256
|
-
# Technically this is a limitation of C code, but Ruby version is most likely slow and inaccurate beyond 170
|
257
|
-
raise "Too many dice to calculate numbers of arrangements" if n > 170
|
258
|
-
check_keep_mode( kmode )
|
259
|
-
|
260
|
-
if k >= n
|
261
|
-
return repeat_sum( n )
|
262
|
-
end
|
263
|
-
new_probs = Array.new( @probs.count * k, 0.0 )
|
264
|
-
new_offset = @offset * k
|
265
|
-
d = n - k
|
266
|
-
|
267
|
-
each do | q, p_maybe |
|
268
|
-
next unless p_maybe > 0.0
|
269
|
-
|
270
|
-
# keep_distributions is array of Probabilities, indexed by number of keepers > q, which is in 0...k
|
271
|
-
keep_distributions = calc_keep_distributions( k, q, kmode )
|
272
|
-
p_table = calc_p_table( q, p_maybe, kmode )
|
273
|
-
|
274
|
-
(0...k).each do |n|
|
275
|
-
keepers = [2] * n + [1] * (k-n)
|
276
|
-
p_so_far = keepers.inject(1.0) { |p,idx| p * p_table[idx] }
|
277
|
-
next unless p_so_far > 0.0
|
278
|
-
(0..d).each do |dn|
|
279
|
-
discards = [1] * (d-dn) + [0] * dn
|
280
|
-
sequence = keepers + discards
|
281
|
-
p_sequence = discards.inject( p_so_far ) { |p,idx| p * p_table[idx] }
|
282
|
-
next unless p_sequence > 0.0
|
283
|
-
p_sequence *= GamesDice::Combinations.count_variations( sequence )
|
284
|
-
kd = keep_distributions[n]
|
285
|
-
kd.each { |r,p_r| new_probs[r-new_offset] += p_r * p_sequence }
|
286
|
-
end
|
287
|
-
end
|
288
|
-
end
|
289
|
-
GamesDice::Probabilities.new( new_probs, new_offset )
|
290
|
-
end
|
291
|
-
|
292
|
-
private
|
293
|
-
|
294
|
-
def self.add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
|
295
|
-
new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
|
296
|
-
probs_a, offset_a = pd_a.to_ao
|
297
|
-
probs_b, offset_b = pd_b.to_ao
|
298
|
-
|
299
|
-
probs_a.each_with_index do |pa,i|
|
300
|
-
probs_b.each_with_index do |pb,j|
|
301
|
-
k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
|
302
|
-
pc = pa * pb
|
303
|
-
new_probs[ k ] += pc
|
304
|
-
end
|
305
|
-
end
|
306
|
-
GamesDice::Probabilities.new( new_probs, combined_min )
|
307
|
-
end
|
308
|
-
|
309
|
-
def check_probs_array probs_array
|
310
|
-
raise TypeError unless probs_array.is_a?( Array )
|
311
|
-
probs_array.map!{ |n| Float(n) }
|
312
|
-
total = probs_array.inject(0.0) do |t,x|
|
313
|
-
if x < 0.0 || x > 1.0
|
314
|
-
raise ArgumentError, "Found probability value #{x} which is not in range 0.0..1.0"
|
315
|
-
end
|
316
|
-
t+x
|
317
|
-
end
|
318
|
-
if (total-1.0).abs > 1e-6
|
319
|
-
raise ArgumentError, "Total probabilities too far from 1.0 for a valid distribution"
|
320
|
-
end
|
321
|
-
probs_array
|
322
|
-
end
|
323
|
-
|
324
|
-
def calc_keep_distributions k, q, kmode
|
325
|
-
kd_probabilities = calc_keep_definite_distributions q, kmode
|
326
|
-
|
327
|
-
keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ]
|
328
|
-
if kd_probabilities && k > 1
|
329
|
-
(1...k).each do |n|
|
330
|
-
extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) )
|
331
|
-
n_probs = kd_probabilities.repeat_sum( n )
|
332
|
-
keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs )
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
keep_distributions
|
337
|
-
end
|
338
|
-
|
339
|
-
def calc_keep_definite_distributions q, kmode
|
340
|
-
kd_probabilities = nil
|
341
|
-
case kmode
|
342
|
-
when :keep_best
|
343
|
-
p_definites = p_gt(q)
|
344
|
-
kd_probabilities = given_ge( q + 1 ) if p_definites > 0.0
|
345
|
-
when :keep_worst
|
346
|
-
p_definites = p_lt(q)
|
347
|
-
kd_probabilities = given_le( q - 1 ) if p_definites > 0.0
|
348
|
-
end
|
349
|
-
kd_probabilities
|
350
|
-
end
|
351
|
-
|
352
|
-
def calc_p_table q, p_maybe, kmode
|
353
|
-
case kmode
|
354
|
-
when :keep_best
|
355
|
-
p_kept = p_gt(q)
|
356
|
-
p_rejected = p_lt(q)
|
357
|
-
when :keep_worst
|
358
|
-
p_kept = p_lt(q)
|
359
|
-
p_rejected = p_gt(q)
|
360
|
-
end
|
361
|
-
[ p_rejected, p_maybe, p_kept ]
|
362
|
-
end
|
363
|
-
|
364
|
-
# Convert hash to array,offset notation
|
365
|
-
def self.prob_h_to_ao h
|
366
|
-
rmin,rmax = h.keys.minmax
|
367
|
-
o = rmin
|
368
|
-
s = 1 + rmax - rmin
|
369
|
-
raise ArgumentError, "Range of possible results too large" if s > 1000000
|
370
|
-
a = Array.new( s, 0.0 )
|
371
|
-
h.each { |k,v| a[k-rmin] = Float(v) }
|
372
|
-
[a,o]
|
373
|
-
end
|
374
|
-
|
375
|
-
# Convert array,offset notation to hash
|
376
|
-
def self.prob_ao_to_h a, o
|
377
|
-
h = Hash.new
|
378
|
-
a.each_with_index { |v,i| h[i+o] = v if v > 0.0 }
|
379
|
-
h
|
380
|
-
end
|
381
|
-
|
382
|
-
def calc_expected
|
383
|
-
total = 0.0
|
384
|
-
@probs.each_with_index { |v,i| total += (i+@offset)*v }
|
385
|
-
total
|
386
|
-
end
|
387
|
-
|
388
|
-
def check_keep_mode kmode
|
389
|
-
raise "Keep mode #{kmode.inspect} not recognised" unless [:keep_best,:keep_worst].member?( kmode )
|
390
|
-
end
|
391
|
-
|
392
|
-
end # class GamesDice::Probabilities
|
393
|
-
|
394
|
-
# @!visibility private
|
395
|
-
# Helper module with optimised Ruby for counting variations of arrays, such as those returned by
|
396
|
-
# Array#repeated_combination
|
397
|
-
#
|
398
|
-
# @example How many ways can [3,3,6] be arranged?
|
399
|
-
# GamesDice::Combinations.count_variations( [3,3,6] )
|
400
|
-
# => 3
|
401
|
-
#
|
402
|
-
# @example When prob( a ) and result( a ) are same for any arrangement of Array a
|
403
|
-
# items = [1,2,3,4,5,6]
|
404
|
-
# items.repeated_combination(5).each do |a|
|
405
|
-
# this_result = result( a )
|
406
|
-
# this_prob = prob( a ) * GamesDice::Combinations.count_variations( a )
|
407
|
-
# # Do something useful with this knowledge! E.g. save it to probability array.
|
408
|
-
# end
|
409
|
-
#
|
410
|
-
module GamesDice::Combinations
|
411
|
-
@@variations_cache = {}
|
412
|
-
@@factorial_cache = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
|
413
|
-
|
414
|
-
# Counts variations of an array. A unique variation is an arrangement of the array elements which
|
415
|
-
# is detectably different (using ==) from any other. So [1,1,1] has only 1 unique arrangement,
|
416
|
-
# but [1,2,3] has 6 possibilities.
|
417
|
-
# @param [Array] array List of things that can be arranged
|
418
|
-
# @return [Integer] Number of unique arrangements
|
419
|
-
def self.count_variations array
|
420
|
-
all_count = array.count
|
421
|
-
group_sizes = group_counts( array )
|
422
|
-
cache_key = all_count.to_s + ":" + group_sizes.join(',')
|
423
|
-
@@variations_cache[cache_key] ||= variations_of( all_count, group_sizes )
|
424
|
-
end
|
425
|
-
|
426
|
-
private
|
427
|
-
|
428
|
-
def self.variations_of all_count, groups
|
429
|
-
all_arrangements = factorial( all_count )
|
430
|
-
# The reject is an optimisation to avoid calculating and multplying by factorial(1) (==1)
|
431
|
-
identical_arrangements = groups.reject {|x| x==1 }.inject(1) { |prod,g| prod * factorial(g) }
|
432
|
-
all_arrangements/identical_arrangements
|
433
|
-
end
|
434
|
-
|
435
|
-
# Returns counts of unique items in array e.g. [8,8,8,7,6,6] returns [1,2,3]
|
436
|
-
# Sort is for caching
|
437
|
-
def self.group_counts array
|
438
|
-
array.group_by {|x| x}.values.map {|v| v.count}.sort
|
439
|
-
end
|
440
|
-
|
441
|
-
def self.factorial n
|
442
|
-
# Can start range from 2 because we have pre-cached the result for n=1
|
443
|
-
@@factorial_cache[n] ||= (2..n).inject(:*)
|
444
|
-
end
|
445
|
-
end
|