Class: Money

Inherits:
Object
  • Object
show all
Defined in:
lib/simple_money/money.rb

Overview

Used to work with financial calculations. Tries to avoid the pitfalls of using Float by storing the value in cents. All calculations are floored.

Constant Summary collapse

VALID_AS_VALUES =

The valid values for as

[:cents, :decimal]
VALID_ROUNDING_METHOD_VALUES =

The valid rounding methods

[:away_from_zero, :toward_zero,
:nearest_up, :nearest_down, :bankers, :up, :down]
ROUNDING_METHOD_TRANSLATION =

Translations from SimpleMoney rounding methods to BigDecimal rounding methods.

{
  :away_from_zero => BigDecimal::ROUND_UP,
  :toward_zero    => BigDecimal::ROUND_DOWN,
  :nearest_up     => BigDecimal::ROUND_HALF_UP,
  :nearest_down   => BigDecimal::ROUND_HALF_DOWN,
  :bankers        => BigDecimal::ROUND_HALF_EVEN,
  :up             => BigDecimal::ROUND_CEILING,
  :down           => BigDecimal::ROUND_FLOOR,
}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(n = 0, options = {}) ⇒ Money

Creates a new Money object. If as is set to :cents, n will be coerced to a Fixnum. If as is set to :decimal, n will be coerced to a BigDecimal.

Examples:

Money.new                        #=> #<Money:... @cents: 0>
Money.new(1)                     #=> #<Money:... @cents: 1>
Money.new(1_00)                  #=> #<Money:... @cents: 100>
Money.new(1_00, :as => :cents)   #=> #<Money:... @cents: 100>
Money.new(1_00, :as => :decimal) #=> #<Money:... @cents: 10000>
Money.new(1.99, :as => :cents)   #=> #<Money:... @cents: 1>
Money.new(1.99, :as => :decimal) #=> #<Money:... @cents: 199>

Parameters:

  • n (#to_s) (defaults to: 0)

    Value of the new object.

  • options (Hash) (defaults to: {})

    The options used to build the new object.

Options Hash (options):

  • :as (Symbol)

    How n is represented (defaults to self.class.default_as).

  • :rounding_method (Symbol)

    How any calculations resulting in fractions of a cent should be rounded.

Raises:

  • (ArgumentError)

    Will raise an ArgumentError if as is not valid.

  • (ArgumentError)

    Will raise an ArgumentError if rounding method is not valid.



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/simple_money/money.rb', line 234

def initialize(n = 0, options = {})
  options = {
    :currency => self.class.default_currency,
    :rounding_method => self.class.default_rounding_method,
    :as => self.class.default_as
  }.merge(options)

  @currency = Currency[options[:currency]]

  raise ArgumentError, "invalid `rounding_method`" unless (
    self.class.valid_rounding_method? options[:rounding_method]
  )
  @rounding_method = options[:rounding_method]

  raise ArgumentError, "invalid `as`" unless (
    self.class.valid_as? options[:as]
  )

  @cents = case options[:as]
           when :cents
             Money.round(
               BigDecimal(n.to_s), rounding_method
             )
           when :decimal
             case currency.subunit_to_unit
             when 10, 100, 1000
               Money.round(
                 (
                   BigDecimal(n.to_s) *
                   BigDecimal(currency.subunit_to_unit.to_s)
                 ),
                 rounding_method
               )
             when 1
               Money.round(
                 BigDecimal(n.to_s), rounding_method
               )
             when 5
               unit = BigDecimal(n.to_s).floor * BigDecimal("5")
               subunit = (
                 (BigDecimal(n.to_s) % BigDecimal("1")) * BigDecimal("10")
               )
               Money.round(unit + subunit)
             else
               raise Exception, "creation of Money objects with subunit_to_unit = `#{currency.subunit_to_unit}` is not implmented"
             end
           end
end

Class Attribute Details

.default_asSymbol

Returns The default as used to create a new Money (defaults to :cents).

Returns:

  • (Symbol)

    The default as used to create a new Money (defaults to :cents).



36
37
38
# File 'lib/simple_money/money.rb', line 36

def default_as
  @default_as
end

.default_currencyObject

The default currency used to create a new Money object (default to :usd).



113
114
115
# File 'lib/simple_money/money.rb', line 113

def default_currency
  @default_currency
end

.default_rounding_methodSymbol

Returns The default rounding method used when calculations do not result in an Fixnum (defaults to :bankers).

Returns:

  • (Symbol)

    The default rounding method used when calculations do not result in an Fixnum (defaults to :bankers).



74
75
76
# File 'lib/simple_money/money.rb', line 74

def default_rounding_method
  @default_rounding_method
end

.overflowBigDecimal

Returns The factional cents left over from any transactions that overflowed.

Returns:

  • (BigDecimal)

    The factional cents left over from any transactions that overflowed.



133
134
135
# File 'lib/simple_money/money.rb', line 133

def overflow
  @overflow
end

Instance Attribute Details

#centsInteger (readonly)

Returns The value of the object in cents.

Returns:

  • (Integer)

    The value of the object in cents.



197
198
199
# File 'lib/simple_money/money.rb', line 197

def cents
  @cents
end

#currencyCurrency::CurrencyStruct (readonly)

was created using.

Returns:



207
208
209
# File 'lib/simple_money/money.rb', line 207

def currency
  @currency
end

#rounding_methodSymbol (readonly)

fractions of a cent.

Returns:

  • (Symbol)

    The rounding method used when calculations result in



202
203
204
# File 'lib/simple_money/money.rb', line 202

def rounding_method
  @rounding_method
end

Class Method Details

.reset_overflowBigDecimal

Resets the overflow bucket to 0.

Returns:

  • (BigDecimal)


156
157
158
# File 'lib/simple_money/money.rb', line 156

def reset_overflow
  self.overflow = 0
end

.round(n, rounding_method = default_rounding_method) ⇒ Fixnum

Returns n rounded to an integer using the given rounding method, or the default rounding method when none is provided. When rounding, the fractional cents are added to the overflow bucket.

Examples:

Money.round(1.5, :bankers) #=> 2

Parameters:

  • n (#to_s)

    The value to round.

  • rounding_method (Symbol) (defaults to: default_rounding_method)

    The rounding method to use.

Returns:

  • (Fixnum)

Raises:

  • (ArgumentError)

    Will raise an ArgumentError if an invalid rounding method is given.



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/simple_money/money.rb', line 175

def round(n, rounding_method = default_rounding_method)
  raise ArgumentError, "invalid `rounding_method`" unless (
    valid_rounding_method? rounding_method
  )

  original = BigDecimal(n.to_s)
  rounded  = original.round(
    0,
    ROUNDING_METHOD_TRANSLATION[rounding_method]
  )
  @overflow += original - rounded
  rounded.to_i
end

.valid_as?(as) ⇒ true, false

Returns true if argument is a valid value for :as, otherwise false.

Examples:

Money.valid_as? :cents #=> True
Money.valid_as? :foo   #=> False

Parameters:

  • as (Symbol)

    The value to check.

Returns:

  • (true, false)


67
68
69
# File 'lib/simple_money/money.rb', line 67

def valid_as?(as)
  VALID_AS_VALUES.include? as
end

.valid_rounding_method?(rounding_method) ⇒ true, false

Returns true if argument is a valid rounding method, otherwise false.

Examples:

Money.valid_rounding_method? :up    #=> True
Money.valid_rounding_method? :foo   #=> False

Parameters:

  • rounding_method (Symbol)

    The value to check.

Returns:

  • (true, false)


107
108
109
# File 'lib/simple_money/money.rb', line 107

def valid_rounding_method?(rounding_method)
  VALID_ROUNDING_METHOD_VALUES.include? rounding_method
end

Instance Method Details

#%(n) ⇒ Numeric, Money

Modulo self by a Money/Numeric; return the results as a new Numeric/Money.

Examples:

Money.new(10) % Money.new(5) #=> 2
Money.new(10) % 5            #=> #<Money:... @cents: 2>

Parameters:

  • n (Money, Numeric)

    The object to modulo. If n is Numeric, it will be coerced to a BigDecimal before any calculations are done.

Returns:

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Money object or Numeric.

  • (ArgumentError)

    Will raise an ArgumentError if n is an incompatible currency.



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/simple_money/money.rb', line 417

def %(n)
  case n
  when Money
    raise ArgumentError, "n is an incompatible currency" unless (
      n.currency == currency
    )

    BigDecimal(self.cents.to_s) % BigDecimal(n.cents.to_s)
  when Numeric
    Money.new(
      BigDecimal(self.cents.to_s) % BigDecimal(n.to_s),
      :as => :cents,
      :rounding_method => rounding_method,
      :currency => currency
    )
  else
    raise ArgumentError, "n must be a Money or Numeric"
  end
end

#*(n) ⇒ Money

Multiply Money by a Numeric; return the results as a new Money object.

Examples:

Money.new(2) * 2 #=> #<Money:... @cents: 4>

Parameters:

  • n (Numeric)

    The object to multiply. n will be coerced to a BigDecimal before any calculations are done.

Returns:

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Numeric object.



352
353
354
355
356
357
358
359
360
361
# File 'lib/simple_money/money.rb', line 352

def *(n)
  raise ArgumentError, "n must be a Numeric" unless n.is_a? Numeric

  Money.new(
    BigDecimal(self.cents.to_s) * BigDecimal(n.to_s),
    :as => :cents,
    :rounding_method => rounding_method,
    :currency => currency
  )
end

#+(n) ⇒ Money

Add two Money objects; return the results as a new Money object.

Examples:

Money.new(1) + Money.new(2) #=> #<Money:... @cents: 3>

Parameters:

  • n (Money)

    The object to add.

Returns:

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Money object.

  • (ArgumentError)

    Will raise an ArgumentError if currency of n is incompatible.



297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/simple_money/money.rb', line 297

def +(n)
  raise ArgumentError, "n must be a Money" unless n.is_a? Money
  raise ArgumentError, "n is an incompatible currency" unless (
    n.currency == currency
  )

  Money.new(
    self.cents + n.cents,
    :as => :cents,
    :rounding_method => rounding_method,
    :currency => currency
  )
end

#-(n) ⇒ Money

Subtract two Money; return the results as a new Money object.

Examples:

Money.new(2) - Money.new(1) #=> #<Money:... @cents: 1>

Parameters:

  • n (Money)

    The object to subtract.

Returns:

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Money object.

  • (ArgumentError)

    Will raise an ArgumentError if currency of n is incompatible.



325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/simple_money/money.rb', line 325

def -(n)
  raise ArgumentError, "n must be a Money" unless n.is_a? Money
  raise ArgumentError, "n is an incompatible currency" unless (
    n.currency == currency
  )

  Money.new(
    self.cents - n.cents,
    :as => :cents,
    :rounding_method => rounding_method,
    :currency => currency
  )
end

#/(n) ⇒ Numeric, Money

Divide self by a Money/Numeric; return the results as a new Numeric/Money.

Examples:

Money.new(10) / Money.new(5) #=> 2
Money.new(10) / 5            #=> #<Money:... @cents: 2>

Parameters:

  • n (Money, Numeric)

    The object to divide. If n is Numeric, it will be coerced to a BigDecimal before any calculations are done.

Returns:

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Money object or Numeric.

  • (ArgumentError)

    Will raise an ArgumentError if currency of n is incompatible.



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/simple_money/money.rb', line 379

def /(n)
  case n
  when Money
    raise ArgumentError, "n is an incompatible currency" unless (
      n.currency == currency
    )

    BigDecimal(self.cents.to_s) / BigDecimal(n.cents.to_s)
  when Numeric
    result, overflow = BigDecimal(self.cents.to_s).divmod(BigDecimal(n.to_s))
    self.class.overflow = self.class.overflow + overflow
    Money.new(
      result,
      :as => :cents,
      :rounding_method => rounding_method,
      :currency => currency
    )
  else
    raise ArgumentError, "n must be a Money or Numeric"
  end
end

#<=>(n) ⇒ Object

Compare self to n. When self < n, return -1. When self > n, return 1. When self == n, return 0.

Examples:

Money.new(1_00) <=> Money.new(2_00) #=> -1
Money.new(2_00) <=> Money.new(1_00) #=> 1
Money.new(1_00) <=> Money.new(1_00) #=> 0

Parameters:

  • n (Money)

    The object to compare with.

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Money object or Numeric.

  • (ArgumentError)

    Will raise an ArgumentError if n is an incompatible currency.



490
491
492
493
494
495
496
497
# File 'lib/simple_money/money.rb', line 490

def <=>(n)
  raise ArgumentError, "n must be a Money" unless n.is_a? Money
  raise ArgumentError, "n is an incompatible currency" unless (
    n.currency == currency
  )

  self.cents <=> n.cents
end

#==(n) ⇒ Object

Compare self to n. When self == n, return true, otherwise false.

Examples:

Money.new(1_00) == Money.new(2_00) #=> false
Money.new(2_00) == Money.new(2_00) #=> true

Parameters:

  • n (Money)

    The object to compare with.

Raises:

  • (ArgumentError)

    Will raise an ArgumentError unless n is a Money object or Numeric.

  • (ArgumentError)

    Will raise an ArgumentError if n is an incompatible currency.



512
513
514
515
516
517
518
519
# File 'lib/simple_money/money.rb', line 512

def ==(n)
  raise ArgumentError, "n must be a Money" unless n.is_a? Money
  raise ArgumentError, "n is an incompatible currency" unless (
    n.currency == currency
  )

  self.cents == n.cents
end

#absMoney

Return a new Money object using the absolute value of cents.

Examples:

Money.new(-1_00).abs #=> #<Money:... cents: 100>

Returns:



528
529
530
531
532
533
534
535
# File 'lib/simple_money/money.rb', line 528

def abs
  Money.new(
    self.cents.abs,
    :as => :cents,
    :rounding_method => rounding_method,
    :currency => currency
  )
end

#divmod(n) ⇒ Object



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/simple_money/money.rb', line 437

def divmod(n)
  case n
  when Money
    raise ArgumentError, "n is an incompatible currency" unless (
      n.currency == currency
    )

    a, b = BigDecimal(self.cents.to_s).divmod(BigDecimal(n.cents.to_s))
    [
      a,
      Money.new(
        b,
        :as => :cents,
        :rounding_method => rounding_method,
        :currency => currency
      )
    ]
  when Numeric
    a, b = BigDecimal(self.cents.to_s).divmod(BigDecimal(n.to_s))
    [
      Money.new(
        a,
        :as => :cents,
        :rounding_method => rounding_method,
        :currency => currency
      ),
      Money.new(
        b,
        :as => :cents,
        :rounding_method => rounding_method,
        :currency => currency
      )
    ]
  else
    raise ArgumentError, "n must be a Money or Numeric"
  end
end

#to_s(options = {}) ⇒ String

Returns cents formatted as a string, based on any options passed.

Examples:

n = Money.new(1_00, :as => :cents)
n.to_s                  #=> "100"
n.to_s(:as => :decimal) #=> "1.00"

Parameters:

  • options (Hash) (defaults to: {})

    The options used to format the string.

Options Hash (options):

  • :as (Symbol)

    How cents should be returned (defaults to self.class.default_as).

Returns:

  • (String)

Raises:

  • (ArgumentError)

    Will raise an ArgumentError if as is not valid.



552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/simple_money/money.rb', line 552

def to_s(options = {})
  options = {
    :as => :cents
  }.merge(options)

  raise ArgumentError, "invalid `as`" unless (
    self.class.valid_as? options[:as]
  )

  case options[:as]
  when :cents
    cents.to_s
  when :decimal
    if currency.subunit_to_unit == 1
      return cents.to_s
    end
    unit, subunit = cents.divmod(currency.subunit_to_unit).map(&:to_s)
    subunit = (("0" * currency.decimal_places) + subunit)
    subunit = subunit[
      (-1 * currency.decimal_places),
      currency.decimal_places
    ]
    "#{unit}.#{subunit}"
  end
end