From f0029b262698e7f1a1c14d52d661b99cff904431 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sun, 27 Sep 2026 00:58:48 +0700 Subject: [PATCH 1/3] Keep a rational allocation amount exact. A rational was converted through a float, so one part of 1/3 was not 1/3. --- lib/money/money/allocation.rb | 2 +- spec/money/allocation_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/money/money/allocation.rb b/lib/money/money/allocation.rb index 9ef208a7f8..22acc7e2c9 100644 --- a/lib/money/money/allocation.rb +++ b/lib/money/money/allocation.rb @@ -79,7 +79,7 @@ def self.convert_to_big_decimal(number) if number.is_a? BigDecimal number elsif number.is_a? Rational - BigDecimal(number.to_f.to_s) + number.to_d else BigDecimal(number.to_s) end diff --git a/spec/money/allocation_spec.rb b/spec/money/allocation_spec.rb index 264a27ced0..80989e998f 100644 --- a/spec/money/allocation_spec.rb +++ b/spec/money/allocation_spec.rb @@ -42,6 +42,11 @@ expect(described_class.generate(BigDecimal(5), 4, false)).to eq([1.25, 1.25, 1.25, 1.25]) end + it "keeps a rational amount instead of a float approximation" do + amount = Rational(1, 3) + expect(described_class.generate(amount, 1, false)).to eq([amount.to_d]) + end + it "handles splits into repeating decimals" do amount = BigDecimal(100) parts = described_class.generate(amount, 3, false) From 7be84f1248e541530dd8be5882541c7ea8f79538 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Tue, 29 Sep 2026 02:12:07 +0700 Subject: [PATCH 2/3] Use conversion precision when a rational becomes a decimal. Allocation of 1/3 used to_d with no precision. It now uses Money.conversion_precision. --- CHANGELOG.md | 1 + lib/money/money/allocation.rb | 2 +- spec/money/allocation_spec.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 000bc8422a..519ff8fabe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Fix `Money::Allocation.generate` converting a Rational with unlimited `to_d` precision. It now uses `Money.conversion_precision` - Fix `Money::Currency.reset!` keeping the cached key set, so a currency registered before the reset was still accepted by `Money::Currency.new` and raised `NoMethodError` instead of `UnknownCurrency` - Fix `Money::Bank::VariableExchange#import_rates` parsing `:json` with `JSON.load`, which instantiates any class named by a `json_class` key; it now uses `JSON.parse` - Fix deprecation warning for eql? diff --git a/lib/money/money/allocation.rb b/lib/money/money/allocation.rb index 22acc7e2c9..4bfa14e0eb 100644 --- a/lib/money/money/allocation.rb +++ b/lib/money/money/allocation.rb @@ -79,7 +79,7 @@ def self.convert_to_big_decimal(number) if number.is_a? BigDecimal number elsif number.is_a? Rational - number.to_d + number.to_d(Money.conversion_precision) else BigDecimal(number.to_s) end diff --git a/spec/money/allocation_spec.rb b/spec/money/allocation_spec.rb index 80989e998f..48e35a93ba 100644 --- a/spec/money/allocation_spec.rb +++ b/spec/money/allocation_spec.rb @@ -44,7 +44,7 @@ it "keeps a rational amount instead of a float approximation" do amount = Rational(1, 3) - expect(described_class.generate(amount, 1, false)).to eq([amount.to_d]) + expect(described_class.generate(amount, 1, false)).to eq([amount.to_d(Money.conversion_precision)]) end it "handles splits into repeating decimals" do From ad5ab3435c6a081a61daeeedbfde2b0e122ac6fe Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Tue, 29 Sep 2026 21:13:10 +0700 Subject: [PATCH 3/3] Assert the rational allocation values directly. The previous example compared the result to the same to_d call, so it passed on main. 2/3, 1e400, and 1e-400 are now fixed expected values. --- spec/money/allocation_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/money/allocation_spec.rb b/spec/money/allocation_spec.rb index 48e35a93ba..c3e6f8aaff 100644 --- a/spec/money/allocation_spec.rb +++ b/spec/money/allocation_spec.rb @@ -42,9 +42,10 @@ expect(described_class.generate(BigDecimal(5), 4, false)).to eq([1.25, 1.25, 1.25, 1.25]) end - it "keeps a rational amount instead of a float approximation" do - amount = Rational(1, 3) - expect(described_class.generate(amount, 1, false)).to eq([amount.to_d(Money.conversion_precision)]) + it "converts a rational with Money.conversion_precision instead of through a float" do + expect(described_class.generate(Rational(2, 3), 1, false)).to eq([BigDecimal("0.6666666666666667")]) + expect(described_class.generate(Rational(10**400, 1), 1, false)).to eq([BigDecimal("1e400")]) + expect(described_class.generate(Rational(1, 10**400), 1, false)).to eq([BigDecimal("1e-400")]) end it "handles splits into repeating decimals" do