ecr_money 3.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1213 @@
1
+ # encoding: utf-8
2
+ require 'money/bank/variable_exchange'
3
+
4
+ # Represents an amount of money in a given currency.
5
+ class Money
6
+ include Comparable
7
+
8
+ # The value of the money in cents.
9
+ #
10
+ # @return [Integer]
11
+ attr_reader :cents
12
+
13
+ # The currency the money is in.
14
+ #
15
+ # @return [Currency]
16
+ attr_reader :currency
17
+
18
+ # The +Money::Bank+ based object used to perform currency exchanges with.
19
+ #
20
+ # @return [Money::Bank::*]
21
+ attr_reader :bank
22
+
23
+ attr_reader :exact_number
24
+
25
+ # Class Methods
26
+ class << self
27
+ # Each Money object is associated to a bank object, which is responsible
28
+ # for currency exchange. This property allows you to specify the default
29
+ # bank object. The default value for this property is an instance if
30
+ # +Bank::VariableExchange.+ It allows one to specify custom exchange rates.
31
+ #
32
+ # @return [Money::Bank::*]
33
+ attr_accessor :default_bank
34
+
35
+ # The default currency, which is used when +Money.new+ is called without an
36
+ # explicit currency argument. The default value is Currency.new("USD"). The
37
+ # value must be a valid +Money::Currency+ instance.
38
+ #
39
+ # @return [Money::Currency]
40
+ attr_accessor :default_currency
41
+ end
42
+
43
+ # Set the default bank for creating new +Money+ objects.
44
+ self.default_bank = Bank::VariableExchange.instance
45
+
46
+ # Set the default currency for creating new +Money+ object.
47
+ self.default_currency = Currency.new("USD")
48
+
49
+ # Create a new money object with value 0.
50
+ #
51
+ # @param [Currency, String, Symbol] currency The currency to use.
52
+ #
53
+ # @return [Money]
54
+ #
55
+ # @example
56
+ # Money.empty #=> #<Money @cents=0>
57
+ def self.empty(currency = default_currency)
58
+ Money.new(0, currency)
59
+ end
60
+
61
+ # Creates a new Money object of the given value, using the Canadian
62
+ # dollar currency.
63
+ #
64
+ # @param [Integer] cents The cents value.
65
+ #
66
+ # @return [Money]
67
+ #
68
+ # @example
69
+ # n = Money.ca_dollar(100)
70
+ # n.cents #=> 100
71
+ # n.currency #=> #<Money::Currency id: cad>
72
+ def self.ca_dollar(cents)
73
+ Money.new(cents, "CAD")
74
+ end
75
+
76
+ # Creates a new Money object of the given value, using the American dollar
77
+ # currency.
78
+ #
79
+ # @param [Integer] cents The cents value.
80
+ #
81
+ # @return [Money]
82
+ #
83
+ # @example
84
+ # n = Money.us_dollar(100)
85
+ # n.cents #=> 100
86
+ # n.currency #=> #<Money::Currency id: usd>
87
+ def self.us_dollar(cents)
88
+ Money.new(cents, "USD")
89
+ end
90
+
91
+ # Creates a new Money object of the given value, using the Euro currency.
92
+ #
93
+ # @param [Integer] cents The cents value.
94
+ #
95
+ # @return [Money]
96
+ #
97
+ # @example
98
+ # n = Money.euro(100)
99
+ # n.cents #=> 100
100
+ # n.currency #=> #<Money::Currency id: eur>
101
+ def self.euro(cents)
102
+ Money.new(cents, "EUR")
103
+ end
104
+
105
+
106
+ # Creates a new Money object of +amount+ value in dollars,
107
+ # with given +currency+.
108
+ #
109
+ # The amount value is expressed in +dollars+
110
+ # where the +dollar+ is the main monetary unit,
111
+ # opposite to the subunit-based representation
112
+ # used internally by this library called +cents+.
113
+ #
114
+ # @param [Numeric] amount The money amount, in dollars.
115
+ # @param [Currency, String, Symbol] currency The currency format.
116
+ # @param [Money::Bank::*] bank The exchange bank to use.
117
+ #
118
+ # @return [Money]
119
+ #
120
+ # @example
121
+ # Money.new_with_dollars(100)
122
+ # #=> #<Money @cents=10000 @currency="USD">
123
+ # Money.new_with_dollars(100, "USD")
124
+ # #=> #<Money @cents=10000 @currency="USD">
125
+ # Money.new_with_dollars(100, "EUR")
126
+ # #=> #<Money @cents=10000 @currency="EUR">
127
+ #
128
+ # @see Money.new
129
+ #
130
+ def self.new_with_dollars(amount, currency = Money.default_currency, bank = Money.default_bank)
131
+ money = from_numeric(amount, currency)
132
+ # Hack! You can't change a bank
133
+ money.instance_variable_set("@bank", bank)
134
+ money
135
+ end
136
+
137
+
138
+ # Parses the current string and converts it to a +Money+ object.
139
+ # Excess characters will be discarded.
140
+ #
141
+ # @param [String, #to_s] input The input to parse.
142
+ # @param [Currency, String, Symbol] currency The currency format.
143
+ # The currency to set the resulting +Money+ object to.
144
+ #
145
+ # @return [Money]
146
+ #
147
+ # @raise [ArgumentError] If any +currency+ is supplied and
148
+ # given value doesn't match the one extracted from
149
+ # the +input+ string.
150
+ #
151
+ # @example
152
+ # '100'.to_money #=> #<Money @cents=10000>
153
+ # '100.37'.to_money #=> #<Money @cents=10037>
154
+ # '100 USD'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
155
+ # 'USD 100'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
156
+ # '$100 USD'.to_money #=> #<Money @cents=10000, @currency=#<Money::Currency id: usd>>
157
+ # 'hello 2000 world'.to_money #=> #<Money @cents=200000 @currency=#<Money::Currency id: usd>>
158
+ #
159
+ # @example Mismatching currencies
160
+ # 'USD 2000'.to_money("EUR") #=> ArgumentError
161
+ #
162
+ # @see Money.from_string
163
+ #
164
+ def self.parse(input, currency = nil)
165
+ i = input.to_s
166
+
167
+ # Get the currency.
168
+ m = i.scan /([A-Z]{2,3})/
169
+ c = m[0] ? m[0][0] : nil
170
+
171
+ # check that currency passed and embedded currency are the same,
172
+ # and negotiate the final currency
173
+ if currency.nil? and c.nil?
174
+ currency = Money.default_currency
175
+ elsif currency.nil?
176
+ currency = c
177
+ elsif c.nil?
178
+ currency = currency
179
+ elsif currency != c
180
+ # TODO: ParseError
181
+ raise ArgumentError, "Mismatching Currencies"
182
+ end
183
+ currency = Money::Currency.wrap(currency)
184
+
185
+ cents = extract_cents(i, currency)
186
+ Money.new(cents, currency)
187
+ end
188
+
189
+ # Converts a String into a Money object treating the +value+
190
+ # as dollars and converting them to the corresponding cents value,
191
+ # according to +currency+ subunit property,
192
+ # before instantiating the Money object.
193
+ #
194
+ # Behind the scenes, this method relies on {Money.from_bigdecimal}
195
+ # to avoid problems with string-to-numeric conversion.
196
+ #
197
+ # @param [String, #to_s] value The money amount, in dollars.
198
+ # @param [Currency, String, Symbol] currency
199
+ # The currency to set the resulting +Money+ object to.
200
+ #
201
+ # @return [Money]
202
+ #
203
+ # @example
204
+ # Money.from_string("100")
205
+ # #=> #<Money @cents=10000 @currency="USD">
206
+ # Money.from_string("100", "USD")
207
+ # #=> #<Money @cents=10000 @currency="USD">
208
+ # Money.from_string("100", "EUR")
209
+ # #=> #<Money @cents=10000 @currency="EUR">
210
+ # Money.from_string("100", "BHD")
211
+ # #=> #<Money @cents=100 @currency="BHD">
212
+ #
213
+ # @see String#to_money
214
+ # @see Money.parse
215
+ #
216
+ def self.from_string(value, currency = Money.default_currency)
217
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
218
+ end
219
+
220
+ # Converts a Fixnum into a Money object treating the +value+
221
+ # as dollars and converting them to the corresponding cents value,
222
+ # according to +currency+ subunit property,
223
+ # before instantiating the Money object.
224
+ #
225
+ # @param [Fixnum] value The money amount, in dollars.
226
+ # @param [Currency, String, Symbol] currency The currency format.
227
+ #
228
+ # @return [Money]
229
+ #
230
+ # @example
231
+ # Money.from_fixnum(100)
232
+ # #=> #<Money @cents=10000 @currency="USD">
233
+ # Money.from_fixnum(100, "USD")
234
+ # #=> #<Money @cents=10000 @currency="USD">
235
+ # Money.from_fixnum(100, "EUR")
236
+ # #=> #<Money @cents=10000 @currency="EUR">
237
+ # Money.from_fixnum(100, "BHD")
238
+ # #=> #<Money @cents=100 @currency="BHD">
239
+ #
240
+ # @see Fixnum#to_money
241
+ # @see Money.from_numeric
242
+ #
243
+ def self.from_fixnum(value, currency = Money.default_currency)
244
+ currency = Money::Currency.wrap(currency)
245
+ amount = value * currency.subunit_to_unit
246
+ Money.new(amount, currency)
247
+ end
248
+
249
+ # Converts a Float into a Money object treating the +value+
250
+ # as dollars and converting them to the corresponding cents value,
251
+ # according to +currency+ subunit property,
252
+ # before instantiating the Money object.
253
+ #
254
+ # Behind the scenes, this method relies on Money.from_bigdecimal
255
+ # to avoid problems with floating point precision.
256
+ #
257
+ # @param [Float] value The money amount, in dollars.
258
+ # @param [Currency, String, Symbol] currency The currency format.
259
+ #
260
+ # @return [Money]
261
+ #
262
+ # @example
263
+ # Money.from_float(100.0)
264
+ # #=> #<Money @cents=10000 @currency="USD">
265
+ # Money.from_float(100.0, "USD")
266
+ # #=> #<Money @cents=10000 @currency="USD">
267
+ # Money.from_float(100.0, "EUR")
268
+ # #=> #<Money @cents=10000 @currency="EUR">
269
+ # Money.from_float(100.0, "BHD")
270
+ # #=> #<Money @cents=100 @currency="BHD">
271
+ #
272
+ # @see Float#to_money
273
+ # @see Money.from_numeric
274
+ #
275
+ def self.from_float(value, currency = Money.default_currency)
276
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
277
+ end
278
+
279
+ # Converts a BigDecimal into a Money object treating the +value+
280
+ # as dollars and converting them to the corresponding cents value,
281
+ # according to +currency+ subunit property,
282
+ # before instantiating the Money object.
283
+ #
284
+ # @param [BigDecimal] value The money amount, in dollars.
285
+ # @param [Currency, String, Symbol] currency The currency format.
286
+ #
287
+ # @return [Money]
288
+ #
289
+ # @example
290
+ # Money.from_bigdecimal(BigDecimal.new("100")
291
+ # #=> #<Money @cents=10000 @currency="USD">
292
+ # Money.from_bigdecimal(BigDecimal.new("100", "USD")
293
+ # #=> #<Money @cents=10000 @currency="USD">
294
+ # Money.from_bigdecimal(BigDecimal.new("100", "EUR")
295
+ # #=> #<Money @cents=10000 @currency="EUR">
296
+ # Money.from_bigdecimal(BigDecimal.new("100", "BHD")
297
+ # #=> #<Money @cents=100 @currency="BHD">
298
+ #
299
+ # @see BigDecimal#to_money
300
+ # @see Money.from_numeric
301
+ #
302
+ def self.from_bigdecimal(value, currency = Money.default_currency)
303
+ currency = Money::Currency.wrap(currency)
304
+ amount = value * currency.subunit_to_unit
305
+ Money.new(amount.fix, currency)
306
+ end
307
+
308
+ # Converts a Numeric value into a Money object treating the +value+
309
+ # as dollars and converting them to the corresponding cents value,
310
+ # according to +currency+ subunit property,
311
+ # before instantiating the Money object.
312
+ #
313
+ # This method relies on various +Money.from_*+ methods
314
+ # and tries to forwards the call to the most appropriate method
315
+ # in order to reduce computation effort.
316
+ # For instance, if +value+ is an Integer, this method calls
317
+ # {Money.from_fixnum} instead of using the default
318
+ # {Money.from_bigdecimal} which adds the overload to converts
319
+ # the value into a slower BigDecimal instance.
320
+ #
321
+ # @param [Numeric] value The money amount, in dollars.
322
+ # @param [Currency, String, Symbol] currency The currency format.
323
+ #
324
+ # @return [Money]
325
+ #
326
+ # @raise +ArgumentError+ Unless +value+ is a supported type.
327
+ #
328
+ # @example
329
+ # Money.from_numeric(100)
330
+ # #=> #<Money @cents=10000 @currency="USD">
331
+ # Money.from_numeric(100.00)
332
+ # #=> #<Money @cents=10000 @currency="USD">
333
+ # Money.from_numeric("100")
334
+ # #=> ArgumentError
335
+ #
336
+ # @see Numeric#to_money
337
+ # @see Money.from_fixnum
338
+ # @see Money.from_float
339
+ # @see Money.from_bigdecimal
340
+ #
341
+ def self.from_numeric(value, currency = Money.default_currency)
342
+ case value
343
+ when Fixnum
344
+ from_fixnum(value, currency)
345
+ when Numeric
346
+ from_bigdecimal(BigDecimal.new(value.to_s), currency)
347
+ else
348
+ raise ArgumentError, "`value' should be a Numeric object"
349
+ end
350
+ end
351
+
352
+
353
+ # Adds a new exchange rate to the default bank and return the rate.
354
+ #
355
+ # @param [Currency, String, Symbol] from_currency Currency to exchange from.
356
+ # @param [Currency, String, Symbol] to_currency Currency to exchange to.
357
+ # @param [Numeric] rate Rate to exchange with.
358
+ #
359
+ # @return [Numeric]
360
+ #
361
+ # @example
362
+ # Money.add_rate("USD", "CAD", 1.25) #=> 1.25
363
+ def self.add_rate(from_currency, to_currency, rate)
364
+ Money.default_bank.add_rate(from_currency, to_currency, rate)
365
+ end
366
+
367
+
368
+ # Creates a new Money object of +cents+ value in cents,
369
+ # with given +currency+.
370
+ #
371
+ # Alternatively you can use the convenience
372
+ # methods like {Money.ca_dollar} and {Money.us_dollar}.
373
+ #
374
+ # @param [Integer] cents The money amount, in cents.
375
+ # @param [Currency, String, Symbol] currency The currency format.
376
+ # @param [Money::Bank::*] bank The exchange bank to use.
377
+ #
378
+ # @return [Money]
379
+ #
380
+ # @example
381
+ # Money.new(100)
382
+ # #=> #<Money @cents=100 @currency="USD">
383
+ # Money.new(100, "USD")
384
+ # #=> #<Money @cents=100 @currency="USD">
385
+ # Money.new(100, "EUR")
386
+ # #=> #<Money @cents=100 @currency="EUR">
387
+ #
388
+ # @see Money.new_with_dollars
389
+ #
390
+ def initialize(exact_number, currency = Money.default_currency, bank = Money.default_bank)
391
+ @exact_number = exact_number
392
+ @cents = exact_number.round.to_i
393
+ @currency = Currency.wrap(currency)
394
+ @bank = bank
395
+ end
396
+
397
+ # Returns the value of the money in dollars,
398
+ # instead of in cents.
399
+ #
400
+ # @return [Float]
401
+ #
402
+ # @example
403
+ # Money.new(100).dollars # => 1.0
404
+ # Money.new_with_dollars(1).dollar # => 1.0
405
+ #
406
+ # @see #to_f
407
+ # @see #cents
408
+ #
409
+ def dollars
410
+ to_f
411
+ end
412
+
413
+ # Return string representation of currency object
414
+ #
415
+ # @return [String]
416
+ #
417
+ # @example
418
+ # Money.new(100, :USD).currency_as_string #=> "USD"
419
+ def currency_as_string
420
+ self.currency.to_s
421
+ end
422
+
423
+ # Set currency object using a string
424
+ #
425
+ # @param [String] val The currency string.
426
+ #
427
+ # @return [Money::Currency]
428
+ #
429
+ # @example
430
+ # Money.new(100).currency_as_string("CAD") #=> #<Money::Currency id: cad>
431
+ def currency_as_string=(val)
432
+ @currency = Currency.wrap(val)
433
+ end
434
+
435
+ # Checks whether two money objects have the same currency and the same
436
+ # amount. Checks against money objects with a different currency and checks
437
+ # against objects that do not respond to #to_money will always return false.
438
+ #
439
+ # @param [Money] other_money Value to compare with.
440
+ #
441
+ # @return [Boolean]
442
+ #
443
+ # @example
444
+ # Money.new(100) == Money.new(101) #=> false
445
+ # Money.new(100) == Money.new(100) #=> true
446
+ def ==(other_money)
447
+ if other_money.respond_to?(:to_money)
448
+ other_money = other_money.to_money
449
+ cents == other_money.cents && self.currency == other_money.currency
450
+ else
451
+ false
452
+ end
453
+ end
454
+
455
+ # Synonymous with +#==+.
456
+ #
457
+ # @param [Money] other_money Value to compare with.
458
+ #
459
+ # @return [Money]
460
+ #
461
+ # @see #==
462
+ def eql?(other_money)
463
+ self == other_money
464
+ end
465
+
466
+ # Returns a Fixnum hash value based on the +cents+ and +currency+ attributes
467
+ # in order to use functions like & (intersection), group_by, etc.
468
+ #
469
+ # @return [Fixnum]
470
+ #
471
+ # @example
472
+ # Money.new(100).hash #=> 908351
473
+ def hash
474
+ [cents.hash, currency.hash].hash
475
+ end
476
+
477
+ # Compares this money object against another object. +other_money+ must
478
+ # respond to #to_money. Returns -1 when less than, 0 when equal and 1 when
479
+ # greater than.
480
+ #
481
+ # If +other_money+ is a different currency, then +other_money+ will first be
482
+ # converted into this money object's currency by calling +#exchange+ on
483
+ # +other_money+.
484
+ #
485
+ # Comparisons against objects that do not respond to #to_money will cause an
486
+ # +ArgumentError+ to be raised.
487
+ #
488
+ # @param [Money, #to_money] other_money Value to compare with.
489
+ #
490
+ # @return [-1, 0, 1]
491
+ #
492
+ # @raise [ArgumentError]
493
+ #
494
+ # @example
495
+ # Money.new(100) <=> 99 #=> 1
496
+ # Money.new(100) <=> Money.new(100) #=> 0
497
+ # Money.new(100) <=> "$101.00" #=> -1
498
+ def <=>(other_money)
499
+ if other_money.respond_to?(:to_money)
500
+ other_money = other_money.to_money
501
+ if self.currency == other_money.currency
502
+ cents <=> other_money.cents
503
+ else
504
+ cents <=> other_money.exchange_to(currency).cents
505
+ end
506
+ else
507
+ raise ArgumentError, "Comparison of #{self.class} with #{other_money.inspect} failed"
508
+ end
509
+ end
510
+
511
+ # Returns a new Money object containing the sum of the two operands' monetary
512
+ # values. If +other_money+ has a different currency then its monetary value
513
+ # is automatically exchanged to this object's currency using +exchange_to+.
514
+ #
515
+ # @param [Money] other_money Other +Money+ object to add.
516
+ #
517
+ # @return [Money]
518
+ #
519
+ # @example
520
+ # Money.new(100) + Money.new(100) #=> #<Money @cents=200>
521
+ def +(other_money)
522
+ if currency == other_money.currency
523
+ Money.new(cents + other_money.cents, other_money.currency)
524
+ else
525
+ Money.new(cents + other_money.exchange_to(currency).cents, currency)
526
+ end
527
+ end
528
+
529
+ # Returns a new Money object containing the difference between the two
530
+ # operands' monetary values. If +other_money+ has a different currency then
531
+ # its monetary value is automatically exchanged to this object's currency
532
+ # using +exchange_to+.
533
+ #
534
+ # @param [Money] other_money Other +Money+ object to subtract.
535
+ #
536
+ # @return [Money]
537
+ #
538
+ # @example
539
+ # Money.new(100) - Money.new(99) #=> #<Money @cents=1>
540
+ def -(other_money)
541
+ if currency == other_money.currency
542
+ Money.new(cents - other_money.cents, other_money.currency)
543
+ else
544
+ Money.new(cents - other_money.exchange_to(currency).cents, currency)
545
+ end
546
+ end
547
+
548
+ # Multiplies the monetary value with the given number and returns a new
549
+ # +Money+ object with this monetary value and the same currency.
550
+ #
551
+ # Note that you can't multiply a Money object by an other +Money+ object.
552
+ #
553
+ # @param [Numeric] value Number to multiply by.
554
+ #
555
+ # @return [Money] The resulting money.
556
+ #
557
+ # @raise [ArgumentError] If +value+ is a Money instance.
558
+ #
559
+ # @example
560
+ # Money.new(100) * 2 #=> #<Money @cents=200>
561
+ #
562
+ def *(value)
563
+ if value.is_a?(Money)
564
+ raise ArgumentError, "Can't multiply a Money by a Money"
565
+ else
566
+ Money.new(cents * value, currency)
567
+ end
568
+ end
569
+
570
+ # Divides the monetary value with the given number and returns a new +Money+
571
+ # object with this monetary value and the same currency.
572
+ # Can also divide by another +Money+ object to get a ratio.
573
+ #
574
+ # +Money/Numeric+ returns +Money+. +Money/Money+ returns +Float+.
575
+ #
576
+ # @param [Money, Numeric] value Number to divide by.
577
+ #
578
+ # @return [Money] The resulting money if you divide Money by a number.
579
+ # @return [Float] The resulting number if you divide Money by a Money.
580
+ #
581
+ # @example
582
+ # Money.new(100) / 10 #=> #<Money @cents=10>
583
+ # Money.new(100) / Money.new(10) #=> 10.0
584
+ #
585
+ def /(value)
586
+ if value.is_a?(Money)
587
+ if currency == value.currency
588
+ (cents / BigDecimal.new(value.cents.to_s)).to_f
589
+ else
590
+ (cents / BigDecimal(value.exchange_to(currency).cents.to_s)).to_f
591
+ end
592
+ else
593
+ Money.new(cents / value, currency)
594
+ end
595
+ end
596
+
597
+ # Synonym for +#/+.
598
+ #
599
+ # @param [Money, Numeric] value Number to divide by.
600
+ #
601
+ # @return [Money] The resulting money if you divide Money by a number.
602
+ # @return [Float] The resulting number if you divide Money by a Money.
603
+ #
604
+ # @see #/
605
+ #
606
+ def div(value)
607
+ self / value
608
+ end
609
+
610
+ # Divide money by money or fixnum and return array containing quotient and
611
+ # modulus.
612
+ #
613
+ # @param [Money, Fixnum] val Number to divmod by.
614
+ #
615
+ # @return [Array<Money,Money>,Array<Fixnum,Money>]
616
+ #
617
+ # @example
618
+ # Money.new(100).divmod(9) #=> [#<Money @cents=11>, #<Money @cents=1>]
619
+ # Money.new(100).divmod(Money.new(9)) #=> [11, #<Money @cents=1>]
620
+ def divmod(val)
621
+ if val.is_a?(Money)
622
+ a = self.cents
623
+ b = self.currency == val.currency ? val.cents : val.exchange_to(self.currency).cents
624
+ q, m = a.divmod(b)
625
+ return [q, Money.new(m, self.currency)]
626
+ else
627
+ return [self.div(val), Money.new(self.cents.modulo(val), self.currency)]
628
+ end
629
+ end
630
+
631
+ # Equivalent to +self.divmod(val)[1]+
632
+ #
633
+ # @param [Money, Fixnum] val Number take modulo with.
634
+ #
635
+ # @return [Money]
636
+ #
637
+ # @example
638
+ # Money.new(100).modulo(9) #=> #<Money @cents=1>
639
+ # Money.new(100).modulo(Money.new(9)) #=> #<Money @cents=1>
640
+ def modulo(val)
641
+ self.divmod(val)[1]
642
+ end
643
+
644
+ # Synonym for +#modulo+.
645
+ #
646
+ # @param [Money, Fixnum] val Number take modulo with.
647
+ #
648
+ # @return [Money]
649
+ #
650
+ # @see #modulo
651
+ def %(val)
652
+ self.modulo(val)
653
+ end
654
+
655
+ # If different signs +self.modulo(val) - val+ otherwise +self.modulo(val)+
656
+ #
657
+ # @param [Money, Fixnum] val Number to rake remainder with.
658
+ #
659
+ # @return [Money]
660
+ #
661
+ # @example
662
+ # Money.new(100).remainder(9) #=> #<Money @cents=1>
663
+ def remainder(val)
664
+ a, b = self, val
665
+ b = b.exchange_to(a.currency) if b.is_a?(Money) and a.currency != b.currency
666
+
667
+ a_sign, b_sign = :pos, :pos
668
+ a_sign = :neg if a.cents < 0
669
+ b_sign = :neg if (b.is_a?(Money) and b.cents < 0) or (b < 0)
670
+
671
+ return a.modulo(b) if a_sign == b_sign
672
+ a.modulo(b) - (b.is_a?(Money) ? b : Money.new(b, a.currency))
673
+ end
674
+
675
+ # Return absolute value of self as a new Money object.
676
+ #
677
+ # @return [Money]
678
+ #
679
+ # @example
680
+ # Money.new(-100).abs #=> #<Money @cents=100>
681
+ def abs
682
+ Money.new(self.cents.abs, self.currency)
683
+ end
684
+
685
+ # Test if the money amount is zero.
686
+ #
687
+ # @return [Boolean]
688
+ #
689
+ # @example
690
+ # Money.new(100).zero? #=> false
691
+ # Money.new(0).zero? #=> true
692
+ def zero?
693
+ cents == 0
694
+ end
695
+
696
+ # Test if the money amount is non-zero. Returns this money object if it is
697
+ # non-zero, or nil otherwise, like +Numeric#nonzero?+.
698
+ #
699
+ # @return [Money, nil]
700
+ #
701
+ # @example
702
+ # Money.new(100).nonzero? #=> #<Money @cents=100>
703
+ # Money.new(0).nonzero? #=> nil
704
+ def nonzero?
705
+ cents != 0 ? self : nil
706
+ end
707
+
708
+ # Uses +Currency#symbol+. If +nil+ is returned, defaults to "¤".
709
+ #
710
+ # @return [String]
711
+ #
712
+ # @example
713
+ # Money.new(100, "USD").symbol #=> "$"
714
+ def symbol
715
+ currency.symbol || "¤"
716
+ end
717
+
718
+ # If I18n is loaded, looks up key +:number.format.delimiter+.
719
+ # Otherwise and as fallback it uses +Currency#thousands_separator+.
720
+ # If +nil+ is returned, default to ",".
721
+ #
722
+ # @return [String]
723
+ #
724
+ # @example
725
+ # Money.new(100, "USD").thousands_separator #=> ","
726
+ if Object.const_defined?("I18n")
727
+ def thousands_separator
728
+ I18n.t(:"number.format.thousands_separator", :default => currency.thousands_separator || ",")
729
+ end
730
+ else
731
+ def thousands_separator
732
+ currency.thousands_separator || ","
733
+ end
734
+ end
735
+ alias :delimiter :thousands_separator
736
+
737
+ # If I18n is loaded, looks up key +:number.format.seperator+.
738
+ # Otherwise and as fallback it uses +Currency#seperator+.
739
+ # If +nil+ is returned, default to ",".
740
+ #
741
+ # @return [String]
742
+ #
743
+ # @example
744
+ # Money.new(100, "USD").decimal_mark #=> "."
745
+ if Object.const_defined?("I18n")
746
+ def decimal_mark
747
+ I18n.t(:"number.format.decimal_mark", :default => currency.decimal_mark || ".")
748
+ end
749
+ else
750
+ def decimal_mark
751
+ currency.decimal_mark || "."
752
+ end
753
+ end
754
+ alias :separator :decimal_mark
755
+
756
+ # Creates a formatted price string according to several rules.
757
+ #
758
+ # @param [Hash] *rules The options used to format the string.
759
+ #
760
+ # @return [String]
761
+ #
762
+ # @option *rules [Boolean, String] :display_free (false) Whether a zero
763
+ # amount of money should be formatted of "free" or as the supplied string.
764
+ #
765
+ # @example
766
+ # Money.us_dollar(0).format(:display_free => true) #=> "free"
767
+ # Money.us_dollar(0).format(:display_free => "gratis") #=> "gratis"
768
+ # Money.us_dollar(0).format #=> "$0.00"
769
+ #
770
+ # @option *rules [Boolean] :with_currency (false) Whether the currency name
771
+ # should be appended to the result string.
772
+ #
773
+ # @example
774
+ # Money.ca_dollar(100).format => "$1.00"
775
+ # Money.ca_dollar(100).format(:with_currency => true) #=> "$1.00 CAD"
776
+ # Money.us_dollar(85).format(:with_currency => true) #=> "$0.85 USD"
777
+ #
778
+ # @option *rules [Boolean] :no_cents (false) Whether cents should be omitted.
779
+ #
780
+ # @example
781
+ # Money.ca_dollar(100).format(:no_cents => true) #=> "$1"
782
+ # Money.ca_dollar(599).format(:no_cents => true) #=> "$5"
783
+ #
784
+ # @option *rules [Boolean, String, nil] :symbol (true) Whether a money symbol
785
+ # should be prepended to the result string. The default is true. This method
786
+ # attempts to pick a symbol that's suitable for the given currency.
787
+ #
788
+ # @example
789
+ # Money.new(100, "USD") #=> "$1.00"
790
+ # Money.new(100, "GBP") #=> "£1.00"
791
+ # Money.new(100, "EUR") #=> "€1.00"
792
+ #
793
+ # # Same thing.
794
+ # Money.new(100, "USD").format(:symbol => true) #=> "$1.00"
795
+ # Money.new(100, "GBP").format(:symbol => true) #=> "£1.00"
796
+ # Money.new(100, "EUR").format(:symbol => true) #=> "€1.00"
797
+ #
798
+ # # You can specify a false expression or an empty string to disable
799
+ # # prepending a money symbol.
800
+ # Money.new(100, "USD").format(:symbol => false) #=> "1.00"
801
+ # Money.new(100, "GBP").format(:symbol => nil) #=> "1.00"
802
+ # Money.new(100, "EUR").format(:symbol => "") #=> "1.00"
803
+ #
804
+ # # If the symbol for the given currency isn't known, then it will default
805
+ # # to "¤" as symbol.
806
+ # Money.new(100, "AWG").format(:symbol => true) #=> "¤1.00"
807
+ #
808
+ # # You can specify a string as value to enforce using a particular symbol.
809
+ # Money.new(100, "AWG").format(:symbol => "ƒ") #=> "ƒ1.00"
810
+ #
811
+ # @option *rules [Boolean, String, nil] :decimal_mark (true) Whether the
812
+ # currency should be separated by the specified character or '.'
813
+ #
814
+ # @example
815
+ # # If a string is specified, it's value is used.
816
+ # Money.new(100, "USD").format(:decimal_mark => ",") #=> "$1,00"
817
+ #
818
+ # # If the decimal_mark for a given currency isn't known, then it will default
819
+ # # to "." as decimal_mark.
820
+ # Money.new(100, "FOO").format #=> "$1.00"
821
+ #
822
+ # @option *rules [Boolean, String, nil] :thousands_separator (true) Whether
823
+ # the currency should be delimited by the specified character or ','
824
+ #
825
+ # @example
826
+ # # If false is specified, no thousands_separator is used.
827
+ # Money.new(100000, "USD").format(:thousands_separator => false) #=> "1000.00"
828
+ # Money.new(100000, "USD").format(:thousands_separator => nil) #=> "1000.00"
829
+ # Money.new(100000, "USD").format(:thousands_separator => "") #=> "1000.00"
830
+ #
831
+ # # If a string is specified, it's value is used.
832
+ # Money.new(100000, "USD").format(:thousands_separator => ".") #=> "$1.000.00"
833
+ #
834
+ # # If the thousands_separator for a given currency isn't known, then it will
835
+ # # default to "," as thousands_separator.
836
+ # Money.new(100000, "FOO").format #=> "$1,000.00"
837
+ #
838
+ # @option *rules [Boolean] :html (false) Whether the currency should be
839
+ # HTML-formatted. Only useful in combination with +:with_currency+.
840
+ #
841
+ # @example
842
+ # s = Money.ca_dollar(570).format(:html => true, :with_currency => true)
843
+ # s #=> "$5.70 <span class=\"currency\">CAD</span>"
844
+ def format(*rules)
845
+ # support for old format parameters
846
+ rules = normalize_formatting_rules(rules)
847
+
848
+ if cents == 0
849
+ if rules[:display_free].respond_to?(:to_str)
850
+ return rules[:display_free]
851
+ elsif rules[:display_free]
852
+ return "free"
853
+ end
854
+ end
855
+
856
+ symbol_value =
857
+ if rules.has_key?(:symbol)
858
+ if rules[:symbol] === true
859
+ symbol
860
+ elsif rules[:symbol]
861
+ rules[:symbol]
862
+ else
863
+ ""
864
+ end
865
+ elsif rules[:html]
866
+ currency.html_entity
867
+ else
868
+ symbol
869
+ end
870
+
871
+ formatted = case rules[:no_cents]
872
+ when true
873
+ "#{self.to_s.to_i}"
874
+ else
875
+ "#{self.to_s}"
876
+ end
877
+
878
+ symbol_position =
879
+ if rules.has_key?(:symbol_position)
880
+ rules[:symbol_position]
881
+ elsif currency.symbol_first?
882
+ :before
883
+ else
884
+ :after
885
+ end
886
+
887
+ if symbol_value && !symbol_value.empty?
888
+ formatted = (symbol_position == :before ? "#{symbol_value}#{formatted}" : "#{formatted} #{symbol_value}")
889
+ end
890
+
891
+ if rules.has_key?(:decimal_mark) and rules[:decimal_mark] and
892
+ rules[:decimal_mark] != decimal_mark
893
+ formatted.sub!(decimal_mark, rules[:decimal_mark])
894
+ end
895
+
896
+ thousands_separator_value = thousands_separator
897
+ # Determine thousands_separator
898
+ if rules.has_key?(:thousands_separator)
899
+ if rules[:thousands_separator] === false or rules[:thousands_separator].nil?
900
+ thousands_separator_value = ""
901
+ elsif rules[:thousands_separator]
902
+ thousands_separator_value = rules[:thousands_separator]
903
+ end
904
+ end
905
+
906
+ # Apply thousands_separator
907
+ formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]|$))/, "\\1#{thousands_separator_value}")
908
+
909
+ if rules[:with_currency]
910
+ formatted << " "
911
+ formatted << '<span class="currency">' if rules[:html]
912
+ formatted << currency.to_s
913
+ formatted << '</span>' if rules[:html]
914
+ end
915
+ formatted
916
+ end
917
+
918
+ # Returns the amount of money as a string.
919
+ #
920
+ # @return [String]
921
+ #
922
+ # @example
923
+ # Money.ca_dollar(100).to_s #=> "1.00"
924
+ def to_s
925
+ unit, subunit = cents.abs.divmod(currency.subunit_to_unit).map{|o| o.to_s}
926
+ if currency.decimal_places == 0
927
+ return "-#{unit}" if cents < 0
928
+ return unit
929
+ end
930
+ subunit = (("0" * currency.decimal_places) + subunit)[(-1*currency.decimal_places)..-1]
931
+ return "-#{unit}#{decimal_mark}#{subunit}" if cents < 0
932
+ "#{unit}#{decimal_mark}#{subunit}"
933
+ end
934
+
935
+ # Return the amount of money as a float. Floating points cannot guarantee
936
+ # precision. Therefore, this function should only be used when you no longer
937
+ # need to represent currency or working with another system that requires
938
+ # decimals.
939
+ #
940
+ # @return [Float]
941
+ #
942
+ # @example
943
+ # Money.us_dollar(100).to_f => 1.0
944
+ def to_f
945
+ (BigDecimal.new(cents.to_s) / currency.subunit_to_unit).to_f
946
+ end
947
+
948
+ # Receive the amount of this money object in another Currency.
949
+ #
950
+ # @param [Currency, String, Symbol] other_currency Currency to exchange to.
951
+ #
952
+ # @return [Money]
953
+ #
954
+ # @example
955
+ # Money.new(2000, "USD").exchange_to("EUR")
956
+ # Money.new(2000, "USD").exchange_to(Currency.new("EUR"))
957
+ def exchange_to(other_currency)
958
+ other_currency = Currency.wrap(other_currency)
959
+ @bank.exchange_with(self, other_currency)
960
+ end
961
+
962
+ # Receive a money object with the same amount as the current Money object
963
+ # in american dollars.
964
+ #
965
+ # @return [Money]
966
+ #
967
+ # @example
968
+ # n = Money.new(100, "CAD").as_us_dollar
969
+ # n.currency #=> #<Money::Currency id: usd>
970
+ def as_us_dollar
971
+ exchange_to("USD")
972
+ end
973
+
974
+ # Receive a money object with the same amount as the current Money object
975
+ # in canadian dollar.
976
+ #
977
+ # @return [Money]
978
+ #
979
+ # @example
980
+ # n = Money.new(100, "USD").as_ca_dollar
981
+ # n.currency #=> #<Money::Currency id: cad>
982
+ def as_ca_dollar
983
+ exchange_to("CAD")
984
+ end
985
+
986
+ # Receive a money object with the same amount as the current Money object
987
+ # in euro.
988
+ #
989
+ # @return [Money]
990
+ #
991
+ # @example
992
+ # n = Money.new(100, "USD").as_euro
993
+ # n.currency #=> #<Money::Currency id: eur>
994
+ def as_euro
995
+ exchange_to("EUR")
996
+ end
997
+
998
+ # Conversation to +self+.
999
+ #
1000
+ # @return [self]
1001
+ def to_money
1002
+ self
1003
+ end
1004
+
1005
+ # Common inspect function
1006
+ #
1007
+ # @return [String]
1008
+ def inspect
1009
+ "#<Money cents:#{cents} currency:#{currency}>"
1010
+ end
1011
+
1012
+ # Allocates money between different parties without loosing pennies.
1013
+ # After the mathmatically split has been performed, left over pennies will
1014
+ # be distributed round-robin amongst the parties. This means that parties
1015
+ # listed first will likely recieve more pennies then ones that are listed later
1016
+ #
1017
+ # @param [0.50, 0.25, 0.25] to give 50% of the cash to party1, 25% ot party2, and 25% to party3.
1018
+ #
1019
+ # @return [Array<Money, Money, Money>]
1020
+ #
1021
+ # @example
1022
+ # Money.new(5, "USD").allocate([0.3,0.7)) #=> [Money.new(2), Money.new(3)]
1023
+ # Money.new(100, "USD").allocate([0.33,0.33,0.33]) #=> [Money.new(34), Money.new(33), Money.new(33)]
1024
+ def allocate(splits)
1025
+ allocations = splits.inject(0.0) {|sum, i| sum += i }
1026
+ raise ArgumentError, "splits add to more then 100%" if allocations > 1.0
1027
+
1028
+ left_over = cents
1029
+
1030
+ amounts = splits.collect do |ratio|
1031
+ fraction = (cents * ratio / allocations).floor
1032
+ left_over -= fraction
1033
+ fraction
1034
+ end
1035
+
1036
+ left_over.times { |i| amounts[i % amounts.length] += 1 }
1037
+
1038
+ return amounts.collect { |cents| Money.new(cents, currency) }
1039
+ end
1040
+
1041
+ # Split money amongst parties evenly without loosing pennies.
1042
+ #
1043
+ # @param [2] number of parties.
1044
+ #
1045
+ # @return [Array<Money, Money, Money>]
1046
+ #
1047
+ # @example
1048
+ # Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
1049
+ def split(num)
1050
+ raise ArgumentError, "need at least one party" if num < 1
1051
+ low = Money.new(cents / num)
1052
+ high = Money.new(low.cents + 1)
1053
+
1054
+ remainder = cents % num
1055
+ result = []
1056
+
1057
+ num.times do |index|
1058
+ result[index] = index < remainder ? high : low
1059
+ end
1060
+
1061
+ return result
1062
+ end
1063
+
1064
+ private
1065
+
1066
+ # Cleans up formatting rules.
1067
+ #
1068
+ # @param [Hash]
1069
+ #
1070
+ # @return [Hash]
1071
+ def normalize_formatting_rules(rules)
1072
+ if rules.size == 0
1073
+ rules = {}
1074
+ elsif rules.size == 1
1075
+ rules = rules.pop
1076
+ rules = { rules => true } if rules.is_a?(Symbol)
1077
+ end
1078
+ if not rules.include?(:decimal_mark) and rules.include?(:separator)
1079
+ rules[:decimal_mark] = rules[:separator]
1080
+ end
1081
+ if not rules.include?(:thousands_separator) and rules.include?(:delimiter)
1082
+ rules[:thousands_separator] = rules[:delimiter]
1083
+ end
1084
+ rules
1085
+ end
1086
+
1087
+ # Takes a number string and attempts to massage out the number.
1088
+ #
1089
+ # @param [String] input The string containing a potential number.
1090
+ #
1091
+ # @return [Integer]
1092
+ #
1093
+ def self.extract_cents(input, currency = Money.default_currency)
1094
+ # remove anything that's not a number, potential thousands_separator, or minus sign
1095
+ num = input.gsub(/[^\d|\.|,|\'|\-]/, '').strip
1096
+
1097
+ # set a boolean flag for if the number is negative or not
1098
+ negative = num.split(//).first == "-"
1099
+
1100
+ # if negative, remove the minus sign from the number
1101
+ # if it's not negative, the hyphen makes the value invalid
1102
+ if negative
1103
+ num = num.gsub(/^-/, '')
1104
+ else
1105
+ raise ArgumentError, "Invalid currency amount (hyphen)" if num.include?('-')
1106
+ end
1107
+
1108
+ #if the number ends with punctuation, just throw it out. If it means decimal,
1109
+ #it won't hurt anything. If it means a literal period or comma, this will
1110
+ #save it from being mis-interpreted as a decimal.
1111
+ num.chop! if num.match /[\.|,]$/
1112
+
1113
+ # gather all decimal_marks within the result number
1114
+ used_decimal_marks = num.scan /[^\d]/
1115
+
1116
+ # determine the number of unique decimal_marks within the number
1117
+ #
1118
+ # e.g.
1119
+ # $1,234,567.89 would return 2 (, and .)
1120
+ # $125,00 would return 1
1121
+ # $199 would return 0
1122
+ # $1 234,567.89 would raise an error (decimal_marks are space, comma, and period)
1123
+ case used_decimal_marks.uniq.length
1124
+ # no decimal_mark or thousands_separator; major (dollars) is the number, and minor (cents) is 0
1125
+ when 0 then major, minor = num, 0
1126
+
1127
+ # two decimal_marks, so we know the last item in this array is the
1128
+ # major/minor thousands_separator and the rest are decimal_marks
1129
+ when 2
1130
+ decimal_mark, thousands_separator = used_decimal_marks.uniq
1131
+ # remove all decimal_marks, split on the thousands_separator
1132
+ major, minor = num.gsub(decimal_mark, '').split(thousands_separator)
1133
+ min = 0 unless min
1134
+ when 1
1135
+ # we can't determine if the comma or period is supposed to be a decimal_mark or a thousands_separator
1136
+ # e.g.
1137
+ # 1,00 - comma is a thousands_separator
1138
+ # 1.000 - period is a thousands_separator
1139
+ # 1,000 - comma is a decimal_mark
1140
+ # 1,000,000 - comma is a decimal_mark
1141
+ # 10000,00 - comma is a thousands_separator
1142
+ # 1000,000 - comma is a thousands_separator
1143
+
1144
+ # assign first decimal_mark for reusability
1145
+ decimal_mark = used_decimal_marks.first
1146
+
1147
+ # decimal_mark is used as a decimal_mark when there are multiple instances, always
1148
+ if num.scan(decimal_mark).length > 1 # multiple matches; treat as decimal_mark
1149
+ major, minor = num.gsub(decimal_mark, ''), 0
1150
+ else
1151
+ # ex: 1,000 - 1.0000 - 10001.000
1152
+ # split number into possible major (dollars) and minor (cents) values
1153
+ possible_major, possible_minor = num.split(decimal_mark)
1154
+ possible_major ||= "0"
1155
+ possible_minor ||= "00"
1156
+
1157
+ # if the minor (cents) length isn't 3, assign major/minor from the possibles
1158
+ # e.g.
1159
+ # 1,00 => 1.00
1160
+ # 1.0000 => 1.00
1161
+ # 1.2 => 1.20
1162
+ if possible_minor.length != 3 # thousands_separator
1163
+ major, minor = possible_major, possible_minor
1164
+ else
1165
+ # minor length is three
1166
+ # let's try to figure out intent of the thousands_separator
1167
+
1168
+ # the major length is greater than three, which means
1169
+ # the comma or period is used as a thousands_separator
1170
+ # e.g.
1171
+ # 1000,000
1172
+ # 100000,000
1173
+ if possible_major.length > 3
1174
+ major, minor = possible_major, possible_minor
1175
+ else
1176
+ # number is in format ###{sep}### or ##{sep}### or #{sep}###
1177
+ # handle as , is sep, . is thousands_separator
1178
+ if decimal_mark == '.'
1179
+ major, minor = possible_major, possible_minor
1180
+ else
1181
+ major, minor = "#{possible_major}#{possible_minor}", 0
1182
+ end
1183
+ end
1184
+ end
1185
+ end
1186
+ else
1187
+ # TODO: ParseError
1188
+ raise ArgumentError, "Invalid currency amount"
1189
+ end
1190
+
1191
+ # build the string based on major/minor since decimal_mark/thousands_separator have been removed
1192
+ # avoiding floating point arithmetic here to ensure accuracy
1193
+ cents = (major.to_i * currency.subunit_to_unit)
1194
+ # Because of an bug in JRuby, we can't just call #floor
1195
+ minor = minor.to_s
1196
+ minor = if minor.size < currency.decimal_places
1197
+ (minor + ("0" * currency.decimal_places))[0,currency.decimal_places].to_i
1198
+ elsif minor.size > currency.decimal_places
1199
+ if minor[currency.decimal_places,1].to_i >= 5
1200
+ minor[0,currency.decimal_places].to_i+1
1201
+ else
1202
+ minor[0,currency.decimal_places].to_i
1203
+ end
1204
+ else
1205
+ minor.to_i
1206
+ end
1207
+ cents += minor
1208
+
1209
+ # if negative, multiply by -1; otherwise, return positive cents
1210
+ negative ? cents * -1 : cents
1211
+ end
1212
+
1213
+ end