2
2
3
3
module Monetize
4
4
class Parser
5
- CURRENCY_SYMBOLS = {
5
+ INITIAL_CURRENCY_SYMBOLS = {
6
6
'$' => 'USD' ,
7
7
'€' => 'EUR' ,
8
8
'£' => 'GBP' ,
@@ -28,16 +28,35 @@ class Parser
28
28
'S$' => 'SGD' ,
29
29
'HK$' => 'HKD' ,
30
30
'NT$' => 'TWD' ,
31
- '₱' => 'PHP' ,
32
- }
33
-
34
- CURRENCY_SYMBOL_REGEX = /(?<![A-Z])(#{ CURRENCY_SYMBOLS . keys . map { |key | Regexp . escape ( key ) } . join ( '|' ) } )(?![A-Z])/i
31
+ '₱' => 'PHP'
32
+ } . freeze
33
+
34
+ # FIXME: This ignored symbols could be ambiguous or conflict with other symbols
35
+ IGNORED_SYMBOLS = [ 'kr' , 'NIO$' , 'UM' , 'L' , 'oz t' , "so'm" , 'CUC$' ] . freeze
36
+
35
37
MULTIPLIER_SUFFIXES = { 'K' => 3 , 'M' => 6 , 'B' => 9 , 'T' => 12 }
36
38
MULTIPLIER_SUFFIXES . default = 0
37
- MULTIPLIER_REGEXP = Regexp . new ( format ( ' ^(.*?\d)(%s) \b([^\d]*)$' , MULTIPLIER_SUFFIXES . keys . join ( '|' ) ) , 'i' )
39
+ MULTIPLIER_REGEXP = / ^(.*?\d )(#{ MULTIPLIER_SUFFIXES . keys . join ( '|' ) } ) \b ([^\d ]*)$/i
38
40
39
41
DEFAULT_DECIMAL_MARK = '.' . freeze
40
42
43
+ def self . currency_symbols
44
+ @@currency_symbols ||= Money ::Currency . table . reduce ( INITIAL_CURRENCY_SYMBOLS . dup ) do |memo , ( _ , currency ) |
45
+ symbol = currency [ :symbol ]
46
+ symbol = currency [ :disambiguate_symbol ] if memo . key? ( symbol )
47
+
48
+ next memo if is_invalid_currency_symbol? ( symbol )
49
+
50
+ memo [ symbol ] = currency [ :iso_code ] unless memo . value? ( currency [ :iso_code ] )
51
+
52
+ memo
53
+ end . freeze
54
+ end
55
+
56
+ def self . currency_symbol_regex
57
+ @@currency_symbol_regex ||= /(?<![A-Z])(#{ currency_symbols . keys . map { |key | Regexp . escape ( key ) } . join ( '|' ) } )(?![A-Z])/i
58
+ end
59
+
41
60
def initialize ( input , fallback_currency = Money . default_currency , options = { } )
42
61
@input = input . to_s . strip
43
62
@fallback_currency = fallback_currency
@@ -66,6 +85,17 @@ def parse
66
85
67
86
private
68
87
88
+ def self . is_invalid_currency_symbol? ( symbol )
89
+ currency_symbol_blank? ( symbol ) ||
90
+ symbol . include? ( '.' ) || # Ignore symbols with dots because they can be confused with decimal marks
91
+ IGNORED_SYMBOLS . include? ( symbol ) ||
92
+ MULTIPLIER_REGEXP . match? ( "1#{ symbol } " ) # Ignore symbols that can be confused with multipliers
93
+ end
94
+
95
+ def self . currency_symbol_blank? ( symbol )
96
+ symbol . nil? || symbol . empty?
97
+ end
98
+
69
99
def to_big_decimal ( value )
70
100
BigDecimal ( value )
71
101
rescue ::ArgumentError => err
@@ -75,11 +105,8 @@ def to_big_decimal(value)
75
105
attr_reader :input , :fallback_currency , :options
76
106
77
107
def parse_currency
78
- computed_currency = nil
79
- computed_currency = input [ /[A-Z]{2,3}/ ]
80
- computed_currency = nil unless Monetize ::Parser ::CURRENCY_SYMBOLS . value? ( computed_currency )
81
- computed_currency ||= compute_currency if assume_from_symbol?
82
-
108
+ computed_currency = compute_currency_from_iso_code
109
+ computed_currency ||= compute_currency_from_symbol if assume_from_symbol?
83
110
84
111
computed_currency || fallback_currency || Money . default_currency
85
112
end
@@ -100,9 +127,18 @@ def apply_sign(negative, amount)
100
127
negative ? amount * -1 : amount
101
128
end
102
129
103
- def compute_currency
104
- match = input . match ( CURRENCY_SYMBOL_REGEX )
105
- CURRENCY_SYMBOLS [ match . to_s ] if match
130
+ def compute_currency_from_iso_code
131
+ computed_currency = input [ /[A-Z]{2,4}/ ]
132
+
133
+ return unless computed_currency
134
+
135
+ computed_currency if self . class . currency_symbols . value? ( computed_currency )
136
+ end
137
+
138
+ def compute_currency_from_symbol
139
+ match = input . match ( self . class . currency_symbol_regex )
140
+
141
+ self . class . currency_symbols [ match . to_s ] if match
106
142
end
107
143
108
144
def extract_major_minor ( num , currency )
@@ -127,21 +163,20 @@ def minor_has_correct_dp_for_currency_subunit?(minor, currency)
127
163
128
164
def extract_major_minor_with_single_delimiter ( num , currency , delimiter )
129
165
if expect_whole_subunits?
130
- _possible_major , possible_minor = split_major_minor ( num , delimiter )
166
+ possible_major , possible_minor = split_major_minor ( num , delimiter )
167
+
131
168
if minor_has_correct_dp_for_currency_subunit? ( possible_minor , currency )
132
- split_major_minor ( num , delimiter )
133
- else
134
- extract_major_minor_with_tentative_delimiter ( num , delimiter )
169
+ return [ possible_major , possible_minor ]
135
170
end
136
171
else
137
- if delimiter == currency . decimal_mark
138
- split_major_minor ( num , delimiter )
139
- elsif Monetize . enforce_currency_delimiters && delimiter == currency . thousands_separator
140
- [ num . gsub ( delimiter , '' ) , 0 ]
141
- else
142
- extract_major_minor_with_tentative_delimiter ( num , delimiter )
172
+ return split_major_minor ( num , delimiter ) if delimiter == currency . decimal_mark
173
+
174
+ if Monetize . enforce_currency_delimiters && delimiter == currency . thousands_separator
175
+ return [ num . gsub ( delimiter , '' ) , 0 ]
143
176
end
144
177
end
178
+
179
+ extract_major_minor_with_tentative_delimiter ( num , delimiter )
145
180
end
146
181
147
182
def extract_major_minor_with_tentative_delimiter ( num , delimiter )
@@ -166,7 +201,9 @@ def extract_major_minor_with_tentative_delimiter(num, delimiter)
166
201
end
167
202
168
203
def extract_multiplier
169
- if ( matches = MULTIPLIER_REGEXP . match ( input ) )
204
+ matches = MULTIPLIER_REGEXP . match ( input )
205
+
206
+ if matches
170
207
multiplier_suffix = matches [ 2 ] . upcase
171
208
[ MULTIPLIER_SUFFIXES [ multiplier_suffix ] , "#{ $1} #{ $3} " ]
172
209
else
0 commit comments