22
33module Monetize
44 class Parser
5- CURRENCY_SYMBOLS = {
5+ INITIAL_CURRENCY_SYMBOLS = {
66 '$' => 'USD' ,
77 '€' => 'EUR' ,
88 '£' => 'GBP' ,
@@ -28,15 +28,30 @@ class Parser
2828 'S$' => 'SGD' ,
2929 'HK$' => 'HKD' ,
3030 'NT$' => 'TWD' ,
31- '₱' => 'PHP' ,
32- }
31+ '₱' => 'PHP'
32+ } . freeze
33+ # FIXME: This ignored symbols could be ambiguous or conflict with other symbols
34+ IGNORED_SYMBOLS = [ 'kr' , 'NIO$' , 'UM' , 'L' , 'oz t' , "so'm" , 'CUC$' ] . freeze
3335
3436 MULTIPLIER_SUFFIXES = { 'K' => 3 , 'M' => 6 , 'B' => 9 , 'T' => 12 }
3537 MULTIPLIER_SUFFIXES . default = 0
3638 MULTIPLIER_REGEXP = Regexp . new ( format ( '^(.*?\d)(%s)\b([^\d]*)$' , MULTIPLIER_SUFFIXES . keys . join ( '|' ) ) , 'i' )
3739
3840 DEFAULT_DECIMAL_MARK = '.' . freeze
3941
42+ def self . currency_symbols
43+ @@currency_symbols ||= Money ::Currency . table . reduce ( INITIAL_CURRENCY_SYMBOLS . dup ) do |memo , ( _ , currency ) |
44+ symbol = currency [ :symbol ]
45+ symbol = currency [ :disambiguate_symbol ] if symbol && memo . key? ( symbol )
46+
47+ next memo if is_invalid_currency_symbol? ( symbol )
48+
49+ memo [ symbol ] = currency [ :iso_code ] unless memo . value? ( currency [ :iso_code ] )
50+
51+ memo
52+ end . freeze
53+ end
54+
4055 def initialize ( input , fallback_currency = Money . default_currency , options = { } )
4156 @input = input . to_s . strip
4257 @fallback_currency = fallback_currency
@@ -65,6 +80,17 @@ def parse
6580
6681 private
6782
83+ def self . is_invalid_currency_symbol? ( symbol )
84+ currency_symbol_blank? ( symbol ) ||
85+ symbol . include? ( '.' ) || # Ignore symbols with dots because they can be confused with decimal marks
86+ IGNORED_SYMBOLS . include? ( symbol ) ||
87+ MULTIPLIER_REGEXP . match? ( "1#{ symbol } " ) # Ignore symbols that can be confused with multipliers
88+ end
89+
90+ def self . currency_symbol_blank? ( symbol )
91+ symbol . nil? || symbol . empty?
92+ end
93+
6894 def to_big_decimal ( value )
6995 BigDecimal ( value )
7096 rescue ::ArgumentError => err
@@ -74,11 +100,8 @@ def to_big_decimal(value)
74100 attr_reader :input , :fallback_currency , :options
75101
76102 def parse_currency
77- computed_currency = nil
78- computed_currency = input [ /[A-Z]{2,3}/ ]
79- computed_currency = nil unless Monetize ::Parser ::CURRENCY_SYMBOLS . value? ( computed_currency )
80- computed_currency ||= compute_currency if assume_from_symbol?
81-
103+ computed_currency = compute_currency_from_iso_code
104+ computed_currency ||= compute_currency_from_symbol if assume_from_symbol?
82105
83106 computed_currency || fallback_currency || Money . default_currency
84107 end
@@ -99,9 +122,18 @@ def apply_sign(negative, amount)
99122 negative ? amount * -1 : amount
100123 end
101124
102- def compute_currency
125+ def compute_currency_from_iso_code
126+ computed_currency = input [ /[A-Z]{2,4}/ ]
127+
128+ return unless computed_currency
129+
130+ computed_currency if self . class . currency_symbols . value? ( computed_currency )
131+ end
132+
133+ def compute_currency_from_symbol
103134 match = input . match ( currency_symbol_regex )
104- CURRENCY_SYMBOLS [ match . to_s ] if match
135+
136+ self . class . currency_symbols [ match . to_s ] if match
105137 end
106138
107139 def extract_major_minor ( num , currency )
@@ -127,20 +159,19 @@ def minor_has_correct_dp_for_currency_subunit?(minor, currency)
127159 def extract_major_minor_with_single_delimiter ( num , currency , delimiter )
128160 if expect_whole_subunits?
129161 possible_major , possible_minor = split_major_minor ( num , delimiter )
162+
130163 if minor_has_correct_dp_for_currency_subunit? ( possible_minor , currency )
131- split_major_minor ( num , delimiter )
132- else
133- extract_major_minor_with_tentative_delimiter ( num , delimiter )
164+ return [ possible_major , possible_minor ]
134165 end
135166 else
136- if delimiter == currency . decimal_mark
137- split_major_minor ( num , delimiter )
138- elsif Monetize . enforce_currency_delimiters && delimiter == currency . thousands_separator
139- [ num . gsub ( delimiter , '' ) , 0 ]
140- else
141- extract_major_minor_with_tentative_delimiter ( num , delimiter )
167+ return split_major_minor ( num , delimiter ) if delimiter == currency . decimal_mark
168+
169+ if Monetize . enforce_currency_delimiters && delimiter == currency . thousands_separator
170+ return [ num . gsub ( delimiter , '' ) , 0 ]
142171 end
143172 end
173+
174+ extract_major_minor_with_tentative_delimiter ( num , delimiter )
144175 end
145176
146177 def extract_major_minor_with_tentative_delimiter ( num , delimiter )
@@ -165,7 +196,9 @@ def extract_major_minor_with_tentative_delimiter(num, delimiter)
165196 end
166197
167198 def extract_multiplier
168- if ( matches = MULTIPLIER_REGEXP . match ( input ) )
199+ matches = MULTIPLIER_REGEXP . match ( input )
200+
201+ if matches
169202 multiplier_suffix = matches [ 2 ] . upcase
170203 [ MULTIPLIER_SUFFIXES [ multiplier_suffix ] , "#{ $1} #{ $3} " ]
171204 else
@@ -180,7 +213,7 @@ def extract_sign(input)
180213 end
181214
182215 def regex_safe_symbols
183- CURRENCY_SYMBOLS . keys . map { |key | Regexp . escape ( key ) } . join ( '|' )
216+ self . class . currency_symbols . keys . map { |key | Regexp . escape ( key ) } . join ( '|' )
184217 end
185218
186219 def split_major_minor ( num , delimiter )
0 commit comments