diff --git a/AUTHORS b/AUTHORS index c89896a8d2..8c6c382ced 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,8 +1,8 @@ Abhay Kumar Adrian Longley +Alex Speller Alexander Donkin Alexander Ross -Alex Speller Andreas Loupasakis Andrei Andrew White @@ -49,8 +49,8 @@ Greg Byrne Hakan Ensari Hongli Lai Ilia Lobsanov -Ivan Shamatov Ingo Wichmann +Ivan Shamatov Jack Spiva Jacob Atzen James Cotterill @@ -83,8 +83,8 @@ Marco Otte-Witte Mateus Gomes Mateusz Wolsza Matias Korhonen -Matthew McEachen Matt Jankowski +Matthew McEachen Max Melentiev Michael Irwin Michael J. Cohen @@ -96,14 +96,15 @@ Mike Połétyn Musannif Zahir Neil Middleton Nick Lozon +Nicolay Hvidsten Nihad Abbasov Olek Janiszewski Orien Madgwick -Paweł Madejski Paul McMahon Paulo Diniz Pavan Sudarshan Pavel Gabriel +Paweł Madejski pconnor Pedro Nascimento Pelle Braendgaard @@ -119,9 +120,11 @@ sankaranarayanan Scott Pierce Semyon Perepelitsa Shane Emmons +Simon Neutert Simone Carletti Spencer Rinehart Steve Morris +Sunny Ripert Thomas E Enebo Thomas Weymuth Ticean Bennett @@ -142,5 +145,3 @@ Yuri Sidorov Yuusuke Takizawa Zubin Henner Бродяной Александр -Nicolay Hvidsten -Simon Neutert diff --git a/CHANGELOG.md b/CHANGELOG.md index 534e0cd226..fb2ee361f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,9 @@ - Add Zimbabwe Gold (ZWG) currency - Update thousands_separator for CHF - Add Caribbean Guilder (XCG) as replacement for Netherlands Antillean Gulden (ANG) -- Add `Currency#cents_based?` to check if currency is cents-based +- Add `Money#to_nearest_cash_value` to return a rounded Money instance to the smallest denomination +- Deprecate `Money#round_to_nearest_cash_value` in favor of calling `to_nearest_cash_value.fractional` +- Add `Money::Currency#cents_based?` to check if currency is cents-based - Add ability to nest `Money.with_rounding_mode` blocks - Allow `nil` to be used as a default_currency diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 155cccc59e..f37f087b27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,8 @@ 3. Make sure everything is working: `bundle exec rake spec` 4. Make your changes 5. Test your changes -5. Create a Pull Request -6. Celebrate!!!!! +6. Create a Pull Request +7. Celebrate! 🎉 ## Notes diff --git a/README.md b/README.md index 51dfc8818b..6f968b25fd 100644 --- a/README.md +++ b/README.md @@ -471,7 +471,7 @@ Money.from_amount(2.34567).format #=> "$2.34567" To round to the nearest cent (or anything more precise), you can use the `round` method. However, note that the `round` method on a `Money` object does not work the same way as a normal Ruby `Float` object. Money's `round` method accepts different arguments. The first argument to the round method is the rounding mode, while the second argument is the level of precision relative to the cent. -``` +```ruby # Float 2.34567.round #=> 2 2.34567.round(2) #=> 2.35 @@ -484,10 +484,18 @@ Money.from_cents(2.34567).round(BigDecimal::ROUND_DOWN, 2).format #=> "$0.0234" ``` You can set the default rounding mode by passing one of the `BigDecimal` mode enumerables like so: + ```ruby Money.rounding_mode = BigDecimal::ROUND_HALF_EVEN ``` -See [BigDecimal::ROUND_MODE](https://ruby-doc.org/3.4.1/gems/bigdecimal/BigDecimal.html#ROUND_MODE) for more information + +See [BigDecimal::ROUND_MODE](https://ruby-doc.org/3.4.1/gems/bigdecimal/BigDecimal.html#ROUND_MODE) for more information. + +To round to the nearest cash value in currencies without small denominations: + +```ruby +Money.from_cents(11_11, "CHF").to_nearest_cash_value.format # => "CHF 11.10" +``` ## Ruby on Rails @@ -527,7 +535,7 @@ This will work seamlessly with [rails-i18n](https://github.com/svenfuchs/rails-i If you wish to disable this feature and use defaults instead: -``` ruby +```ruby Money.locale_backend = nil ``` diff --git a/lib/money/money.rb b/lib/money/money.rb index c647854dd5..0d19462c0d 100644 --- a/lib/money/money.rb +++ b/lib/money/money.rb @@ -69,15 +69,31 @@ def fractional # # @see infinite_precision def round_to_nearest_cash_value + warn "[DEPRECATION] `round_to_nearest_cash_value` is deprecated - use " \ + "`to_nearest_cash_value.fractional` instead" + + to_nearest_cash_value.fractional + end + + # Round a given amount of money to the nearest possible money in cash value. + # For example, in Swiss franc (CHF), the smallest possible amount of cash + # value is CHF 0.05. Therefore, this method rounds CHF 0.07 to CHF 0.05, and + # CHF 0.08 to CHF 0.10. + # + # @return [Money] + def to_nearest_cash_value unless self.currency.smallest_denomination - raise UndefinedSmallestDenomination, 'Smallest denomination of this currency is not defined' + raise UndefinedSmallestDenomination, + "Smallest denomination of this currency is not defined" end fractional = as_d(@fractional) smallest_denomination = as_d(self.currency.smallest_denomination) - rounded_value = (fractional / smallest_denomination).round(0, self.class.rounding_mode) * smallest_denomination + rounded_value = + (fractional / smallest_denomination) + .round(0, self.class.rounding_mode) * smallest_denomination - return_value(rounded_value) + dup_with(fractional: return_value(rounded_value)) end # @!attribute [r] currency diff --git a/sig/lib/money/money.rbs b/sig/lib/money/money.rbs index 2219ca4847..9a55e233aa 100644 --- a/sig/lib/money/money.rbs +++ b/sig/lib/money/money.rbs @@ -58,6 +58,14 @@ class Money # @see Money.default_infinite_precision def round_to_nearest_cash_value: () -> (Integer | BigDecimal) + # Round a given amount of money to the nearest possible money in cash value. + # For example, in Swiss franc (CHF), the smallest possible amount of cash + # value is CHF 0.05. Therefore, this method rounds CHF 0.07 to CHF 0.05, and + # CHF 0.08 to CHF 0.10. + # + # @return [Money] + def to_nearest_cash_value: () -> Money + attr_reader currency: Currency attr_reader bank: untyped diff --git a/spec/money_spec.rb b/spec/money_spec.rb index 87ef2b09be..26c7ebed3c 100644 --- a/spec/money_spec.rb +++ b/spec/money_spec.rb @@ -503,6 +503,36 @@ def expectation.fractional end end + describe "#to_nearest_cash_value" do + it "rounds to the nearest possible cash value" do + expect(Money.new(23_50, "AED").to_nearest_cash_value).to eq(Money.new(23_50, "AED")) + expect(Money.new(-23_50, "AED").to_nearest_cash_value).to eq(Money.new(-23_50, "AED")) + expect(Money.new(22_13, "AED").to_nearest_cash_value).to eq(Money.new(22_25, "AED")) + expect(Money.new(-22_13, "AED").to_nearest_cash_value).to eq(Money.new(-22_25, "AED")) + expect(Money.new(22_12, "AED").to_nearest_cash_value).to eq(Money.new(22_00, "AED")) + expect(Money.new(-22_12, "AED").to_nearest_cash_value).to eq(Money.new(-22_00, "AED")) + + expect(Money.new(1_78, "CHF").to_nearest_cash_value).to eq(Money.new(1_80, "CHF")) + expect(Money.new(-1_78, "CHF").to_nearest_cash_value).to eq(Money.new(-1_80, "CHF")) + expect(Money.new(1_77, "CHF").to_nearest_cash_value).to eq(Money.new(1_75, "CHF")) + expect(Money.new(-1_77, "CHF").to_nearest_cash_value).to eq(Money.new(-1_75, "CHF")) + expect(Money.new(1_75, "CHF").to_nearest_cash_value).to eq(Money.new(1_75, "CHF")) + expect(Money.new(-1_75, "CHF").to_nearest_cash_value).to eq(Money.new(-1_75, "CHF")) + + expect(Money.new(2_99, "USD").to_nearest_cash_value).to eq(Money.new(2_99, "USD")) + expect(Money.new(-2_99, "USD").to_nearest_cash_value).to eq(Money.new(-2_99, "USD")) + expect(Money.new(3_00, "USD").to_nearest_cash_value).to eq(Money.new(3_00, "USD")) + expect(Money.new(-3_00, "USD").to_nearest_cash_value).to eq(Money.new(-3_00, "USD")) + expect(Money.new( 3_01, "USD").to_nearest_cash_value).to eq(Money.new(3_01, "USD")) + expect(Money.new(-3_01, "USD").to_nearest_cash_value).to eq(Money.new(-3_01, "USD")) + end + + it "raises an error if smallest denomination is not defined" do + expect {Money.new(1_00, "XAG").to_nearest_cash_value} + .to raise_error(Money::UndefinedSmallestDenomination) + end + end + describe "#amount" do it "returns the amount of cents as dollars" do expect(Money.new(1_00).amount).to eq 1