Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/money/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ module Helpers

DECIMAL_ZERO = BigDecimal(0).freeze
MAX_DECIMAL = 21
# Largest supported BigDecimal#exponent (number of integer digits). Rejects
# values like "1e1000000000" that are cheap to store but expand into huge
# Integers or Strings in #subunits or #to_s.
MAX_INTEGER_DIGITS = 1000

def value_to_decimal(num)
value =
Expand Down
3 changes: 3 additions & 0 deletions lib/money/money.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ def new_from_money(amount, currency)
def initialize(value, currency)
raise ArgumentError if value.nan?
raise ArgumentError if value.infinite?
if value.exponent > Helpers::MAX_INTEGER_DIGITS
raise ArgumentError, "value exceeds the supported range of #{Helpers::MAX_INTEGER_DIGITS} integer digits"
end

@currency = currency
@value = BigDecimal(value.round(@currency.minor_units))
Expand Down
1 change: 1 addition & 0 deletions sig/money/helpers.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Money
module Helpers
DECIMAL_ZERO: BigDecimal
MAX_DECIMAL: Integer
MAX_INTEGER_DIGITS: Integer

def self.value_to_decimal: (Numeric | String | Money | nil num) -> BigDecimal
def self.value_to_currency: (String | Currency | NullCurrency | nil currency) -> (Currency | NullCurrency)
Expand Down
41 changes: 41 additions & 0 deletions spec/money_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,47 @@
expect { Money.new(-Float::INFINITY) }.to raise_error(ArgumentError)
end

describe "magnitude bound" do
it "raises when constructed with an exponent-form string beyond the supported range" do
expect { Money.new("1e1000000000", "USD") }.to raise_error(ArgumentError, /supported range/)
expect { Money.new("-1e1000000000", "USD") }.to raise_error(ArgumentError, /supported range/)
end

it "raises when constructed with an out-of-range BigDecimal" do
expect { Money.new(BigDecimal("1e1000"), "USD") }.to raise_error(ArgumentError, /supported range/)
end

it "raises when converting out-of-range subunits back to money" do
expect { Money.from_subunits("1e1000000000", "USD") }.to raise_error(ArgumentError, /supported range/)
end

it "raises when arithmetic leaves the supported range" do
expect { Money.new("1e999", "USD") * 10 }.to raise_error(ArgumentError, /supported range/)
end

it "raises when loaded from YAML with an out-of-range value" do
yaml = "--- !ruby/object:Money\nvalue: '1e1000000000'\ncurrency: USD\n"
expect { yaml_load(yaml) }.to raise_error(ArgumentError, /supported range/)
end

it "accepts the largest in-range value and converts it to subunits unchanged" do
max = "9" * Money::Helpers::MAX_INTEGER_DIGITS
expect(Money.new(max, "USD").value).to eq(BigDecimal(max))
expect(Money.new(max, "USD").subunits).to eq(Integer(max) * 100)
expect(Money.new("-#{max}", "USD").subunits).to eq(-Integer(max) * 100)
end

it "accepts values larger than a DECIMAL(21,3) column" do
expect(Money.new("1e25", "USD") * 10**25).to eq(Money.new("1e50", "USD"))
expect(Money.new("1e25", "USD").subunits).to eq(10**27)
end

it "still accepts ordinary exponent-form strings" do
expect(Money.new("1.5e3", "USD")).to eq(Money.new(1500, "USD"))
expect(Money.new("1e-10", "USD")).to eq(Money.new(0, "USD"))
end
end

it "is comparable with non-money objects" do
expect(money).not_to eq(nil)
end
Expand Down
Loading