22
33module Monetize
44 class Parser
5- CURRENCY_SYMBOLS = {
5+ INITIAL_CURRENCY_SYMBOLS = {
66 '$' => 'USD' ,
77 '€' => 'EUR' ,
88 '£' => 'GBP' ,
@@ -28,16 +28,35 @@ class Parser
2828 'S$' => 'SGD' ,
2929 'HK$' => 'HKD' ,
3030 '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+
3537 MULTIPLIER_SUFFIXES = { 'K' => 3 , 'M' => 6 , 'B' => 9 , 'T' => 12 }
3638 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
3840
3941 DEFAULT_DECIMAL_MARK = '.' . freeze
4042
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+
4160 def initialize ( input , fallback_currency = Money . default_currency , options = { } )
4261 @input = input . to_s . strip
4362 @fallback_currency = fallback_currency
@@ -66,6 +85,17 @@ def parse
6685
6786 private
6887
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+
6999 def to_big_decimal ( value )
70100 BigDecimal ( value )
71101 rescue ::ArgumentError => err
@@ -75,11 +105,8 @@ def to_big_decimal(value)
75105 attr_reader :input , :fallback_currency , :options
76106
77107 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?
83110
84111 computed_currency || fallback_currency || Money . default_currency
85112 end
@@ -100,9 +127,18 @@ def apply_sign(negative, amount)
100127 negative ? amount * -1 : amount
101128 end
102129
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
106142 end
107143
108144 def extract_major_minor ( num , currency )
@@ -127,21 +163,20 @@ def minor_has_correct_dp_for_currency_subunit?(minor, currency)
127163
128164 def extract_major_minor_with_single_delimiter ( num , currency , delimiter )
129165 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+
131168 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 ]
135170 end
136171 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 ]
143176 end
144177 end
178+
179+ extract_major_minor_with_tentative_delimiter ( num , delimiter )
145180 end
146181
147182 def extract_major_minor_with_tentative_delimiter ( num , delimiter )
@@ -166,7 +201,9 @@ def extract_major_minor_with_tentative_delimiter(num, delimiter)
166201 end
167202
168203 def extract_multiplier
169- if ( matches = MULTIPLIER_REGEXP . match ( input ) )
204+ matches = MULTIPLIER_REGEXP . match ( input )
205+
206+ if matches
170207 multiplier_suffix = matches [ 2 ] . upcase
171208 [ MULTIPLIER_SUFFIXES [ multiplier_suffix ] , "#{ $1} #{ $3} " ]
172209 else
0 commit comments