Skip to content
Merged
2 changes: 2 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Kenn Ejima
Kenneth Salomon
kirillian
Laurynas Butkus
Łukasz Wójcik
Marcel Scherf
Marco Otte-Witte
Mateus Gomes
Expand All @@ -98,6 +99,7 @@ Nick Lozon
Nihad Abbasov
Olek Janiszewski
Orien Madgwick
Paweł Madejski
Paul McMahon
Paulo Diniz
Pavan Sudarshan
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- 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 ability to nest `Money.with_rounding_mode` blocks
- Allow `nil` to be used as a default_currency

## 6.19.0
Expand Down
3 changes: 2 additions & 1 deletion lib/money/money.rb
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,11 @@ def self.rounding_mode(mode = nil)
# Money.new(1200) * BigDecimal('0.029')
# end
def self.with_rounding_mode(mode)
original_mode = Thread.current[:money_rounding_mode]
Thread.current[:money_rounding_mode] = mode
yield
ensure
Thread.current[:money_rounding_mode] = nil
Thread.current[:money_rounding_mode] = original_mode
end

# Adds a new exchange rate to the default bank and return the rate.
Expand Down
105 changes: 92 additions & 13 deletions spec/money_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,33 @@
expect(Money.from_amount(1, "USD", bank).bank).to eq bank
end

it 'warns about rounding_mode deprecation' do
context 'given a currency is provided' do
context 'and the currency is nil' do
let(:currency) { nil }

it "should have the default currency" do
expect(Money.from_amount(1, currency).currency).to eq Money.default_currency
end
end
end
end

describe '.with_rounding_mode' do
it 'sets the .rounding_mode method deprecated' do
allow(Money).to receive(:warn)
allow(Money).to receive(:with_rounding_mode).and_call_original

expect(Money.from_amount(1.999).to_d).to eq 2
expect(Money.rounding_mode(BigDecimal::ROUND_DOWN) do
rounding_block = lambda do
Money.from_amount(1.999).to_d
end).to eq 1.99
end

expect(Money.from_amount(1.999).to_d).to eq 2
expect(Money.rounding_mode(BigDecimal::ROUND_DOWN, &rounding_block)).to eq 1.99
expect(Money)
.to have_received(:warn)
.with('[DEPRECATION] calling `rounding_mode` with a block is deprecated. ' \
'Please use `.with_rounding_mode` instead.')
expect(Money).to have_received(:with_rounding_mode).with(BigDecimal::ROUND_DOWN, &rounding_block)
end

it 'rounds using with_rounding_mode' do
Expand All @@ -244,14 +260,77 @@
end).to eq 1.99
end

context 'given a currency is provided' do
context 'and the currency is nil' do
let(:currency) { nil }
it 'allows blocks nesting' do
Money.with_rounding_mode(BigDecimal::ROUND_DOWN) do
expect(Money.rounding_mode).to eq(BigDecimal::ROUND_DOWN)

it "should have the default currency" do
expect(Money.from_amount(1, currency).currency).to eq Money.default_currency
Money.with_rounding_mode(BigDecimal::ROUND_UP) do
expect(Money.rounding_mode).to eq(BigDecimal::ROUND_UP)
expect(Money.from_amount(2.137).to_d).to eq 2.14
end

expect(
Money.rounding_mode
).to eq(BigDecimal::ROUND_DOWN), 'Outer mode should be restored after inner block'
expect(Money.from_amount(2.137).to_d).to eq 2.13
end

expect(
Money.rounding_mode
).to eq(BigDecimal::ROUND_HALF_EVEN), 'Original mode should be restored after outer block'
expect(Money.from_amount(2.137).to_d).to eq 2.14
end

it 'safely handles concurrent usage in different threads' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I always find it very difficult to test these scenarios and you made it so easy. I'll keep this in mind for my future self 👏🏻

test_value = 1.999
expected_down = 1.99
expected_up = 2.00

results = Queue.new

test_money_with_rounding_mode = lambda do |rounding_mode|
Thread.new do
Money.with_rounding_mode(rounding_mode) do
results.push({
set_rounding_mode: rounding_mode,
mode: Money.rounding_mode,
result: Money.from_amount(test_value).to_d
})

# Sleep to allow interleaving with other thread
sleep 0.01

results.push({
set_rounding_mode: rounding_mode,
mode: Money.rounding_mode,
result: Money.from_amount(test_value).to_d
})
end
end
end

[
test_money_with_rounding_mode.call(BigDecimal::ROUND_DOWN),
test_money_with_rounding_mode.call(BigDecimal::ROUND_UP)
].each(&:join)

all_results = []
all_results << results.pop until results.empty?

round_down_results = all_results.select { |r| r[:set_rounding_mode] == BigDecimal::ROUND_DOWN }
round_up_results = all_results.select { |r| r[:set_rounding_mode] == BigDecimal::ROUND_UP }

round_down_results.each do |result|
expect(result[:mode]).to eq(BigDecimal::ROUND_DOWN)
expect(result[:result]).to eq(expected_down)
end

round_up_results.each do |result|
expect(result[:mode]).to eq(BigDecimal::ROUND_UP)
expect(result[:result]).to eq(expected_up)
end

expect(Money.rounding_mode).to eq(BigDecimal::ROUND_HALF_EVEN)
end
end

Expand Down Expand Up @@ -337,23 +416,23 @@ def expectation.fractional

context "with a block" do
it "respects the rounding_mode" do
expect(Money.rounding_mode(BigDecimal::ROUND_DOWN) do
expect(Money.with_rounding_mode(BigDecimal::ROUND_DOWN) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for changing this to use the non-deprecated method! 😍

Money.new(1.9).fractional
end).to eq 1

expect(Money.rounding_mode(BigDecimal::ROUND_UP) do
expect(Money.with_rounding_mode(BigDecimal::ROUND_UP) do
Money.new(1.1).fractional
end).to eq 2

expect(Money.rounding_mode).to eq BigDecimal::ROUND_HALF_EVEN
end

it "works for multiplication within a block" do
Money.rounding_mode(BigDecimal::ROUND_DOWN) do
Money.with_rounding_mode(BigDecimal::ROUND_DOWN) do
expect((Money.new(1_00) * "0.019".to_d).fractional).to eq 1
end

Money.rounding_mode(BigDecimal::ROUND_UP) do
Money.with_rounding_mode(BigDecimal::ROUND_UP) do
expect((Money.new(1_00) * "0.011".to_d).fractional).to eq 2
end

Expand Down