money 6.0.1.beta2 → 6.0.1.beta3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/money/bank/variable_exchange.rb +67 -28
- data/lib/money/currency.rb +69 -108
- data/lib/money/currency/heuristics.rb +121 -117
- data/lib/money/currency/loader.rb +19 -17
- data/lib/money/money.rb +81 -85
- data/lib/money/money/arithmetic.rb +42 -31
- data/lib/money/money/formatting.rb +45 -52
- data/lib/money/version.rb +1 -1
- data/spec/bank/variable_exchange_spec.rb +42 -0
- metadata +2 -2
@@ -1,149 +1,153 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
class Money
|
4
|
+
class Currency
|
5
|
+
module Heuristics
|
6
|
+
|
7
|
+
# An robust and efficient algorithm for finding currencies in
|
8
|
+
# text. Using several algorithms it can find symbols, iso codes and
|
9
|
+
# even names of currencies.
|
10
|
+
# Although not recommendable, it can also attempt to find the given
|
11
|
+
# currency in an entire sentence
|
12
|
+
#
|
13
|
+
# Returns: Array (matched results)
|
14
|
+
def analyze(str)
|
15
|
+
return Analyzer.new(str, search_tree).process
|
16
|
+
end
|
15
17
|
|
16
|
-
|
18
|
+
private
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
# Build a search tree from the currency database
|
21
|
+
def search_tree
|
22
|
+
@_search_tree ||= {
|
23
|
+
:by_symbol => currencies_by_symbol,
|
24
|
+
:by_iso_code => currencies_by_iso_code,
|
25
|
+
:by_name => currencies_by_name
|
26
|
+
}
|
27
|
+
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
def currencies_by_symbol
|
30
|
+
{}.tap do |r|
|
31
|
+
table.each do |dummy, c|
|
32
|
+
symbol = (c[:symbol]||"").downcase
|
33
|
+
symbol.chomp!('.')
|
34
|
+
(r[symbol] ||= []) << c
|
35
|
+
|
36
|
+
(c[:alternate_symbols]||[]).each do |ac|
|
37
|
+
ac = ac.downcase
|
38
|
+
ac.chomp!('.')
|
39
|
+
(r[ac] ||= []) << c
|
40
|
+
end
|
41
|
+
end
|
38
42
|
end
|
39
43
|
end
|
40
|
-
end
|
41
|
-
end
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
def currencies_by_iso_code
|
46
|
+
{}.tap do |r|
|
47
|
+
table.each do |dummy,c|
|
48
|
+
(r[c[:iso_code].downcase] ||= []) << c
|
49
|
+
end
|
50
|
+
end
|
47
51
|
end
|
48
|
-
end
|
49
|
-
end
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
def currencies_by_name
|
54
|
+
{}.tap do |r|
|
55
|
+
table.each do |dummy,c|
|
56
|
+
name_parts = c[:name].downcase.split
|
57
|
+
name_parts.each {|part| part.chomp!('.')}
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
# construct one branch per word
|
60
|
+
root = r
|
61
|
+
while name_part = name_parts.shift
|
62
|
+
root = (root[name_part] ||= {})
|
63
|
+
end
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
+
# the leaf is a currency
|
66
|
+
(root[:value] ||= []) << c
|
67
|
+
end
|
68
|
+
end
|
65
69
|
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
class Analyzer
|
70
|
-
attr_reader :search_tree, :words
|
71
|
-
attr_accessor :str, :currencies
|
72
|
-
|
73
|
-
def initialize str, search_tree
|
74
|
-
@str = (str||'').dup
|
75
|
-
@search_tree = search_tree
|
76
|
-
@currencies = []
|
77
|
-
end
|
78
70
|
|
79
|
-
|
80
|
-
|
81
|
-
|
71
|
+
class Analyzer
|
72
|
+
attr_reader :search_tree, :words
|
73
|
+
attr_accessor :str, :currencies
|
82
74
|
|
83
|
-
|
84
|
-
|
85
|
-
|
75
|
+
def initialize str, search_tree
|
76
|
+
@str = (str||'').dup
|
77
|
+
@search_tree = search_tree
|
78
|
+
@currencies = []
|
79
|
+
end
|
86
80
|
|
87
|
-
|
88
|
-
|
81
|
+
def process
|
82
|
+
format
|
83
|
+
return [] if str.empty?
|
89
84
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
str.gsub!(/[0-9]/, '')
|
94
|
-
str.downcase!
|
95
|
-
@words = str.split
|
96
|
-
@words.each {|word| word.chomp!('.'); word.chomp!(',') }
|
97
|
-
end
|
85
|
+
search_by_symbol
|
86
|
+
search_by_iso_code
|
87
|
+
search_by_name
|
98
88
|
|
99
|
-
|
100
|
-
words.each do |word|
|
101
|
-
if found = search_tree[:by_symbol][word]
|
102
|
-
currencies.concat(found)
|
89
|
+
prepare_reply
|
103
90
|
end
|
104
|
-
end
|
105
|
-
end
|
106
91
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
92
|
+
def format
|
93
|
+
str.gsub!(/[\r\n\t]/,'')
|
94
|
+
str.gsub!(/[0-9][\.,:0-9]*[0-9]/,'')
|
95
|
+
str.gsub!(/[0-9]/, '')
|
96
|
+
str.downcase!
|
97
|
+
@words = str.split
|
98
|
+
@words.each {|word| word.chomp!('.'); word.chomp!(',') }
|
111
99
|
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def search_by_name
|
116
|
-
# remember, the search tree by name is a construct of branches and leaf!
|
117
|
-
# We need to try every combination of words within the sentence, so we
|
118
|
-
# end up with a x^2 equation, which should be fine as most names are either
|
119
|
-
# one or two words, and this is multiplied with the words of given sentence
|
120
100
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
101
|
+
def search_by_symbol
|
102
|
+
words.each do |word|
|
103
|
+
if found = search_tree[:by_symbol][word]
|
104
|
+
currencies.concat(found)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
125
108
|
|
126
|
-
|
127
|
-
|
128
|
-
if
|
129
|
-
currencies.concat(
|
109
|
+
def search_by_iso_code
|
110
|
+
words.each do |word|
|
111
|
+
if found = search_tree[:by_iso_code][word]
|
112
|
+
currencies.concat(found)
|
130
113
|
end
|
131
|
-
else
|
132
|
-
break
|
133
114
|
end
|
134
115
|
end
|
135
116
|
|
136
|
-
|
137
|
-
|
138
|
-
|
117
|
+
def search_by_name
|
118
|
+
# remember, the search tree by name is a construct of branches and leaf!
|
119
|
+
# We need to try every combination of words within the sentence, so we
|
120
|
+
# end up with a x^2 equation, which should be fine as most names are either
|
121
|
+
# one or two words, and this is multiplied with the words of given sentence
|
122
|
+
|
123
|
+
search_words = words.dup
|
124
|
+
|
125
|
+
while search_words.length > 0
|
126
|
+
root = search_tree[:by_name]
|
127
|
+
|
128
|
+
search_words.each do |word|
|
129
|
+
if root = root[word]
|
130
|
+
if root[:value]
|
131
|
+
currencies.concat(root[:value])
|
132
|
+
end
|
133
|
+
else
|
134
|
+
break
|
135
|
+
end
|
136
|
+
end
|
139
137
|
|
140
|
-
|
141
|
-
|
142
|
-
|
138
|
+
search_words.delete_at(0)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def prepare_reply
|
143
|
+
codes = currencies.map do |currency|
|
144
|
+
currency[:iso_code]
|
145
|
+
end
|
146
|
+
codes.uniq!
|
147
|
+
codes.sort!
|
148
|
+
codes
|
149
|
+
end
|
143
150
|
end
|
144
|
-
codes.uniq!
|
145
|
-
codes.sort!
|
146
|
-
codes
|
147
151
|
end
|
148
152
|
end
|
149
153
|
end
|
@@ -1,22 +1,24 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
class Money
|
2
|
+
class Currency
|
3
|
+
module Loader
|
4
|
+
DATA_PATH = File.expand_path("../../../../config", __FILE__)
|
3
5
|
|
4
|
-
|
6
|
+
# Loads and returns the currencies stored in JSON files in the config directory.
|
7
|
+
#
|
8
|
+
# @return [Hash]
|
9
|
+
def load_currencies
|
10
|
+
currencies = parse_currency_file("currency_iso.json")
|
11
|
+
currencies.merge! parse_currency_file("currency_non_iso.json")
|
12
|
+
currencies.merge! parse_currency_file("currency_backwards_compatible.json")
|
13
|
+
end
|
5
14
|
|
6
|
-
|
7
|
-
#
|
8
|
-
# @return [Hash]
|
9
|
-
def load_currencies
|
10
|
-
currencies = parse_currency_file("currency_iso.json")
|
11
|
-
currencies.merge! parse_currency_file("currency_non_iso.json")
|
12
|
-
currencies.merge! parse_currency_file("currency_backwards_compatible.json")
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
15
|
+
private
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def parse_currency_file(filename)
|
18
|
+
json = File.read("#{DATA_PATH}/#{filename}")
|
19
|
+
json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
|
20
|
+
JSON.parse(json, :symbolize_names => true)
|
21
|
+
end
|
22
|
+
end
|
21
23
|
end
|
22
24
|
end
|
data/lib/money/money.rb
CHANGED
@@ -78,56 +78,36 @@ class Money
|
|
78
78
|
end
|
79
79
|
private :as_d
|
80
80
|
|
81
|
-
# The currency the money is in.
|
82
|
-
#
|
83
|
-
#
|
84
|
-
attr_reader :currency
|
85
|
-
|
86
|
-
# The +Money::Bank+ based object used to perform currency exchanges with.
|
87
|
-
#
|
88
|
-
# @return [Money::Bank::*]
|
89
|
-
attr_reader :bank
|
81
|
+
# @attr_reader [Currency] currency The currency the money is in.
|
82
|
+
# @attr_reader [Money::Bank::*] bank The +Money::Bank+ based object used to
|
83
|
+
# perform currency exchanges with.
|
84
|
+
attr_reader :currency, :bank
|
90
85
|
|
91
86
|
# Class Methods
|
92
87
|
class << self
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
# @
|
99
|
-
|
100
|
-
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
attr_accessor
|
107
|
-
|
108
|
-
# Use this to
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
# Use this to
|
114
|
-
#
|
115
|
-
# @return [true,false]
|
116
|
-
attr_accessor :assume_from_symbol
|
117
|
-
|
118
|
-
# Use this to enable infinite precision cents
|
119
|
-
#
|
120
|
-
# @return [true,false]
|
121
|
-
attr_accessor :infinite_precision
|
122
|
-
|
123
|
-
# Use this to specify the rounding mode
|
88
|
+
# @attr_accessor [Money::Bank::*] default_bank Each Money object is
|
89
|
+
# associated to a bank object, which is responsible for currency exchange.
|
90
|
+
# This property allows you to specify the default bank object. The default
|
91
|
+
# value for this property is an instance of +Bank::VariableExchange.+ It
|
92
|
+
# allows one to specify custom exchange rates.
|
93
|
+
# @attr_accessor [Money::Currency] default_currency The default currency,
|
94
|
+
# which is used when +Money.new+ is called without an explicit currency
|
95
|
+
# argument. The default value is Currency.new("USD"). The value must be a
|
96
|
+
# valid +Money::Currency+ instance.
|
97
|
+
# @attr_accessor [true, false] use_i18n Use this to disable i18n even if
|
98
|
+
# it's used by other objects in your app.
|
99
|
+
# @attr_accessor [true, false] assume_from_symbol Use this to enable the
|
100
|
+
# ability to assume the currency from a passed symbol
|
101
|
+
# @attr_accessor [true, false] infinite_precision Use this to enable
|
102
|
+
# infinite precision cents
|
103
|
+
# @attr_accessor [Integer] conversion_precision Use this to specify
|
104
|
+
# precision for converting Rational to BigDecimal
|
105
|
+
attr_accessor :default_bank, :default_currency, :use_i18n,
|
106
|
+
:assume_from_symbol, :infinite_precision, :conversion_precision
|
107
|
+
|
108
|
+
# @attr_writer rounding_mode Use this to specify the rounding mode
|
124
109
|
attr_writer :rounding_mode
|
125
110
|
|
126
|
-
# Use this to specify precision for converting Rational to BigDecimal
|
127
|
-
#
|
128
|
-
# @return [Integer]
|
129
|
-
attr_accessor :conversion_precision
|
130
|
-
|
131
111
|
# Create a new money object with value 0.
|
132
112
|
#
|
133
113
|
# @param [Currency, String, Symbol] currency The currency to use.
|
@@ -185,10 +165,12 @@ class Money
|
|
185
165
|
# Money.new(1200) * BigDecimal.new('0.029')
|
186
166
|
# end
|
187
167
|
def self.rounding_mode(mode=nil)
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
168
|
+
if mode.nil?
|
169
|
+
Thread.current[:money_rounding_mode] || @rounding_mode
|
170
|
+
else
|
171
|
+
Thread.current[:money_rounding_mode] = mode
|
172
|
+
yield
|
173
|
+
end
|
192
174
|
ensure
|
193
175
|
Thread.current[:money_rounding_mode] = nil
|
194
176
|
end
|
@@ -258,10 +240,9 @@ class Money
|
|
258
240
|
# @see Money.new
|
259
241
|
#
|
260
242
|
def self.new_with_amount(amount, currency = Money.default_currency, bank = Money.default_bank)
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
money
|
243
|
+
from_numeric(amount, currency).tap do |money|
|
244
|
+
money.instance_variable_set("@bank", bank) # Hack! You can't change a bank
|
245
|
+
end
|
265
246
|
end
|
266
247
|
|
267
248
|
# Synonym of #new_with_amount
|
@@ -310,17 +291,14 @@ class Money
|
|
310
291
|
#
|
311
292
|
# @see Money.new_with_dollars
|
312
293
|
#
|
313
|
-
def initialize(
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
@currency = Currency.wrap(currency)
|
322
|
-
@bank = bank
|
323
|
-
end
|
294
|
+
def initialize(obj, currency = Money.default_currency, bank = Money.default_bank)
|
295
|
+
@fractional = obj.fractional
|
296
|
+
@currency = obj.currency
|
297
|
+
@bank = obj.bank
|
298
|
+
rescue NoMethodError
|
299
|
+
@fractional = as_d(obj)
|
300
|
+
@currency = Currency.wrap(currency)
|
301
|
+
@bank = bank
|
324
302
|
end
|
325
303
|
|
326
304
|
# Assuming using a currency using dollars:
|
@@ -479,7 +457,7 @@ class Money
|
|
479
457
|
#
|
480
458
|
# @return [self]
|
481
459
|
def to_money(given_currency = nil)
|
482
|
-
given_currency = Currency.wrap(given_currency)
|
460
|
+
given_currency = Currency.wrap(given_currency)
|
483
461
|
if given_currency.nil? || self.currency == given_currency
|
484
462
|
self
|
485
463
|
else
|
@@ -551,30 +529,42 @@ class Money
|
|
551
529
|
# Money.new(100, "USD").allocate([0.33, 0.33, 0.33]) #=> [Money.new(34), Money.new(33), Money.new(33)]
|
552
530
|
#
|
553
531
|
def allocate(splits)
|
554
|
-
allocations = splits
|
532
|
+
allocations = allocations_from_splits(splits)
|
555
533
|
|
556
534
|
if (allocations - BigDecimal("1")) > Float::EPSILON
|
557
535
|
raise ArgumentError, "splits add to more then 100%"
|
558
536
|
end
|
559
537
|
|
538
|
+
amounts, left_over = amounts_from_splits(allocations, splits)
|
539
|
+
|
540
|
+
unless self.class.infinite_precision
|
541
|
+
left_over.to_i.times { |i| amounts[i % amounts.length] += 1 }
|
542
|
+
end
|
543
|
+
|
544
|
+
amounts.collect { |fractional| Money.new(fractional, currency) }
|
545
|
+
end
|
546
|
+
|
547
|
+
def allocations_from_splits(splits)
|
548
|
+
splits.inject(0) { |sum, n| sum + as_d(n) }
|
549
|
+
end
|
550
|
+
private :allocations_from_splits
|
551
|
+
|
552
|
+
def amounts_from_splits(allocations, splits)
|
560
553
|
left_over = fractional
|
561
554
|
|
562
555
|
amounts = splits.map do |ratio|
|
563
556
|
if self.class.infinite_precision
|
564
|
-
|
557
|
+
fractional * ratio
|
565
558
|
else
|
566
|
-
|
567
|
-
|
568
|
-
|
559
|
+
(fractional * ratio / allocations).floor.tap do |frac|
|
560
|
+
left_over -= frac
|
561
|
+
end
|
569
562
|
end
|
570
563
|
end
|
571
564
|
|
572
|
-
|
573
|
-
left_over.to_i.times { |i| amounts[i % amounts.length] += 1 }
|
574
|
-
end
|
575
|
-
|
576
|
-
amounts.collect { |fractional| Money.new(fractional, currency) }
|
565
|
+
[amounts, left_over]
|
577
566
|
end
|
567
|
+
private :amounts_from_splits
|
578
568
|
|
579
569
|
# Split money amongst parties evenly without loosing pennies.
|
580
570
|
#
|
@@ -588,22 +578,29 @@ class Money
|
|
588
578
|
raise ArgumentError, "need at least one party" if num < 1
|
589
579
|
|
590
580
|
if self.class.infinite_precision
|
591
|
-
|
592
|
-
|
581
|
+
split_infinite(num)
|
582
|
+
else
|
583
|
+
split_flat(num)
|
593
584
|
end
|
585
|
+
end
|
594
586
|
|
587
|
+
def split_infinite(num)
|
588
|
+
amt = div(as_d(num))
|
589
|
+
1.upto(num).map{amt}
|
590
|
+
end
|
591
|
+
private :split_infinite
|
592
|
+
|
593
|
+
def split_flat(num)
|
595
594
|
low = Money.new(fractional / num, currency)
|
596
595
|
high = Money.new(low.fractional + 1, currency)
|
597
596
|
|
598
597
|
remainder = fractional % num
|
599
|
-
result = []
|
600
598
|
|
601
|
-
num.
|
602
|
-
|
599
|
+
Array.new(num).each_with_index.map do |_, index|
|
600
|
+
index < remainder ? high : low
|
603
601
|
end
|
604
|
-
|
605
|
-
result
|
606
602
|
end
|
603
|
+
private :split_flat
|
607
604
|
|
608
605
|
# Round the monetary amount to smallest unit of coinage.
|
609
606
|
#
|
@@ -622,10 +619,9 @@ class Money
|
|
622
619
|
#
|
623
620
|
def round(rounding_mode = self.class.rounding_mode)
|
624
621
|
if self.class.infinite_precision
|
625
|
-
|
622
|
+
Money.new(fractional.round(0, rounding_mode), self.currency)
|
626
623
|
else
|
627
|
-
|
624
|
+
self
|
628
625
|
end
|
629
626
|
end
|
630
|
-
|
631
627
|
end
|