money 5.1.0.beta1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,7 @@
4
4
  "iso_code": "EEK",
5
5
  "name": "Estonian Kroon",
6
6
  "symbol": "KR",
7
+ "alternate_symbols": [],
7
8
  "subunit": "Sent",
8
9
  "subunit_to_unit": 100,
9
10
  "symbol_first": false,
@@ -17,6 +18,7 @@
17
18
  "iso_code": "GHS",
18
19
  "name": "Ghanaian Cedi",
19
20
  "symbol": "₵",
21
+ "alternate_symbols": ["GH₵"],
20
22
  "subunit": "Pesewa",
21
23
  "subunit_to_unit": 100,
22
24
  "symbol_first": true,
@@ -30,6 +32,7 @@
30
32
  "iso_code": "TMM",
31
33
  "name": "Turkmenistani Manat",
32
34
  "symbol": "m",
35
+ "alternate_symbols": [],
33
36
  "subunit": "Tennesi",
34
37
  "subunit_to_unit": 100,
35
38
  "symbol_first": false,
@@ -43,6 +46,7 @@
43
46
  "iso_code": "JPY",
44
47
  "name": "Japanese Yen",
45
48
  "symbol": "¥",
49
+ "alternate_symbols": ["円", "圓"],
46
50
  "subunit": "Sen",
47
51
  "subunit_to_unit": 100,
48
52
  "symbol_first": true,
@@ -56,6 +60,7 @@
56
60
  "iso_code": "ZWD",
57
61
  "name": "Zimbabwean Dollar",
58
62
  "symbol": "$",
63
+ "alternate_symbols": ["Z$"],
59
64
  "subunit": "Cent",
60
65
  "subunit_to_unit": 100,
61
66
  "symbol_first": true,
@@ -69,6 +74,7 @@
69
74
  "iso_code": "ZWL",
70
75
  "name": "Zimbabwean Dollar",
71
76
  "symbol": "$",
77
+ "alternate_symbols": ["Z$"],
72
78
  "subunit": "Cent",
73
79
  "subunit_to_unit": 100,
74
80
  "symbol_first": true,
@@ -82,6 +88,7 @@
82
88
  "iso_code": "ZWN",
83
89
  "name": "Zimbabwean Dollar",
84
90
  "symbol": "$",
91
+ "alternate_symbols": ["Z$"],
85
92
  "subunit": "Cent",
86
93
  "subunit_to_unit": 100,
87
94
  "symbol_first": true,
@@ -95,6 +102,7 @@
95
102
  "iso_code": "ZWR",
96
103
  "name": "Zimbabwean Dollar",
97
104
  "symbol": "$",
105
+ "alternate_symbols": ["Z$"],
98
106
  "subunit": "Cent",
99
107
  "subunit_to_unit": 100,
100
108
  "symbol_first": true,
@@ -1,7 +1,6 @@
1
1
  require "bigdecimal"
2
2
  require "bigdecimal/util"
3
3
  require "i18n" rescue LoadError
4
- require "money/currency_loader"
5
4
  require "money/currency"
6
5
  require "money/money"
7
6
  require "money/core_extensions"
@@ -7,7 +7,12 @@ class Money
7
7
  # Represents a specific currency unit.
8
8
  class Currency
9
9
  include Comparable
10
- extend CurrencyLoader
10
+
11
+ require "money/currency/loader"
12
+ extend Loader
13
+
14
+ require "money/currency/heuristics"
15
+ extend Heuristics
11
16
 
12
17
  # Thrown when an unknown currency is requested.
13
18
  class UnknownCurrency < StandardError; end
@@ -0,0 +1,149 @@
1
+ # encoding: utf-8
2
+
3
+ module Money::Currency::Heuristics
4
+
5
+ # An robust and efficient algorithm for finding currencies in
6
+ # text. Using several algorithms it can find symbols, iso codes and
7
+ # even names of currencies.
8
+ # Although not recommendable, it can also attempt to find the given
9
+ # currency in an entire sentence
10
+ #
11
+ # Returns: Array (matched results)
12
+ def analyze(str)
13
+ return Analyzer.new(str, search_tree).process
14
+ end
15
+
16
+ private
17
+
18
+ # Build a search tree from the currency database
19
+ def search_tree
20
+ @_search_tree ||= {
21
+ :by_symbol => currencies_by_symbol,
22
+ :by_iso_code => currencies_by_iso_code,
23
+ :by_name => currencies_by_name
24
+ }
25
+ end
26
+
27
+ def currencies_by_symbol
28
+ {}.tap do |r|
29
+ table.each do |dummy, c|
30
+ symbol = (c[:symbol]||"").downcase
31
+ symbol.chomp!('.')
32
+ (r[symbol] ||= []) << c
33
+
34
+ (c[:alternate_symbols]||[]).each do |ac|
35
+ ac = ac.downcase
36
+ ac.chomp!('.')
37
+ (r[ac] ||= []) << c
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def currencies_by_iso_code
44
+ {}.tap do |r|
45
+ table.each do |dummy,c|
46
+ (r[c[:iso_code].downcase] ||= []) << c
47
+ end
48
+ end
49
+ end
50
+
51
+ def currencies_by_name
52
+ {}.tap do |r|
53
+ table.each do |dummy,c|
54
+ name_parts = c[:name].downcase.split
55
+ name_parts.each {|part| part.chomp!('.')}
56
+
57
+ # construct one branch per word
58
+ root = r
59
+ while name_part = name_parts.shift
60
+ root = (root[name_part] ||= {})
61
+ end
62
+
63
+ # the leaf is a currency
64
+ (root[:value] ||= []) << c
65
+ 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
+
79
+ def process
80
+ format
81
+ return [] if str.empty?
82
+
83
+ search_by_symbol
84
+ search_by_iso_code
85
+ search_by_name
86
+
87
+ prepare_reply
88
+ end
89
+
90
+ def format
91
+ str.gsub!(/[\r\n\t]/,'')
92
+ str.gsub!(/[0-9][\.,:0-9]*[0-9]/,'')
93
+ str.gsub!(/[0-9]/, '')
94
+ str.downcase!
95
+ @words = str.split
96
+ @words.each {|word| word.chomp!('.'); word.chomp!(',') }
97
+ end
98
+
99
+ def search_by_symbol
100
+ words.each do |word|
101
+ if found = search_tree[:by_symbol][word]
102
+ currencies.concat(found)
103
+ end
104
+ end
105
+ end
106
+
107
+ def search_by_iso_code
108
+ words.each do |word|
109
+ if found = search_tree[:by_iso_code][word]
110
+ currencies.concat(found)
111
+ 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
+
121
+ search_words = words.dup
122
+
123
+ while search_words.length > 0
124
+ root = search_tree[:by_name]
125
+
126
+ search_words.each do |word|
127
+ if root = root[word]
128
+ if root[:value]
129
+ currencies.concat(root[:value])
130
+ end
131
+ else
132
+ break
133
+ end
134
+ end
135
+
136
+ search_words.delete_at(0)
137
+ end
138
+ end
139
+
140
+ def prepare_reply
141
+ codes = currencies.map do |currency|
142
+ currency[:iso_code]
143
+ end
144
+ codes.uniq!
145
+ codes.sort!
146
+ codes
147
+ end
148
+ end
149
+ end
@@ -1,7 +1,7 @@
1
- module CurrencyLoader
1
+ module Money::Currency::Loader
2
2
  extend self
3
3
 
4
- DATA_PATH = File.expand_path("../../../config", __FILE__)
4
+ DATA_PATH = File.expand_path("../../../../config", __FILE__)
5
5
 
6
6
  # Loads and returns the currencies stored in JSON files in the config directory.
7
7
  #
@@ -42,7 +42,7 @@ class Money
42
42
  # from the stated currency string
43
43
  c = if Money.assume_from_symbol && i =~ /^(\$|€|£)/
44
44
  case i
45
- when /^$/ then "USD"
45
+ when /^\$/ then "USD"
46
46
  when /^€/ then "EUR"
47
47
  when /^£/ then "GBP"
48
48
  end
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |s|
3
3
  s.name = "money"
4
- s.version = "5.1.0.beta1"
4
+ s.version = "5.1.0"
5
5
  s.platform = Gem::Platform::RUBY
6
6
  s.authors = ["Tobias Luetke", "Hongli Lai", "Jeremy McNevin",
7
7
  "Shane Emmons", "Simone Carletti"]
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Money::Currency::Heuristics do
5
+ describe "#analyze_string" do
6
+ let(:it) { Money::Currency }
7
+
8
+ it "is not affected by blank characters and numbers" do
9
+ it.analyze('123').should == []
10
+ it.analyze('\n123 \t').should == []
11
+ end
12
+
13
+ it "returns nothing when given nothing" do
14
+ it.analyze('').should == []
15
+ it.analyze(nil).should == []
16
+ end
17
+
18
+ it "finds a currency by use of its symbol" do
19
+ it.analyze('zł').should == ['PLN']
20
+ end
21
+
22
+ it "is not affected by trailing dot" do
23
+ it.analyze('zł.').should == ['PLN']
24
+ end
25
+
26
+ it "finds match even if has numbers after" do
27
+ it.analyze('zł 123').should == ['PLN']
28
+ end
29
+
30
+ it "finds match even if has numbers before" do
31
+ it.analyze('123 zł').should == ['PLN']
32
+ end
33
+
34
+ it "find match even if symbol is next to number" do
35
+ it.analyze('300zł').should == ['PLN']
36
+ end
37
+
38
+ it "finds match even if has numbers with delimiters" do
39
+ it.analyze('zł 123,000.50').should == ['PLN']
40
+ it.analyze('zł123,000.50').should == ['PLN']
41
+ end
42
+
43
+ it "finds currencies with dots in symbols" do
44
+ it.analyze('L.E.').should == ['EGP']
45
+ end
46
+
47
+ it "finds by name" do
48
+ it.analyze('1900 bulgarian lev').should == ['BGN']
49
+ it.analyze('Swedish Krona').should == ['SEK']
50
+ end
51
+
52
+ it "Finds several currencies when several match" do
53
+ r = it.analyze('$400')
54
+ r.should include("ARS")
55
+ r.should include("USD")
56
+ r.should include("NZD")
57
+
58
+ r = it.analyze('9000 £')
59
+ r.should include("GBP")
60
+ r.should include("SHP")
61
+ r.should include("SYP")
62
+ end
63
+
64
+ it "should use alternate symbols" do
65
+ it.analyze('US$').should == ['USD']
66
+ end
67
+
68
+ it "finds a currency by use of its iso code" do
69
+ it.analyze('USD 200').should == ['USD']
70
+ end
71
+
72
+ it "finds currencies in the middle of a sentence!" do
73
+ it.analyze('It would be nice to have 10000 Albanian lek by tomorrow!').should == ['ALL']
74
+ end
75
+
76
+ it "finds several currencies in the same text!" do
77
+ it.analyze("10EUR is less than 100:- but really, I want US$1").should == ['EUR', 'SEK', 'USD']
78
+ end
79
+
80
+ it "should function with unicode characters" do
81
+ it.analyze("10 դր.").should == ["AMD"]
82
+ end
83
+ end
84
+ end
@@ -124,7 +124,7 @@ describe Money, "formatting" do
124
124
  one_thousand["EUR"].should == "€1.000,00"
125
125
 
126
126
  # Rupees
127
- one_thousand["INR"].should == "1,000.00"
127
+ one_thousand["INR"].should == "1,000.00"
128
128
  one_thousand["NPR"].should == "₨1,000.00"
129
129
  one_thousand["SCR"].should == "1,000.00 ₨"
130
130
  one_thousand["LKR"].should == "1,000.00 ₨"
@@ -223,7 +223,7 @@ describe Money, "formatting" do
223
223
  one["EUR"].should == "€1,00"
224
224
 
225
225
  # Rupees
226
- one["INR"].should == "1.00"
226
+ one["INR"].should == "1.00"
227
227
  one["NPR"].should == "₨1.00"
228
228
  one["SCR"].should == "1.00 ₨"
229
229
  one["LKR"].should == "1.00 ₨"
@@ -361,64 +361,10 @@ describe Money, "formatting" do
361
361
  end
362
362
  end
363
363
 
364
- it "brute forces :subunit_to_unit = 1" do
365
- ("0".."9").each do |amt|
366
- amt.to_money("VUV").format(:symbol => false).should == amt
367
- end
368
- ("-1".."-9").each do |amt|
369
- amt.to_money("VUV").format(:symbol => false).should == amt
370
- end
371
- "1000".to_money("VUV").format(:symbol => false).should == "1,000"
372
- "-1000".to_money("VUV").format(:symbol => false).should == "-1,000"
373
- end
374
-
375
- it "brute forces :subunit_to_unit = 5" do
376
- ("0.0".."9.4").each do |amt|
377
- next if amt[-1].to_i > 4
378
- amt.to_money("MGA").format(:symbol => false).should == amt
379
- end
380
- ("-0.1".."-9.4").each do |amt|
381
- next if amt[-1].to_i > 4
382
- amt.to_money("MGA").format(:symbol => false).should == amt
383
- end
384
- "1000.0".to_money("MGA").format(:symbol => false).should == "1,000.0"
385
- "-1000.0".to_money("MGA").format(:symbol => false).should == "-1,000.0"
386
- end
387
-
388
- it "brute forces :subunit_to_unit = 10" do
389
- ("0.0".."9.9").each do |amt|
390
- amt.to_money("VND").format(:symbol => false).should == amt.to_s.gsub(/\./, ",")
391
- end
392
- ("-0.1".."-9.9").each do |amt|
393
- amt.to_money("VND").format(:symbol => false).should == amt.to_s.gsub(/\./, ",")
394
- end
395
- "1000.0".to_money("VND").format(:symbol => false).should == "1.000,0"
396
- "-1000.0".to_money("VND").format(:symbol => false).should == "-1.000,0"
364
+ it "maintains floating point precision" do
365
+ "0.01".to_money("USD").format(:symbol => false).should == "0.01"
397
366
  end
398
367
 
399
- it "brute forces :subunit_to_unit = 100" do
400
- %w{0.00 0.01 1.00 1.01 5.67 9.99}.each do |amt|
401
- amt.to_money("USD").format(:symbol => false).should == amt
402
- end
403
- %w{-0.01 -1.00 -1.01 -5.67 -9.99}.each do |amt|
404
- amt.to_money("USD").format(:symbol => false).should == amt
405
- end
406
- "1000.00".to_money("USD").format(:symbol => false).should == "1,000.00"
407
- "-1000.00".to_money("USD").format(:symbol => false).should == "-1,000.00"
408
- end
409
-
410
- it "brute forces :subunit_to_unit = 1000" do
411
- %w{0.000 0.001 1.000 1.001 5.678 9.999}.each do |amt|
412
- amt.to_money("IQD").format(:symbol => false).should == amt
413
- end
414
-
415
- %w{-0.001 -1.000 -1.001 -5.678 -9.999}.each do |amt|
416
- amt.to_money("IQD").format(:symbol => false).should == amt
417
- end
418
-
419
- "1000.000".to_money("IQD").format(:symbol => false).should == "1,000.000"
420
- "-1000.000".to_money("IQD").format(:symbol => false).should == "-1,000.000"
421
- end
422
368
  end
423
369
 
424
370
  context "custom currencies with 4 decimal places" do
@@ -27,8 +27,10 @@ describe Money, "parsing" do
27
27
  before do
28
28
  Money.assume_from_symbol = true
29
29
  end
30
- it "parses formatted inputs with the currency passed as a symbol" do
31
- Money.parse("$5.95").should == Money.new(595, 'USD')
30
+ it "parses formatted inputs with the currency passed as a symbol" do
31
+ with_default_currency("EUR") do
32
+ Money.parse("$5.95").should == Money.new(595, 'USD')
33
+ end
32
34
  Money.parse("€5.95").should == Money.new(595, 'EUR')
33
35
  Money.parse(" €5.95 ").should == Money.new(595, 'EUR')
34
36
  Money.parse("£9.99").should == Money.new(999, 'GBP')
@@ -1,8 +1,11 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__)
1
2
  require 'rspec'
2
3
  require 'money'
4
+ require 'support/default_currency_helper'
3
5
 
4
6
  RSpec.configure do |c|
5
7
  c.order = "rand"
8
+ c.include DefaultCurrencyHelper
6
9
  end
7
10
 
8
11
  def silence_warnings