Class: Philiprehberger::Money
- Inherits:
-
Object
- Object
- Philiprehberger::Money
- Includes:
- Comparable, Allocation, Arithmetic, Formatting
- Defined in:
- lib/philiprehberger/money.rb,
lib/philiprehberger/money/parsing.rb,
lib/philiprehberger/money/version.rb,
lib/philiprehberger/money/currency.rb,
lib/philiprehberger/money/allocation.rb,
lib/philiprehberger/money/arithmetic.rb,
lib/philiprehberger/money/formatting.rb,
lib/philiprehberger/money/exchange_rate.rb
Overview
Immutable money value object with integer subunit storage
Defined Under Namespace
Modules: Allocation, Arithmetic, Formatting, Parsing Classes: Currency, CurrencyMismatch, Error, ExchangeRate, InvalidCurrency, ParseError
Constant Summary collapse
- ROUNDING_MODES =
{ half_even: BigDecimal::ROUND_HALF_EVEN, half_up: BigDecimal::ROUND_HALF_UP, ceil: BigDecimal::ROUND_CEILING, floor: BigDecimal::ROUND_FLOOR }.freeze
- VERSION =
'0.5.0'
Instance Attribute Summary collapse
-
#cents ⇒ Object
readonly
Returns the value of attribute cents.
-
#currency ⇒ Object
readonly
Returns the value of attribute currency.
-
#rounding_mode ⇒ Object
readonly
Returns the value of attribute rounding_mode.
Class Method Summary collapse
-
.from_amount(amount, currency_code, rounding: :half_even) ⇒ Money
Create a Money object from a decimal amount.
-
.parse(input, currency: nil) ⇒ Money
Parse a formatted money string into a Money object.
-
.sum(moneys, target_currency:) ⇒ Money
Sum a collection of Money objects, converting to a target currency.
Instance Method Summary collapse
-
#<=>(other) ⇒ Integer?
Compare two Money objects of the same currency.
-
#add_percent(n) ⇒ Money
Return money plus n% (e.g. for tax-inclusive pricing).
-
#amount ⇒ BigDecimal
The decimal amount.
-
#clamp(min, max) ⇒ Money
Constrain the money value within a minimum and maximum bound.
-
#convert_to(target_code, rate:) ⇒ Money
Convert to another currency using a given rate.
-
#deconstruct_keys(keys) ⇒ Hash
Pattern matching support for Ruby 3.x case/in.
-
#eql?(other) ⇒ Boolean
True if same cents and currency.
-
#exchange_to(target_code) ⇒ Money
Convert to another currency using the ExchangeRate store.
-
#hash ⇒ Integer
Hash based on cents and currency code.
-
#initialize(cents, currency_code, rounding: :half_even) ⇒ Money
constructor
Create a Money object from subunit cents.
-
#negative? ⇒ Boolean
True if the amount is negative.
-
#percent(n) ⇒ Money
Return n% of this money amount.
-
#positive? ⇒ Boolean
True if the amount is positive.
-
#round_to_nearest(increment) ⇒ Money
Round to the nearest N subunits.
-
#split(n) ⇒ Array<Money>
Split money equally among n parts.
-
#subtract_percent(n) ⇒ Money
Return money minus n% (e.g. for discounts).
-
#tax_breakdown(rate) ⇒ Hash
Calculate tax breakdown from the current amount as net.
-
#to_f ⇒ Float
The decimal amount as a float.
-
#to_h ⇒ Hash
Hash representation for serialization.
-
#zero? ⇒ Boolean
True if the amount is zero.
Methods included from Allocation
Methods included from Formatting
Methods included from Arithmetic
Constructor Details
#initialize(cents, currency_code, rounding: :half_even) ⇒ Money
Create a Money object from subunit cents
38 39 40 41 42 43 |
# File 'lib/philiprehberger/money.rb', line 38 def initialize(cents, currency_code, rounding: :half_even) @cents = Integer(cents) @currency = Currency.find(currency_code) @rounding_mode = rounding freeze end |
Instance Attribute Details
#cents ⇒ Object (readonly)
Returns the value of attribute cents.
30 31 32 |
# File 'lib/philiprehberger/money.rb', line 30 def cents @cents end |
#currency ⇒ Object (readonly)
Returns the value of attribute currency.
30 31 32 |
# File 'lib/philiprehberger/money.rb', line 30 def currency @currency end |
#rounding_mode ⇒ Object (readonly)
Returns the value of attribute rounding_mode.
30 31 32 |
# File 'lib/philiprehberger/money.rb', line 30 def rounding_mode @rounding_mode end |
Class Method Details
.from_amount(amount, currency_code, rounding: :half_even) ⇒ Money
Create a Money object from a decimal amount
51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/philiprehberger/money.rb', line 51 def self.from_amount(amount, currency_code, rounding: :half_even) curr = Currency.find(currency_code) mode = ROUNDING_MODES.fetch(rounding) do raise ArgumentError, "Unknown rounding mode: #{rounding}. Valid modes: #{ROUNDING_MODES.keys.join(', ')}" end new( (BigDecimal(amount.to_s) * curr.subunit_to_unit).round(0, mode).to_i, currency_code, rounding: rounding ) end |
.parse(input, currency: nil) ⇒ Money
Parse a formatted money string into a Money object
69 70 71 |
# File 'lib/philiprehberger/money.rb', line 69 def self.parse(input, currency: nil) Parsing.parse(input, currency: currency) end |
.sum(moneys, target_currency:) ⇒ Money
Sum a collection of Money objects, converting to a target currency
79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/philiprehberger/money.rb', line 79 def self.sum(moneys, target_currency:) raise Error, 'Collection must not be empty' if moneys.nil? || moneys.empty? moneys.reduce(Money.new(0, target_currency)) do |total, money| converted = if money.currency.code == target_currency.to_s.upcase.to_sym money else money.exchange_to(target_currency) end total + converted end end |
Instance Method Details
#<=>(other) ⇒ Integer?
Compare two Money objects of the same currency
110 111 112 113 114 |
# File 'lib/philiprehberger/money.rb', line 110 def <=>(other) return nil unless other.is_a?(Money) && other.currency.code == currency.code cents <=> other.cents end |
#add_percent(n) ⇒ Money
Return money plus n% (e.g. for tax-inclusive pricing)
167 168 169 |
# File 'lib/philiprehberger/money.rb', line 167 def add_percent(n) self + percent(n) end |
#amount ⇒ BigDecimal
The decimal amount
95 96 97 |
# File 'lib/philiprehberger/money.rb', line 95 def amount BigDecimal(@cents.to_s) / @currency.subunit_to_unit end |
#clamp(min, max) ⇒ Money
Constrain the money value within a minimum and maximum bound
218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/philiprehberger/money.rb', line 218 def clamp(min, max) unless currency.code == min.currency.code raise CurrencyMismatch, "Cannot clamp #{currency.code.upcase} with #{min.currency.code.upcase}" end unless currency.code == max.currency.code raise CurrencyMismatch, "Cannot clamp #{currency.code.upcase} with #{max.currency.code.upcase}" end raise ArgumentError, 'min must not be greater than max' if min > max return min if self < min return max if self > max self end |
#convert_to(target_code, rate:) ⇒ Money
Convert to another currency using a given rate
136 137 138 139 140 |
# File 'lib/philiprehberger/money.rb', line 136 def convert_to(target_code, rate:) Currency.find(target_code) converted = (BigDecimal(@cents.to_s) * BigDecimal(rate.to_s)).round(0, BigDecimal::ROUND_HALF_EVEN).to_i self.class.new(converted, target_code) end |
#deconstruct_keys(keys) ⇒ Hash
Pattern matching support for Ruby 3.x case/in
261 262 263 264 265 266 |
# File 'lib/philiprehberger/money.rb', line 261 def deconstruct_keys(keys) full = { cents: @cents, amount: amount.to_f, currency: @currency.code, formatted: to_s } return full if keys.nil? full.slice(*keys) end |
#eql?(other) ⇒ Boolean
Returns true if same cents and currency.
149 150 151 |
# File 'lib/philiprehberger/money.rb', line 149 def eql?(other) other.is_a?(Money) && cents == other.cents && currency.code == other.currency.code end |
#exchange_to(target_code) ⇒ Money
Convert to another currency using the ExchangeRate store
194 195 196 197 |
# File 'lib/philiprehberger/money.rb', line 194 def exchange_to(target_code) rate = ExchangeRate.store.get(currency.code, target_code) convert_to(target_code, rate: rate) end |
#hash ⇒ Integer
Returns hash based on cents and currency code.
143 144 145 |
# File 'lib/philiprehberger/money.rb', line 143 def hash [@cents, @currency.code].hash end |
#negative? ⇒ Boolean
Returns true if the amount is negative.
127 128 129 |
# File 'lib/philiprehberger/money.rb', line 127 def negative? @cents.negative? end |
#percent(n) ⇒ Money
Return n% of this money amount
157 158 159 160 161 |
# File 'lib/philiprehberger/money.rb', line 157 def percent(n) result = (BigDecimal(cents.to_s) * BigDecimal(n.to_s) / BigDecimal('100')) .round(0, BigDecimal::ROUND_HALF_EVEN).to_i self.class.new(result, currency.code) end |
#positive? ⇒ Boolean
Returns true if the amount is positive.
122 123 124 |
# File 'lib/philiprehberger/money.rb', line 122 def positive? @cents.positive? end |
#round_to_nearest(increment) ⇒ Money
Round to the nearest N subunits
238 239 240 241 242 243 |
# File 'lib/philiprehberger/money.rb', line 238 def round_to_nearest(increment) raise Error, 'Increment must be a positive integer' unless increment.is_a?(Integer) && increment.positive? rounded_cents = ((cents.to_f / increment).round * increment).to_i self.class.new(rounded_cents, currency.code, rounding: rounding_mode) end |
#split(n) ⇒ Array<Money>
Split money equally among n parts
184 185 186 187 188 |
# File 'lib/philiprehberger/money.rb', line 184 def split(n) raise ArgumentError, 'Number of parts must be 1 or more' unless n.is_a?(Integer) && n >= 1 allocate(Array.new(n, 1)) end |
#subtract_percent(n) ⇒ Money
Return money minus n% (e.g. for discounts)
175 176 177 |
# File 'lib/philiprehberger/money.rb', line 175 def subtract_percent(n) self - percent(n) end |
#tax_breakdown(rate) ⇒ Hash
Calculate tax breakdown from the current amount as net
203 204 205 206 207 208 209 210 |
# File 'lib/philiprehberger/money.rb', line 203 def tax_breakdown(rate) raise ArgumentError, 'Tax rate must be non-negative' unless rate.is_a?(Numeric) && rate >= 0 tax_cents = (BigDecimal(cents.to_s) * BigDecimal(rate.to_s)) .round(0, BigDecimal::ROUND_HALF_EVEN).to_i tax = self.class.new(tax_cents, currency.code) { net: self, tax: tax, gross: self + tax } end |
#to_f ⇒ Float
The decimal amount as a float
102 103 104 |
# File 'lib/philiprehberger/money.rb', line 102 def to_f amount.to_f end |
#to_h ⇒ Hash
Hash representation for serialization
248 249 250 251 252 253 254 255 |
# File 'lib/philiprehberger/money.rb', line 248 def to_h { cents: @cents, amount: amount.to_f, currency: @currency.code.to_s.upcase, formatted: to_s } end |
#zero? ⇒ Boolean
Returns true if the amount is zero.
117 118 119 |
# File 'lib/philiprehberger/money.rb', line 117 def zero? @cents.zero? end |