diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 0ae56d3ecd..eb477313ff 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -12,10 +12,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2'] + ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/AUTHORS b/AUTHORS index c5b4d1d1a1..e8b6a56cbc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -123,6 +123,7 @@ Thomas E Enebo Thomas Weymuth Ticean Bennett Tien Nguyen +Till Grosch Tim Hart Tim Krins Tobias Luetke diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d9a815c6..a661446471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,21 @@ ## Upcoming +- + +## 6.19.0 + - Change Peruvian Sol (PEN) decimal mark and thousands separator. - Fix deprecation warning for BigDecimal being moved out from stdlib. +- Improves Precision and Simplifies Allocation Logic +- explicit declaration of i18n locales +- Add space to CHF format +- Update deprecation message to suggest correct formatting. ## 6.18.0 - Add second dobra (STN) from São Tomé and Príncipe +- Add Sierra Leonean (new) leone (SLE) from Sierra Leone - Correct South African Rand (ZAR) to use comma decimal mark, and space thousands separator - Use euro symbol as html_entity for euro currency - Update Georgian Lari symbol diff --git a/config/currency_iso.json b/config/currency_iso.json index 6ca30ba382..f5e1e142c2 100644 --- a/config/currency_iso.json +++ b/config/currency_iso.json @@ -455,7 +455,7 @@ "format": "%u %n", "html_entity": "", "decimal_mark": ".", - "thousands_separator": ",", + "thousands_separator": "'", "iso_numeric": "756", "smallest_denomination": 5 }, diff --git a/lib/money/money/allocation.rb b/lib/money/money/allocation.rb index 6e47b2d279..542bc8cc92 100644 --- a/lib/money/money/allocation.rb +++ b/lib/money/money/allocation.rb @@ -2,16 +2,17 @@ class Money class Allocation - # Splits a given amount in parts without losing pennies. - # The left-over pennies will be distributed round-robin amongst the parts. This means that - # parts listed first will likely receive more pennies than the ones listed later. + # Splits a given amount in parts. The allocation is based on the parts' proportions + # or evenly if parts are numerically specified. # # The results should always add up to the original amount. # - # The parts can be specified as: - # Numeric — performs the split between a given number of parties evenely - # Array — allocates the amounts proportionally to the given array + # @param amount [Numeric] The total amount to be allocated. + # @param parts [Numeric, Array] Number of parts to split into or an array (proportions for allocation) + # @param whole_amounts [Boolean] Specifies whether to allocate whole amounts only. Defaults to true. # + # @return [Array] An array containing the allocated amounts. + # @raise [ArgumentError] If parts is empty or not provided. def self.generate(amount, parts, whole_amounts = true) parts = if parts.is_a?(Numeric) Array.new(parts, 1) @@ -21,7 +22,12 @@ def self.generate(amount, parts, whole_amounts = true) parts.dup end - raise ArgumentError, 'need at least one party' if parts.empty? + raise ArgumentError, 'need at least one part' if parts.empty? + + if [amount, *parts].any? { |i| i.is_a?(BigDecimal) || i.is_a?(Float) || i.is_a?(Rational) } + amount = convert_to_big_decimal(amount) + parts.map! { |p| convert_to_big_decimal(p) } + end result = [] remaining_amount = amount @@ -40,29 +46,23 @@ def self.generate(amount, parts, whole_amounts = true) remaining_amount -= current_split end - ## round-robin allocation of any remaining pennies - if result.size > 0 - while remaining_amount != 0 - index = 0 - - amount_to_distribute = [1, remaining_amount.abs].min - - if remaining_amount > 0 - result[index] += amount_to_distribute - remaining_amount -= amount_to_distribute - else - result[index] -= amount_to_distribute - remaining_amount += amount_to_distribute - end + result + end - index += 1 - if index > result.size - index = 0 - end - end + # Converts a given number to BigDecimal. + # This method supports inputs of BigDecimal, Rational, and other numeric types by ensuring they are all returned + # as BigDecimal instances for consistent handling. + # + # @param number [Numeric, BigDecimal, Rational] The number to convert. + # @return [BigDecimal] The converted number as a BigDecimal. + def self.convert_to_big_decimal(number) + if number.is_a? BigDecimal + number + elsif number.is_a? Rational + BigDecimal(number.to_f.to_s) + else + BigDecimal(number.to_s) end - - result end end end diff --git a/money.gemspec b/money.gemspec index e1e0c2946d..1ce5461adb 100644 --- a/money.gemspec +++ b/money.gemspec @@ -31,5 +31,6 @@ Gem::Specification.new do |s| s.metadata['changelog_uri'] = 'https://github.com/RubyMoney/money/blob/master/CHANGELOG.md' s.metadata['source_code_uri'] = 'https://github.com/RubyMoney/money/' s.metadata['bug_tracker_uri'] = 'https://github.com/RubyMoney/money/issues' + s.metadata['rubygems_mfa_required'] = 'true' end end diff --git a/spec/bank/base_spec.rb b/spec/bank/base_spec.rb index e5976eff7c..061efac074 100644 --- a/spec/bank/base_spec.rb +++ b/spec/bank/base_spec.rb @@ -32,25 +32,25 @@ def setup describe "#exchange_with" do it "is not implemented" do - expect { subject.exchange_with(Money.new(100, 'USD'), 'EUR') }.to raise_exception(NotImplementedError) + expect { subject.exchange_with(Money.new(100, 'USD'), 'EUR') }.to raise_error(NotImplementedError) end end describe "#same_currency?" do it "accepts str/str" do - expect { subject.send(:same_currency?, 'USD', 'EUR') }.to_not raise_exception + expect { subject.send(:same_currency?, 'USD', 'EUR') }.to_not raise_error end it "accepts currency/str" do - expect { subject.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR') }.to_not raise_exception + expect { subject.send(:same_currency?, Money::Currency.wrap('USD'), 'EUR') }.to_not raise_error end it "accepts str/currency" do - expect { subject.send(:same_currency?, 'USD', Money::Currency.wrap('EUR')) }.to_not raise_exception + expect { subject.send(:same_currency?, 'USD', Money::Currency.wrap('EUR')) }.to_not raise_error end it "accepts currency/currency" do - expect { subject.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR')) }.to_not raise_exception + expect { subject.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR')) }.to_not raise_error end it "returns true when currencies match" do @@ -67,8 +67,8 @@ def setup expect(subject.send(:same_currency?, Money::Currency.wrap('USD'), Money::Currency.wrap('EUR'))).to be false end - it "raises an UnknownCurrency exception when an unknown currency is passed" do - expect { subject.send(:same_currency?, 'AAA', 'BBB') }.to raise_exception(Money::Currency::UnknownCurrency) + it "raises an UnknownCurrency error when an unknown currency is passed" do + expect { subject.send(:same_currency?, 'AAA', 'BBB') }.to raise_error(Money::Currency::UnknownCurrency) end end end diff --git a/spec/bank/single_currency_spec.rb b/spec/bank/single_currency_spec.rb index 9be78aeb5c..93cd318340 100644 --- a/spec/bank/single_currency_spec.rb +++ b/spec/bank/single_currency_spec.rb @@ -3,7 +3,7 @@ it "raises when called" do expect { subject.exchange_with(Money.new(100, 'USD'), 'EUR') - }.to raise_exception(Money::Bank::DifferentCurrencyError, "No exchanging of currencies allowed: 1.00 USD to EUR") + }.to raise_error(Money::Bank::DifferentCurrencyError, "No exchanging of currencies allowed: 1.00 USD to EUR") end end end diff --git a/spec/bank/variable_exchange_spec.rb b/spec/bank/variable_exchange_spec.rb index 2c645dea6f..4237a58e46 100644 --- a/spec/bank/variable_exchange_spec.rb +++ b/spec/bank/variable_exchange_spec.rb @@ -33,11 +33,11 @@ describe "#exchange_with" do it "accepts str" do - expect { bank.exchange_with(Money.new(100, 'USD'), 'EUR') }.to_not raise_exception + expect { bank.exchange_with(Money.new(100, 'USD'), 'EUR') }.to_not raise_error end it "accepts currency" do - expect { bank.exchange_with(Money.new(100, 'USD'), Money::Currency.wrap('EUR')) }.to_not raise_exception + expect { bank.exchange_with(Money.new(100, 'USD'), Money::Currency.wrap('EUR')) }.to_not raise_error end it "exchanges one currency to another" do @@ -48,12 +48,12 @@ expect(bank.exchange_with(Money.new(10, 'USD'), 'EUR')).to eq Money.new(13, 'EUR') end - it "raises an UnknownCurrency exception when an unknown currency is requested" do - expect { bank.exchange_with(Money.new(100, 'USD'), 'BBB') }.to raise_exception(Money::Currency::UnknownCurrency) + it "raises an UnknownCurrency error when an unknown currency is requested" do + expect { bank.exchange_with(Money.new(100, 'USD'), 'BBB') }.to raise_error(Money::Currency::UnknownCurrency) end - it "raises an UnknownRate exception when an unknown rate is requested" do - expect { bank.exchange_with(Money.new(100, 'USD'), 'JPY') }.to raise_exception(Money::Bank::UnknownRate) + it "raises an UnknownRate error when an unknown rate is requested" do + expect { bank.exchange_with(Money.new(100, 'USD'), 'JPY') }.to raise_error(Money::Bank::UnknownRate) end #it "rounds the exchanged result down" do @@ -136,8 +136,8 @@ expect(subject.store.get_rate('USD', 'EUR')).to eq 1.25 end - it "raises an UnknownCurrency exception when an unknown currency is passed" do - expect { subject.set_rate('AAA', 'BBB', 1.25) }.to raise_exception(Money::Currency::UnknownCurrency) + it "raises an UnknownCurrency error when an unknown currency is passed" do + expect { subject.set_rate('AAA', 'BBB', 1.25) }.to raise_error(Money::Currency::UnknownCurrency) end end @@ -147,8 +147,8 @@ expect(subject.get_rate('USD', 'EUR')).to eq 1.25 end - it "raises an UnknownCurrency exception when an unknown currency is passed" do - expect { subject.get_rate('AAA', 'BBB') }.to raise_exception(Money::Currency::UnknownCurrency) + it "raises an UnknownCurrency error when an unknown currency is passed" do + expect { subject.get_rate('AAA', 'BBB') }.to raise_error(Money::Currency::UnknownCurrency) end it "delegates options to store, options are a no-op" do diff --git a/spec/currency_spec.rb b/spec/currency_spec.rb index f91d15186c..2e01018bbc 100644 --- a/spec/currency_spec.rb +++ b/spec/currency_spec.rb @@ -367,7 +367,7 @@ def to_s end it "doesn't create new symbols indefinitely" do - expect { described_class.new("bogus") }.to raise_exception(described_class::UnknownCurrency) + expect { described_class.new("bogus") }.to raise_error(described_class::UnknownCurrency) expect(Symbol.all_symbols.map{|s| s.to_s}).not_to include("bogus") end end diff --git a/spec/money/arithmetic_spec.rb b/spec/money/arithmetic_spec.rb index 336212d9bd..a430caa3fc 100644 --- a/spec/money/arithmetic_spec.rb +++ b/spec/money/arithmetic_spec.rb @@ -673,19 +673,19 @@ it "raises TypeError dividing by a Money (unless other is a Money)" do expect { 2 / Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) end it "raises TypeError subtracting by a Money (unless other is a Money)" do expect { 2 - Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) end it "raises TypeError adding by a Money (unless other is a Money)" do expect { 2 + Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) end it "allows subtraction from numeric zero" do @@ -701,7 +701,7 @@ it "treats multiplication as commutative" do expect { 2 * Money.new(2, 'USD') - }.to_not raise_exception + }.to_not raise_error result = 2 * Money.new(2, 'USD') expect(result).to eq(Money.new(4, 'USD')) end @@ -709,25 +709,25 @@ it "doesn't work with non-numerics" do expect { "2" * Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) end it "correctly handles <=>" do expect { 2 < Money.new(2, 'USD') - }.to raise_exception(ArgumentError) + }.to raise_error(ArgumentError) expect { 2 > Money.new(2, 'USD') - }.to raise_exception(ArgumentError) + }.to raise_error(ArgumentError) expect { 2 <= Money.new(2, 'USD') - }.to raise_exception(ArgumentError) + }.to raise_error(ArgumentError) expect { 2 >= Money.new(2, 'USD') - }.to raise_exception(ArgumentError) + }.to raise_error(ArgumentError) expect(2 <=> Money.new(2, 'USD')).to be_nil end @@ -738,24 +738,23 @@ expect(0.0 >= Money.usd(0)).to eq true end - it "raises exceptions for all numeric types, not just Integer" do + it "raises errors for all numeric types, not just Integer" do expect { 2.0 / Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) expect { Rational(2,3) / Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) expect { BigDecimal(2) / Money.new(2, 'USD') - }.to raise_exception(TypeError) + }.to raise_error(TypeError) end end %w(+ - / divmod remainder).each do |op| describe "##{op}" do - subject { ->(other = self.other) { instance.send(op, other) } } let(:instance) { Money.usd(1) } context 'when conversions disallowed' do @@ -771,12 +770,18 @@ context 'and other is money with different currency' do let(:other) { Money.gbp(1) } - it { should raise_error Money::Bank::DifferentCurrencyError } + + it 'raises Money::Bank::DifferentCurrencyError' do + expect { instance.send(op, other) }.to raise_error Money::Bank::DifferentCurrencyError + end context 'even for zero' do let(:instance) { Money.usd(0) } let(:other) { Money.gbp(0) } - it { should raise_error Money::Bank::DifferentCurrencyError } + + it 'raises Money::Bank::DifferentCurrencyError' do + expect { instance.send(op, other) }.to raise_error Money::Bank::DifferentCurrencyError + end end end end diff --git a/spec/money_spec.rb b/spec/money_spec.rb index d8231b9b5d..bfba7b5255 100644 --- a/spec/money_spec.rb +++ b/spec/money_spec.rb @@ -160,7 +160,7 @@ it "disallows conversions when doing money arithmetic" do Money.disallow_currency_conversion! - expect { Money.new(100, "USD") + Money.new(100, "EUR") }.to raise_exception(Money::Bank::DifferentCurrencyError) + expect { Money.new(100, "USD") + Money.new(100, "EUR") }.to raise_error(Money::Bank::DifferentCurrencyError) end end @@ -417,7 +417,7 @@ def expectation.fractional expect(money.round_to_nearest_cash_value).to eq(-301) end - it "raises an exception if smallest denomination is not defined" do + it "raises an error if smallest denomination is not defined" do money = Money.new(100, "XAG") expect {money.round_to_nearest_cash_value}.to raise_error(Money::UndefinedSmallestDenomination) end