dicey 0.15.2 โ 0.16.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 +4 -4
- data/README.md +100 -41
- data/lib/dicey/abstract_die.rb +5 -3
- data/lib/dicey/die_foundry.rb +36 -8
- data/lib/dicey/distribution_properties_calculator.rb +3 -3
- data/lib/dicey/mixins/rational_to_integer.rb +21 -0
- data/lib/dicey/mixins/vectorize_dice.rb +29 -0
- data/lib/dicey/numeric_die.rb +5 -1
- data/lib/dicey/output_formatters/key_value_formatter.rb +2 -2
- data/lib/dicey/regular_die.rb +2 -0
- data/lib/dicey/roller.rb +25 -3
- data/lib/dicey/sum_frequency_calculators/base_calculator.rb +2 -1
- data/lib/dicey/sum_frequency_calculators/brute_force.rb +35 -13
- data/lib/dicey/sum_frequency_calculators/empirical.rb +24 -2
- data/lib/dicey/sum_frequency_calculators/multinomial_coefficients.rb +1 -0
- data/lib/dicey/sum_frequency_calculators/test_runner.rb +28 -1
- data/lib/dicey/version.rb +1 -1
- data/lib/dicey.rb +1 -0
- metadata +22 -7
- data/lib/dicey/rational_to_integer.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b590b1e43f4e265f3a8fab12a949de1cdd20cc39b1f4334d997520cdbc9efc4
|
4
|
+
data.tar.gz: 2c0e3c4577a3f22483296b3a6c238c54937c8b5a360b21c53ca2d0b9aa432fcb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bed55910f504316de9f078f938f2a9a56e8dfbe65c3552eee330bc58f7559f699a0906837f4bda30fe098feed58469e8c7039e78864e79f4d263918729222ebb
|
7
|
+
data.tar.gz: 392b0c5c3b5e547d497092358769260b7b8e1c99bd98f1eb8759c389757386452720a173e619aea5bd2cebe996348c99f314bfc3b1e54fa068583fc083525cd9
|
data/README.md
CHANGED
@@ -24,6 +24,7 @@ In seriousness, this program is mainly useful for calculating total frequency (p
|
|
24
24
|
- [Example 2: Complex distribution with different dice](#example-2-complex-distribution-with-different-dice)
|
25
25
|
- [Example 3: Custom dice](#example-3-custom-dice)
|
26
26
|
- [Example 4: Rolling even more custom dice](#example-4-rolling-even-more-custom-dice)
|
27
|
+
- [Example 5: Non-numeric dice](#example-5-non-numeric-dice)
|
27
28
|
- [All ways to define dice](#all-ways-to-define-dice)
|
28
29
|
- [Usage: API](#usage-api)
|
29
30
|
- [Dice](#dice)
|
@@ -63,7 +64,17 @@ gem install dicey
|
|
63
64
|
|
64
65
|
Or, if using Bundler, add it to your `Gemfile`:
|
65
66
|
```rb
|
66
|
-
gem "dicey", "~> 0.
|
67
|
+
gem "dicey", "~> 0.16"
|
68
|
+
```
|
69
|
+
|
70
|
+
(Optional) If intending to work with non-numeric dice, install **vector_number** too:
|
71
|
+
```sh
|
72
|
+
gem install vector_number
|
73
|
+
```
|
74
|
+
|
75
|
+
or add it to your `Gemfile`:
|
76
|
+
```rb
|
77
|
+
gem "vector_number"
|
67
78
|
```
|
68
79
|
|
69
80
|
> [!TIP]
|
@@ -76,6 +87,7 @@ gem "dicey", "~> 0.14"
|
|
76
87
|
|
77
88
|
**Dicey** is tested to work on CRuby 3.0+, latest JRuby and TruffleRuby. Compatible implementations should work too.
|
78
89
|
- JSON and YAML formatting require `json` and `yaml`.
|
90
|
+
- Non-numeric dice require gem `vector_number` to be installed.
|
79
91
|
|
80
92
|
Otherwise, there are no direct dependencies.
|
81
93
|
|
@@ -238,18 +250,35 @@ roll => 7/2 # You probably will get a different value here.
|
|
238
250
|
> [!NOTE]
|
239
251
|
> ๐ก Roll mode is compatible with `--format` option.
|
240
252
|
|
253
|
+
### Example 5: Non-numeric dice
|
254
|
+
|
255
|
+
You are a wizard and you have a spellbook with an elemental vortex spell that deals three instances of random elemental damage. Let's find out what you have for your enemies in store today:
|
256
|
+
```sh
|
257
|
+
$ dicey 3dโ๏ธ,๐ฅ,โก๏ธ,๐ช,๐ฒ -m r
|
258
|
+
# (โ๏ธ,๐ฅ,โก๏ธ,๐ช,๐ฒ)+(โ๏ธ,๐ฅ,โก๏ธ,๐ช,๐ฒ)+(โ๏ธ,๐ฅ,โก๏ธ,๐ช,๐ฒ)
|
259
|
+
roll => 1โ
๐ช + 1โ
๐ฒ + 1โ
โก๏ธ
|
260
|
+
```
|
261
|
+
|
262
|
+
Wind, wood and lightning in equal proportion it is! Your enemies will tremble!
|
263
|
+
|
264
|
+
Regrettably, it is not possible to use elemental dice without installing **vector_number** gem first.
|
265
|
+
|
241
266
|
### All ways to define dice
|
242
267
|
|
243
|
-
There are
|
268
|
+
There are four *main* ways to define dice:
|
244
269
|
- *"5", "25", or "525"*: a single positive integer makes a regular die (like a D20).
|
245
270
|
- *"3-6", "-5..5", "(0-1)"*: a pair of integers with a separator, possibly in round brackets, makes a numeric die with integers in the range.
|
246
271
|
- Accepted separators: "-", "..", "...", "โ" (en dash), "โ" (em dash), "โฆ" (ellipsis).
|
247
|
-
- *"1,2,4", "(-1.5,0,1.5)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes
|
272
|
+
- *"1,2,4", "(-1.5,0,1.5)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes a custom numeric die.
|
248
273
|
- Lists can end in a comma, allowing single-number lists.
|
274
|
+
- *"1,1.5,Two", "(๐,๐งก,๐,๐)" or "('1','(bracket)')"*: a list of strings and numbers separated by commas, possibly in round brackets, makes an arbitrary die.
|
275
|
+
- Lists can end in a comma, allowing single-string lists.
|
276
|
+
- Single (') or double (") quotes can be used to use other quotes and round brackets in the string. Otherwise, they are prohibited. Commas are always prohibited.
|
277
|
+
- Quotes can also be used to treat numbers as strings.
|
249
278
|
|
250
|
-
*"D6", "d(-1,3)",
|
279
|
+
*"D6", "d(-1,3)", "d2..4", or "d๐,๐งก"*: any definitions can be prefixed with "d" or "D". While this doesn't do anything on its own, it can be useful to not start a definition with "-".
|
251
280
|
|
252
|
-
*"2D6", "5d-1,3",
|
281
|
+
*"2D6", "5d-1,3", "277D(2..4)", or "3d๐,โ ๏ธ,โฅ๏ธ,โฃ๏ธ,โฆ๏ธ,โ๏ธ"*: any definitions can be prefixed with "*N*d" or "*N*D", where *N* is a positive integer. This creates *N* copies of the die.
|
253
282
|
|
254
283
|
## Usage: API
|
255
284
|
|
@@ -260,7 +289,7 @@ There are three *main* ways to define dice:
|
|
260
289
|
### Dice
|
261
290
|
|
262
291
|
There are 3 classes of dice currently:
|
263
|
-
- `Dicey::AbstractDie` is the base class for other dice, but can be used on its own. It has no restrictions on values of sides.
|
292
|
+
- `Dicey::AbstractDie` is the base class for other dice, but can be used on its own. It has no restrictions on values of sides.
|
264
293
|
- `Dicey::NumericDie` behaves much the same as `Dicey::AbstractDie` (being its subclass), except for checking that all values are instances of `Numeric`. It can be initialized with an Array or Range.
|
265
294
|
- `Dicey::RegularDie` is a specialized subclass of `Dicey::NumericDie`. It is defined by a single integer *N* which is expanded to a range (1..*N*).
|
266
295
|
|
@@ -282,7 +311,7 @@ Dicey::DieFoundry.new.call("100")
|
|
282
311
|
Dicey::DieFoundry.new.call("2d6")
|
283
312
|
# same as Dicey::RegularDie.from_count(2, 6)
|
284
313
|
Dicey::DieFoundry.new.call("1d1,2,4")
|
285
|
-
# same as Dicey::NumericDie.
|
314
|
+
# same as Dicey::NumericDie.from_count(1, [1,2,4])
|
286
315
|
```
|
287
316
|
|
288
317
|
It only takes a single argument and may return both an array of dice and a single die. You will probably want to use `Enumerable#flat_map`:
|
@@ -292,6 +321,26 @@ foundry = Dicey::DieFoundry.new
|
|
292
321
|
# same as [Dicey::RegularDie.new(8), *Dicey::RegularDie.from_count(2, 4)]
|
293
322
|
```
|
294
323
|
|
324
|
+
#### Really custom dice
|
325
|
+
|
326
|
+
It is easy enough to create numeric dice or dice with distinct symbols. However, what if a symbolic die is needed, but one which also has custom counts of symbols?
|
327
|
+
|
328
|
+
For example, a game may have a die which can roll 1-3 ๐ or a โฅ๏ธ. You could just use completely different strings for the different numbers of hearts, but they would not be summable (what if you need to roll two such dice and add them together?). In this case, you can directly use `VectorNumber` to create summable strings:
|
329
|
+
```rb
|
330
|
+
# Using Symbols is not required, but they look nicer in output.
|
331
|
+
# `DieFoundry` uses Symbols for this reason.
|
332
|
+
heal = VectorNumber[:"๐"]
|
333
|
+
regen = VectorNumber[:"โฅ๏ธ"]
|
334
|
+
die = Dicey::AbstractDie.new([heal, heal * 2, heal * 3, regen])
|
335
|
+
# => #<Dicey::AbstractDie:0x00007f4a7c95efe8 @current_side_index=0, @sides_list=[(1โ
๐), (2โ
๐), (3โ
๐), (1โ
โฅ๏ธ)], @sides_num=4>
|
336
|
+
```
|
337
|
+
|
338
|
+
Now such dice can easily be rolled together and results summed:
|
339
|
+
```rb
|
340
|
+
die.roll + die.roll
|
341
|
+
# => (5โ
๐)
|
342
|
+
```
|
343
|
+
|
295
344
|
### Rolling
|
296
345
|
|
297
346
|
`Dicey::AbstractDie#roll` implements the rolling:
|
@@ -338,18 +387,18 @@ die.roll
|
|
338
387
|
Distribution calculators live in `Dicey::SumFrequencyCalculators` module. There are four calculators currently:
|
339
388
|
- `Dicey::SumFrequencyCalculators::KroneckerSubstitution` is the recommended calculator, able to handle all `Dicey::RegularDie`. It is very fast, calculating distribution for *100d6* in about 0.1 seconds on a laptop.
|
340
389
|
- `Dicey::SumFrequencyCalculators::MultinomialCoefficients` is specialized for repeated numeric dice, with performance only slightly worse. However, it is currently limited to dice with arithmetic sequences.
|
341
|
-
- `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can
|
342
|
-
- `Dicey::SumFrequencyCalculators::Empirical
|
390
|
+
- `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can work with *any* dice. It needs gem "**vector_number**" to be installed and available to work with non-numeric dice.
|
391
|
+
- `Dicey::SumFrequencyCalculators::Empirical`... this is more of a tool than a calculator. It "calculates" probabilities by performing a large number of rolls and counting frequencies of outcomes. It can be interesting to play around with and see how practical results compare to theoretical ones. Due to its simplicity, it also works with *any* dice.
|
343
392
|
|
344
393
|
Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
|
345
394
|
- `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
|
346
395
|
- `#valid_for?(dice) : Boolean`
|
347
396
|
|
348
|
-
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations.
|
397
|
+
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations of different algorithms.
|
349
398
|
|
350
399
|
### Distribution properties
|
351
400
|
|
352
|
-
While distribution itself is already enough in most cases (we are talking just dice here, after all). it may be of interest to calculate properties of it: mode, mean, expected value, standard deviation, etc. `Dicey::DistributionPropertiesCalculator`
|
401
|
+
While distribution itself is already enough in most cases (we are talking just dice here, after all). it may be of interest to calculate properties of it: mode, mean, expected value, standard deviation, etc. `Dicey::DistributionPropertiesCalculator` provides this functionality:
|
353
402
|
```rb
|
354
403
|
Dicey::DistributionPropertiesCalculator.new.call(
|
355
404
|
Dicey::SumFrequencyCalculators::KroneckerSubstitution.new.call(
|
@@ -358,18 +407,19 @@ Dicey::DistributionPropertiesCalculator.new.call(
|
|
358
407
|
)
|
359
408
|
# =>
|
360
409
|
# {:mode=>[4],
|
361
|
-
#
|
362
|
-
#
|
363
|
-
#
|
364
|
-
#
|
365
|
-
#
|
366
|
-
#
|
367
|
-
#
|
368
|
-
#
|
369
|
-
#
|
370
|
-
#
|
371
|
-
#
|
372
|
-
#
|
410
|
+
# :modes=>[[4]]
|
411
|
+
# :min=>2,
|
412
|
+
# :max=>6,
|
413
|
+
# :total_range=>4,
|
414
|
+
# :mid_range=>4,
|
415
|
+
# :median=>4,
|
416
|
+
# :arithmetic_mean=>4,
|
417
|
+
# :expected_value=>4,
|
418
|
+
# :variance=>(4/3),
|
419
|
+
# :standard_deviation=>1.1547005383792515,
|
420
|
+
# :skewness=>0.0,
|
421
|
+
# :kurtosis=>(9/4),
|
422
|
+
# :excess_kurtosis=>(-3/4)}
|
373
423
|
```
|
374
424
|
|
375
425
|
Of course, for regular dice most properties are quite simple and predicatable due to symmetricity of distribution. It becomes more interesting with unfair, lopsided dice. Remember [Example 3](#example-3-custom-dice)?
|
@@ -381,21 +431,30 @@ Dicey::DistributionPropertiesCalculator.new.call(
|
|
381
431
|
)
|
382
432
|
# =>
|
383
433
|
# {:mode=>[5],
|
384
|
-
#
|
385
|
-
#
|
386
|
-
#
|
387
|
-
#
|
388
|
-
#
|
389
|
-
#
|
390
|
-
#
|
391
|
-
#
|
392
|
-
#
|
393
|
-
#
|
394
|
-
#
|
395
|
-
#
|
434
|
+
# :modes=>[[5]],
|
435
|
+
# :min=>2,
|
436
|
+
# :max=>8,
|
437
|
+
# :total_range=>6,
|
438
|
+
# :mid_range=>5,
|
439
|
+
# :median=>5,
|
440
|
+
# :arithmetic_mean=>5,
|
441
|
+
# :expected_value=>(31/6),
|
442
|
+
# :variance=>(101/36),
|
443
|
+
# :standard_deviation=>1.674979270186815,
|
444
|
+
# :skewness=>-0.15762965389465178,
|
445
|
+
# :kurtosis=>(23145/10201),
|
446
|
+
# :excess_kurtosis=>(-7458/10201)}
|
447
|
+
```
|
448
|
+
|
449
|
+
This disitrubution is obviosuly skewed (as can be immediately seen from non-zero skewness), with expected value no longer equal to mean. This is a mild example. It is easily possible to create a distribution with multiple local maxima or high skewness. For example, let's take two D2 and a weighted die to create a distribution with two peaks:
|
450
|
+
```rb
|
451
|
+
[*Dicey::RegularDie.from_count(2, 2), Dicey::NumericDie.new([1,8,9])]
|
452
|
+
# =>
|
453
|
+
# {:mode=>[11, 12],
|
454
|
+
# :modes=>[[4], [11, 12]],
|
396
455
|
```
|
397
456
|
|
398
|
-
|
457
|
+
You can see that 11 and 12 are the most likely outcomes, coming from a larger peak, but a smaller peak (with lower probability) is placed at 4.
|
399
458
|
|
400
459
|
## Diving deeper
|
401
460
|
|
@@ -418,7 +477,7 @@ Dicey is in principle able to handle any real numeric dice and some abstract dic
|
|
418
477
|
Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
|
419
478
|
|
420
479
|
> [!NOTE]
|
421
|
-
> ๐ก Complexity is listed for
|
480
|
+
> ๐ก Complexity is listed for **n** dice with at most **m** sides and has not been rigorously proven.
|
422
481
|
|
423
482
|
### Kronecker substitution
|
424
483
|
|
@@ -426,7 +485,7 @@ An algorithm based on fast polynomial multiplication. This is the default algori
|
|
426
485
|
|
427
486
|
- Limitations: only **natural** dice are allowed, including **regular** dice.
|
428
487
|
- Example: `dicey 5 3,4,1 0,`
|
429
|
-
- Complexity:
|
488
|
+
- Complexity: **O(mโ
n)** where **m** is the highest value
|
430
489
|
|
431
490
|
### Multinomial coefficients
|
432
491
|
|
@@ -434,15 +493,15 @@ This algorithm is based on raising a univariate polynomial to a power and using
|
|
434
493
|
|
435
494
|
- Limitations: only *equal* **arithmetic** dice are allowed.
|
436
495
|
- Example: `dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6`
|
437
|
-
- Complexity:
|
496
|
+
- Complexity: **O(mโ
nยฒ)**
|
438
497
|
|
439
498
|
### Brute force
|
440
499
|
|
441
500
|
As a last resort, there is a brute force algorithm which goes through every possible dice roll and adds results together. While quickly growing terrible in performace, it has the largest input space, allowing to work with completely nonsensical dice, including aforementioned dice with complex numbers.
|
442
501
|
|
443
|
-
- Limitations:
|
502
|
+
- Limitations: without **vector_number** all values must be numbers, otherwise almost any values are viable.
|
444
503
|
- Example: `dicey 5 1,0.1,2 1,-1,1,-1,0`
|
445
|
-
- Complexity:
|
504
|
+
- Complexity: **O(mโฟ)**
|
446
505
|
|
447
506
|
## Development
|
448
507
|
|
data/lib/dicey/abstract_die.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Dicey
|
4
4
|
# Asbtract die which may have an arbitrary list of sides,
|
5
|
-
# not even neccessarily numbers
|
5
|
+
# not even neccessarily numbers, but strings or other objects.
|
6
6
|
class AbstractDie
|
7
7
|
# rubocop:disable Style/ClassVars
|
8
8
|
|
@@ -69,9 +69,11 @@ module Dicey
|
|
69
69
|
# @param sides_list [Enumerable<Any>]
|
70
70
|
# @raise [DiceyError] if +sides_list+ is empty
|
71
71
|
def initialize(sides_list)
|
72
|
-
@sides_list =
|
72
|
+
@sides_list = sides_list.to_a
|
73
|
+
@sides_list = @sides_list.dup if @sides_list.equal?(sides_list)
|
73
74
|
raise DiceyError, "dice must have at least one side!" if @sides_list.empty?
|
74
75
|
|
76
|
+
@sides_list.freeze
|
75
77
|
@sides_num = @sides_list.size
|
76
78
|
@current_side_index = 0
|
77
79
|
end
|
@@ -97,7 +99,7 @@ module Dicey
|
|
97
99
|
#
|
98
100
|
# @return [Any] rolled side
|
99
101
|
def roll
|
100
|
-
@current_side_index = self.class.rand(
|
102
|
+
@current_side_index = self.class.rand(@sides_num)
|
101
103
|
current
|
102
104
|
end
|
103
105
|
|
data/lib/dicey/die_foundry.rb
CHANGED
@@ -3,27 +3,37 @@
|
|
3
3
|
require_relative "numeric_die"
|
4
4
|
require_relative "regular_die"
|
5
5
|
|
6
|
-
require_relative "rational_to_integer"
|
6
|
+
require_relative "mixins/rational_to_integer"
|
7
7
|
|
8
8
|
module Dicey
|
9
9
|
# Helper class to define die definitions and automatically select the best one.
|
10
10
|
class DieFoundry
|
11
|
-
include RationalToInteger
|
11
|
+
include Mixins::RationalToInteger
|
12
12
|
|
13
13
|
# Regexp for matching a possible count.
|
14
|
-
PREFIX = /(
|
14
|
+
PREFIX = /(?:(?<count>[1-9]\d*+)?+d)?+/i
|
15
|
+
# Regexp for an integer number.
|
16
|
+
INTEGER = /(?:-?\d++)/
|
17
|
+
# Regexp for a (possibly) fractional number.
|
18
|
+
DECIMAL = /(?:-?\d++(?:\.\d++)?)/
|
19
|
+
# Regexp for an "arbitrary" string.
|
20
|
+
STRING = /(?:(?<side>[^"',()]++)|"(?<side>[^",]++)"|'(?<side>[^',]++)')/
|
15
21
|
|
16
22
|
# Possible molds for the dice. They are matched in the order as written.
|
17
23
|
MOLDS = [
|
18
24
|
# Positive integer goes into the RegularDie mold.
|
19
25
|
[/\A#{PREFIX}(?<sides>[1-9]\d*+)\z/, :regular_mold].freeze,
|
20
26
|
# Integer range goes into the NumericDie mold.
|
21
|
-
[/\A#{PREFIX}\(?(?<begin
|
27
|
+
[/\A#{PREFIX}\(?(?<begin>#{INTEGER})(?:[-โโโฆ]|\.{2,3})(?<end>#{INTEGER})\)?\z/,
|
28
|
+
:range_mold].freeze,
|
22
29
|
# List of numbers goes into the NumericDie mold.
|
23
|
-
[/\A#{PREFIX}\(?(?<sides
|
30
|
+
[/\A#{PREFIX}\(?(?<sides>#{INTEGER}(?:(?:,#{INTEGER})++,?+|,))\)?\z/,
|
31
|
+
:weirdly_shaped_mold].freeze,
|
24
32
|
# Non-integers require special handling for precision.
|
25
|
-
[/\A#{PREFIX}\(?(?<sides
|
33
|
+
[/\A#{PREFIX}\(?(?<sides>#{DECIMAL}(?:(?:,#{DECIMAL})++,?+|,))\)?\z/,
|
26
34
|
:weirdly_precise_mold].freeze,
|
35
|
+
# Lists of stuff are broken into AbstractDie.
|
36
|
+
[/\A#{PREFIX}\(?(?<sides>#{STRING}(?:(?:,#{STRING})++,?+|,))\)?\z/, :cursed_mold].freeze,
|
27
37
|
# Anything else is spilled on the floor.
|
28
38
|
].freeze
|
29
39
|
|
@@ -34,13 +44,16 @@ module Dicey
|
|
34
44
|
# - integer range (like "3-6" or "(-5..5)"), which produces a {NumericDie};
|
35
45
|
# - list of integers (like "3,4,5", "(-1,0,1)", or "2,"), which produces a {NumericDie};
|
36
46
|
# - list of decimal numbers (like "0.5,0.2,0.8" or "(2.0,)"), which produces a {NumericDie},
|
37
|
-
# but uses +Rational+ for values to maintain precise results
|
47
|
+
# but uses +Rational+ for values to maintain precise results;
|
48
|
+
# - list of strings, possibly mixed with numbers (like "0.5,asdf" or "(๐,โ ๏ธ,โฅ๏ธ,โฃ๏ธ,โฆ๏ธ,โ๏ธ)"),
|
49
|
+
# which produces an {AbstractDie} with strings converted to Symbols
|
50
|
+
# and numbers treated the same as in previous cases.
|
38
51
|
#
|
39
52
|
# Any die definition can be prefixed with a count, like "2D6" or "1d1,3,5" to create an array.
|
40
53
|
# A plain "d" without an explicit count is ignored instead, creating a single die.
|
41
54
|
#
|
42
55
|
# @param definition [String] die shape
|
43
|
-
# @return [
|
56
|
+
# @return [AbstractDie, Array<AbstractDie>]
|
44
57
|
# @raise [DiceyError] if no mold fits the definition
|
45
58
|
def call(definition)
|
46
59
|
matched, name =
|
@@ -77,6 +90,21 @@ module Dicey
|
|
77
90
|
build_dice(NumericDie, definition[:count], sides)
|
78
91
|
end
|
79
92
|
|
93
|
+
def cursed_mold(definition)
|
94
|
+
sides = definition[:sides].split(",")
|
95
|
+
sides.map! do |side|
|
96
|
+
case side
|
97
|
+
when /\A#{INTEGER}\z/o
|
98
|
+
side.to_i
|
99
|
+
when /\A#{DECIMAL}\z/o
|
100
|
+
rational_to_integer(Rational(side))
|
101
|
+
else
|
102
|
+
side.match(STRING)[:side].to_sym
|
103
|
+
end
|
104
|
+
end
|
105
|
+
build_dice(AbstractDie, definition[:count], sides)
|
106
|
+
end
|
107
|
+
|
80
108
|
def build_dice(die_class, count, sides)
|
81
109
|
if count
|
82
110
|
die_class.from_count(count.to_i, sides)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "rational_to_integer"
|
3
|
+
require_relative "mixins/rational_to_integer"
|
4
4
|
|
5
5
|
module Dicey
|
6
6
|
# Calculates distribution properties,
|
@@ -16,7 +16,7 @@ module Dicey
|
|
16
16
|
# (median, mean, ...) are all equal.
|
17
17
|
# Mode is often not unique, but includes this center.
|
18
18
|
class DistributionPropertiesCalculator
|
19
|
-
include RationalToInteger
|
19
|
+
include Mixins::RationalToInteger
|
20
20
|
|
21
21
|
# Calculate properties for a given distribution.
|
22
22
|
#
|
@@ -27,7 +27,7 @@ module Dicey
|
|
27
27
|
#
|
28
28
|
# @param distribution [Hash{Numeric => Numeric}, Hash{Any => Numeric}]
|
29
29
|
# distribution with pre-sorted keys
|
30
|
-
# @return [Hash{Symbol =>
|
30
|
+
# @return [Hash{Symbol => Any}]
|
31
31
|
def call(distribution)
|
32
32
|
return {} if distribution.empty?
|
33
33
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
# @api private
|
5
|
+
# Various mixins with shared methods.
|
6
|
+
module Mixins
|
7
|
+
# Mix-in for converting rationals with denominator of 1 to integers.
|
8
|
+
module RationalToInteger
|
9
|
+
private
|
10
|
+
|
11
|
+
# Convert +value+ to +Integer+ if it's a +Rational+ with denominator of 1.
|
12
|
+
# Otherwise, return +value+ as-is.
|
13
|
+
#
|
14
|
+
# @value [Numeric, Any]
|
15
|
+
# @return [Numeric, Integer, Any]
|
16
|
+
def rational_to_integer(value)
|
17
|
+
(Rational === value && value.denominator == 1) ? value.numerator : value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
module Mixins
|
5
|
+
# Mix-in for converting dice with non-numeric sides into dice with +VectorNumber+ sides.
|
6
|
+
module VectorizeDice
|
7
|
+
private
|
8
|
+
|
9
|
+
# Vectorize non-numeric sides for AbstractDie instances,
|
10
|
+
# leaving NumericDie instances unchanged.
|
11
|
+
#
|
12
|
+
# Check for VectorNumber availability *before* calling.
|
13
|
+
#
|
14
|
+
# @param dice [Array<AbstractDie>]
|
15
|
+
# @return [Array<AbstractDie>] a new array of dice
|
16
|
+
def vectorize_dice(dice)
|
17
|
+
dice.map do |die|
|
18
|
+
next die if NumericDie === die
|
19
|
+
|
20
|
+
sides =
|
21
|
+
die.sides_list.map do |side|
|
22
|
+
(Numeric === side || VectorNumber === side) ? side : VectorNumber.new([side])
|
23
|
+
end
|
24
|
+
die.class.new(sides)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/dicey/numeric_die.rb
CHANGED
@@ -4,8 +4,12 @@ require_relative "abstract_die"
|
|
4
4
|
|
5
5
|
module Dicey
|
6
6
|
# A die which only has numeric sides, with no shenanigans.
|
7
|
+
#
|
8
|
+
# The only inherent difference in behavior compared to {AbstractDie} is
|
9
|
+
# that this class checks values for sides on initialization.
|
10
|
+
# However, other classes may reject {AbstractDie} even with all numeric sides.
|
7
11
|
class NumericDie < AbstractDie
|
8
|
-
# @param sides_list [Enumerable<Numeric>]
|
12
|
+
# @param sides_list [Array<Numeric>, Range<Numeric>, Enumerable<Numeric>]
|
9
13
|
# @raise [DiceyError] if +sides_list+ contains non-numerical values or is empty
|
10
14
|
def initialize(sides_list)
|
11
15
|
if Range === sides_list
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../rational_to_integer"
|
3
|
+
require_relative "../mixins/rational_to_integer"
|
4
4
|
|
5
5
|
module Dicey
|
6
6
|
module OutputFormatters
|
@@ -8,7 +8,7 @@ module Dicey
|
|
8
8
|
# Can add an optional description into the result.
|
9
9
|
# @abstract
|
10
10
|
class KeyValueFormatter
|
11
|
-
include RationalToInteger
|
11
|
+
include Mixins::RationalToInteger
|
12
12
|
|
13
13
|
# @param hash [Hash{Object => Object}]
|
14
14
|
# @param description [String] text to add as a comment.
|
data/lib/dicey/regular_die.rb
CHANGED
@@ -4,6 +4,8 @@ require_relative "numeric_die"
|
|
4
4
|
|
5
5
|
module Dicey
|
6
6
|
# Regular die, which has N sides with numbers from 1 to N.
|
7
|
+
#
|
8
|
+
# As a subclass of {NumericDie}, enjoys its treatment.
|
7
9
|
class RegularDie < NumericDie
|
8
10
|
# @param max [Integer] maximum side / number of sides
|
9
11
|
def initialize(max)
|
data/lib/dicey/roller.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Try to load "vector_number" pre-emptively.
|
4
|
+
begin
|
5
|
+
require "vector_number"
|
6
|
+
rescue LoadError
|
7
|
+
# VectorNumber not available, sad
|
8
|
+
end
|
9
|
+
|
3
10
|
require_relative "die_foundry"
|
4
11
|
|
5
|
-
require_relative "rational_to_integer"
|
12
|
+
require_relative "mixins/rational_to_integer"
|
13
|
+
require_relative "mixins/vectorize_dice"
|
6
14
|
|
7
15
|
module Dicey
|
8
16
|
# Let the dice roll!
|
17
|
+
#
|
18
|
+
# This is the implementation of roll mode for the CLI.
|
9
19
|
class Roller
|
10
|
-
include RationalToInteger
|
20
|
+
include Mixins::RationalToInteger
|
21
|
+
include Mixins::VectorizeDice
|
11
22
|
|
12
23
|
# @param arguments [Array<String>] die definitions
|
13
24
|
# @param format [#call] formatter for output
|
@@ -17,9 +28,15 @@ module Dicey
|
|
17
28
|
raise DiceyError, "no dice!" if arguments.empty?
|
18
29
|
|
19
30
|
dice = arguments.flat_map { |definition| die_foundry.cast(definition) }
|
20
|
-
result = dice
|
31
|
+
result = roll_dice(dice)
|
21
32
|
|
22
33
|
format.call({ "roll" => rational_to_integer(result) }, AbstractDie.describe(dice))
|
34
|
+
rescue TypeError
|
35
|
+
warn <<~TEXT
|
36
|
+
Dice with non-numeric sides need gem "vector_number" to be present and available.
|
37
|
+
If this is intended, please install the gem.
|
38
|
+
TEXT
|
39
|
+
raise DiceyError, "can not roll dice with non-numeric sides!"
|
23
40
|
end
|
24
41
|
|
25
42
|
private
|
@@ -27,5 +44,10 @@ module Dicey
|
|
27
44
|
def die_foundry
|
28
45
|
@die_foundry ||= DieFoundry.new
|
29
46
|
end
|
47
|
+
|
48
|
+
def roll_dice(dice)
|
49
|
+
dice = vectorize_dice(dice) if defined?(VectorNumber)
|
50
|
+
dice.sum(&:roll)
|
51
|
+
end
|
30
52
|
end
|
31
53
|
end
|
@@ -27,7 +27,8 @@ module Dicey
|
|
27
27
|
|
28
28
|
# @param dice [Enumerable<AbstractDie>]
|
29
29
|
# @param result_type [Symbol] one of {RESULT_TYPES}
|
30
|
-
# @param options [Hash{Symbol => Any}] calculator-specific options
|
30
|
+
# @param options [Hash{Symbol => Any}] calculator-specific options,
|
31
|
+
# refer to the calculator's documentation to see what it accepts
|
31
32
|
# @return [Hash{Numeric => Numeric}] frequencies or probabilities for each outcome
|
32
33
|
# @raise [DiceyError] if +result_type+ is invalid
|
33
34
|
# @raise [DiceyError] if dice list is invalid for the calculator
|
@@ -1,44 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Try to load "vector_number" pre-emptively.
|
4
|
+
begin
|
5
|
+
require "vector_number"
|
6
|
+
rescue LoadError
|
7
|
+
# VectorNumber not available, sad
|
8
|
+
end
|
9
|
+
|
3
10
|
require_relative "base_calculator"
|
4
11
|
|
12
|
+
require_relative "../mixins/vectorize_dice"
|
13
|
+
|
5
14
|
module Dicey
|
6
15
|
module SumFrequencyCalculators
|
7
|
-
# Calculator for a collection of {
|
16
|
+
# Calculator for a collection of {AbstractDie} using exhaustive search (very slow).
|
17
|
+
#
|
18
|
+
# If dice include non-numeric sides, gem +vector_number+ has to be installed.
|
8
19
|
class BruteForce < BaseCalculator
|
20
|
+
include Mixins::VectorizeDice
|
21
|
+
|
9
22
|
private
|
10
23
|
|
11
24
|
def validate(dice)
|
12
|
-
dice.all?(NumericDie)
|
25
|
+
if defined?(VectorNumber) || dice.all?(NumericDie)
|
26
|
+
true
|
27
|
+
else
|
28
|
+
warn <<~TEXT
|
29
|
+
Dice with non-numeric sides need gem "vector_number" to be present and available.
|
30
|
+
If this is intended, please install the gem.
|
31
|
+
TEXT
|
32
|
+
false
|
33
|
+
end
|
13
34
|
end
|
14
35
|
|
15
36
|
def calculate(dice, **nil)
|
16
|
-
|
37
|
+
dice = vectorize_dice(dice) if defined?(VectorNumber)
|
38
|
+
combine_dice_enumerators(dice.map(&:sides_list)).map(&:sum).tally
|
17
39
|
end
|
18
40
|
|
19
41
|
if defined?(Enumerator::Product)
|
20
42
|
# Get an enumerator which goes through all possible permutations of dice sides.
|
21
43
|
#
|
22
|
-
# @param
|
23
|
-
# @return [Enumerator<
|
24
|
-
def combine_dice_enumerators(
|
25
|
-
Enumerator::Product.new(*
|
44
|
+
# @param side_lists [Enumerable<Enumerable<Any>>]
|
45
|
+
# @return [Enumerator<Enumerable<Enumerable<Any>>>]
|
46
|
+
def combine_dice_enumerators(side_lists)
|
47
|
+
Enumerator::Product.new(*side_lists)
|
26
48
|
end
|
27
49
|
# :nocov:
|
28
50
|
else
|
29
51
|
# Get an enumerator which goes through all possible permutations of dice sides.
|
30
52
|
#
|
31
|
-
# @param
|
32
|
-
# @return [Enumerator<Array<
|
33
|
-
def combine_dice_enumerators(
|
34
|
-
product(
|
53
|
+
# @param side_lists [Enumerable<Enumerable<Any>>]
|
54
|
+
# @return [Enumerator<Array<Array<Any>>>]
|
55
|
+
def combine_dice_enumerators(side_lists)
|
56
|
+
product(side_lists)
|
35
57
|
end
|
36
58
|
|
37
59
|
# Simplified implementation of {Enumerator::Product}.
|
38
60
|
# Adapted from {https://bugs.ruby-lang.org/issues/18685#note-10}.
|
39
61
|
#
|
40
|
-
# @param enums [Enumerable<Enumerable<
|
41
|
-
# @return [Enumerator<Array<
|
62
|
+
# @param enums [Enumerable<Enumerable<Any>>]
|
63
|
+
# @return [Enumerator<Array<Array<Any>>>]
|
42
64
|
def product(enums, &block)
|
43
65
|
return to_enum(__method__, enums) unless block_given?
|
44
66
|
|
@@ -1,10 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Try to load "vector_number" pre-emptively.
|
4
|
+
begin
|
5
|
+
require "vector_number"
|
6
|
+
rescue LoadError
|
7
|
+
# VectorNumber not available, sad
|
8
|
+
end
|
9
|
+
|
3
10
|
require_relative "base_calculator"
|
4
11
|
|
12
|
+
require_relative "../mixins/vectorize_dice"
|
13
|
+
|
5
14
|
module Dicey
|
6
15
|
module SumFrequencyCalculators
|
7
|
-
# "Calculator" for a collection of {
|
16
|
+
# "Calculator" for a collection of {AbstractDie} using empirically-obtained statistics.
|
8
17
|
#
|
9
18
|
# @note This calculator is mostly a joke. It can be useful for educational purposes,
|
10
19
|
# or to verify results of {BruteForce} when in doubt. It is not used by default.
|
@@ -12,19 +21,32 @@ module Dicey
|
|
12
21
|
# Does a number of rolls and calculates approximate probabilities from that.
|
13
22
|
# Even if frequencies are requested, results are non-integer.
|
14
23
|
#
|
24
|
+
# If dice include non-numeric sides, gem +vector_number+ has to be installed.
|
25
|
+
#
|
15
26
|
# *Options:*
|
16
27
|
# - *rolls* (Integer) (_defaults_ _to:_ _N_) โ number of rolls to perform
|
17
28
|
class Empirical < BaseCalculator
|
29
|
+
include Mixins::VectorizeDice
|
30
|
+
|
18
31
|
# Default number of rolls to perform.
|
19
32
|
N = 10_000
|
20
33
|
|
21
34
|
private
|
22
35
|
|
23
36
|
def validate(dice)
|
24
|
-
dice.all?(NumericDie)
|
37
|
+
if defined?(VectorNumber) || dice.all?(NumericDie)
|
38
|
+
true
|
39
|
+
else
|
40
|
+
warn <<~TEXT
|
41
|
+
Dice with non-numeric sides need gem "vector_number" to be present and available.
|
42
|
+
If this is intended, please install the gem.
|
43
|
+
TEXT
|
44
|
+
false
|
45
|
+
end
|
25
46
|
end
|
26
47
|
|
27
48
|
def calculate(dice, rolls: N)
|
49
|
+
dice = vectorize_dice(dice) if defined?(VectorNumber)
|
28
50
|
statistics = rolls.times.with_object(Hash.new(0)) { |_, hash| hash[dice.sum(&:roll)] += 1 }
|
29
51
|
total_results = dice.map(&:sides_num).reduce(:*)
|
30
52
|
statistics.transform_values { Rational(_1 * total_results, rolls) }
|
@@ -11,6 +11,7 @@ module Dicey
|
|
11
11
|
# Based on extension of Pascal's triangle for a higher number of coefficients.
|
12
12
|
# @see https://en.wikipedia.org/wiki/Pascal's_triangle
|
13
13
|
# @see https://en.wikipedia.org/wiki/Trinomial_triangle
|
14
|
+
# @see https://en.wikipedia.org/wiki/Multinomial_distribution
|
14
15
|
class MultinomialCoefficients < BaseCalculator
|
15
16
|
private
|
16
17
|
|
@@ -42,6 +42,25 @@ module Dicey
|
|
42
42
|
{ Complex(1, 1) => 1, Complex(2, 1) => 1, Complex(3, 1) => 1,
|
43
43
|
Complex(1, 2) => 1, Complex(2, 2) => 1, Complex(3, 2) => 1,
|
44
44
|
Complex(1, 3) => 1, Complex(2, 3) => 1, Complex(3, 3) => 1 }],
|
45
|
+
*(
|
46
|
+
# :nocov:
|
47
|
+
if defined?(VectorNumber)
|
48
|
+
# :nocov:
|
49
|
+
[
|
50
|
+
[[["s", "a", "d", 33]],
|
51
|
+
{ VectorNumber["s"] => 1, VectorNumber["a"] => 1, VectorNumber["d"] => 1, 33 => 1 }],
|
52
|
+
[[%w[s a d], [0, 1, 2]],
|
53
|
+
{ VectorNumber["s"] => 1, VectorNumber["s", 1] => 1, VectorNumber["s", 2] => 1,
|
54
|
+
VectorNumber["a"] => 1, VectorNumber["a", 1] => 1, VectorNumber["a", 2] => 1,
|
55
|
+
VectorNumber["d"] => 1, VectorNumber["d", 1] => 1, VectorNumber["d", 2] => 1 }],
|
56
|
+
[Array.new(2) { ["s", "a", 4] },
|
57
|
+
{
|
58
|
+
VectorNumber["s"] * 2 => 1, VectorNumber["a"] * 2 => 1, 8 => 1,
|
59
|
+
VectorNumber["s", "a"] => 2, VectorNumber["s", 4] => 2, VectorNumber["a", 4] => 2,
|
60
|
+
}],
|
61
|
+
]
|
62
|
+
end
|
63
|
+
),
|
45
64
|
].freeze
|
46
65
|
|
47
66
|
# Strings for displaying test results.
|
@@ -84,7 +103,15 @@ module Dicey
|
|
84
103
|
# @param definition [Array<Integer, Array<Integer>>]
|
85
104
|
# @return [Array<NumericDie>]
|
86
105
|
def build_dice(definition)
|
87
|
-
definition.map
|
106
|
+
definition.map do |die_def|
|
107
|
+
if die_def.is_a?(Integer)
|
108
|
+
RegularDie.new(die_def)
|
109
|
+
elsif die_def.all?(Numeric)
|
110
|
+
NumericDie.new(die_def)
|
111
|
+
else
|
112
|
+
AbstractDie.new(die_def)
|
113
|
+
end
|
114
|
+
end
|
88
115
|
end
|
89
116
|
|
90
117
|
# Determine test result for the selected calculator.
|
data/lib/dicey/version.rb
CHANGED
data/lib/dicey.rb
CHANGED
@@ -5,6 +5,7 @@ module Dicey
|
|
5
5
|
# General error for Dicey.
|
6
6
|
class DiceyError < StandardError; end
|
7
7
|
|
8
|
+
Dir["dicey/mixins/*.rb", base: __dir__].each { require_relative _1 }
|
8
9
|
Dir["dicey/*.rb", base: __dir__].each { require_relative _1 }
|
9
10
|
Dir["dicey/output_formatters/*.rb", base: __dir__].each { require_relative _1 }
|
10
11
|
Dir["dicey/sum_frequency_calculators/*.rb", base: __dir__].each { require_relative _1 }
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dicey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandr Bulancov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-10-
|
12
|
-
dependencies:
|
11
|
+
date: 2025-10-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: vector_number
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.4.3
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.4.3
|
13
27
|
description: |
|
14
28
|
Dicey provides a CLI executable and a Ruby API for fast calculation of
|
15
29
|
frequency/probability distributions of dice rolls,
|
@@ -36,6 +50,8 @@ files:
|
|
36
50
|
- lib/dicey/cli/options.rb
|
37
51
|
- lib/dicey/die_foundry.rb
|
38
52
|
- lib/dicey/distribution_properties_calculator.rb
|
53
|
+
- lib/dicey/mixins/rational_to_integer.rb
|
54
|
+
- lib/dicey/mixins/vectorize_dice.rb
|
39
55
|
- lib/dicey/numeric_die.rb
|
40
56
|
- lib/dicey/output_formatters/gnuplot_formatter.rb
|
41
57
|
- lib/dicey/output_formatters/hash_formatter.rb
|
@@ -43,7 +59,6 @@ files:
|
|
43
59
|
- lib/dicey/output_formatters/key_value_formatter.rb
|
44
60
|
- lib/dicey/output_formatters/list_formatter.rb
|
45
61
|
- lib/dicey/output_formatters/yaml_formatter.rb
|
46
|
-
- lib/dicey/rational_to_integer.rb
|
47
62
|
- lib/dicey/regular_die.rb
|
48
63
|
- lib/dicey/roller.rb
|
49
64
|
- lib/dicey/sum_frequency_calculators/base_calculator.rb
|
@@ -60,9 +75,9 @@ licenses:
|
|
60
75
|
metadata:
|
61
76
|
homepage_uri: https://github.com/trinistr/dicey
|
62
77
|
bug_tracker_uri: https://github.com/trinistr/dicey/issues
|
63
|
-
documentation_uri: https://rubydoc.info/gems/dicey/0.
|
64
|
-
source_code_uri: https://github.com/trinistr/dicey/tree/v0.
|
65
|
-
changelog_uri: https://github.com/trinistr/dicey/blob/v0.
|
78
|
+
documentation_uri: https://rubydoc.info/gems/dicey/0.16.0
|
79
|
+
source_code_uri: https://github.com/trinistr/dicey/tree/v0.16.0
|
80
|
+
changelog_uri: https://github.com/trinistr/dicey/blob/v0.16.0/CHANGELOG.md
|
66
81
|
rubygems_mfa_required: 'true'
|
67
82
|
post_install_message:
|
68
83
|
rdoc_options:
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Dicey
|
4
|
-
# @api private
|
5
|
-
# Mix-in for converting rationals with denominator of 1 to integers.
|
6
|
-
module RationalToInteger
|
7
|
-
private
|
8
|
-
|
9
|
-
# Convert +value+ to +Integer+ if it's a +Rational+ with denominator of 1.
|
10
|
-
# Otherwise, return +value+ as-is.
|
11
|
-
#
|
12
|
-
# @value [Numeric, Any]
|
13
|
-
# @return [Numeric, Integer, Any]
|
14
|
-
def rational_to_integer(value)
|
15
|
-
(Rational === value && value.denominator == 1) ? value.numerator : value
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|