money 6.0.1.beta2 → 6.0.1.beta3
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.
- 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
|