diff --git a/.gitignore b/.gitignore index 2e57a9e2c..f4a763408 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ test/version_tmp tmp .ruby-version .tool-versions +.ruby-gemset diff --git a/lib/monetize/parser.rb b/lib/monetize/parser.rb index bace626ad..acb8f4354 100644 --- a/lib/monetize/parser.rb +++ b/lib/monetize/parser.rb @@ -2,7 +2,7 @@ module Monetize class Parser - CURRENCY_SYMBOLS = { + INITIAL_CURRENCY_SYMBOLS = { '$' => 'USD', '€' => 'EUR', '£' => 'GBP', @@ -28,44 +28,76 @@ class Parser 'S$' => 'SGD', 'HK$'=> 'HKD', 'NT$'=> 'TWD', - '₱' => 'PHP', - } - - CURRENCY_SYMBOL_REGEX = /(? 'PHP' + }.freeze + + # FIXME: This ignored symbols could be ambiguous or conflict with other symbols + IGNORED_SYMBOLS = ['kr', 'NIO$', 'UM', 'L', 'oz t', "so'm", 'CUC$'].freeze + MULTIPLIER_SUFFIXES = { 'K' => 3, 'M' => 6, 'B' => 9, 'T' => 12 } MULTIPLIER_SUFFIXES.default = 0 - MULTIPLIER_REGEXP = Regexp.new(format('^(.*?\d)(%s)\b([^\d]*)$', MULTIPLIER_SUFFIXES.keys.join('|')), 'i') + MULTIPLIER_REGEXP = /^(.*?\d)(#{MULTIPLIER_SUFFIXES.keys.join('|')})\b([^\d]*)$/i DEFAULT_DECIMAL_MARK = '.'.freeze + def self.currency_symbols + @@currency_symbols ||= Money::Currency.table.reduce(INITIAL_CURRENCY_SYMBOLS.dup) do |memo, (_, currency)| + symbol = currency[:symbol] + symbol = currency[:disambiguate_symbol] if memo.key?(symbol) + + next memo if is_invalid_currency_symbol?(symbol) + + memo[symbol] = currency[:iso_code] unless memo.value?(currency[:iso_code]) + + memo + end.freeze + end + + def self.currency_symbol_regex + @@currency_symbol_regex ||= /(? err @@ -75,11 +107,8 @@ def to_big_decimal(value) attr_reader :input, :fallback_currency, :options def parse_currency - computed_currency = nil - computed_currency = input[/[A-Z]{2,3}/] - computed_currency = nil unless Monetize::Parser::CURRENCY_SYMBOLS.value?(computed_currency) - computed_currency ||= compute_currency if assume_from_symbol? - + computed_currency = compute_currency_from_iso_code + computed_currency ||= compute_currency_from_symbol if assume_from_symbol? computed_currency || fallback_currency || Money.default_currency end @@ -96,16 +125,21 @@ def apply_multiplier(multiplier_exp, amount) amount * 10**multiplier_exp end - def apply_sign(negative, amount) - negative ? amount * -1 : amount + def compute_currency_from_iso_code + computed_currency = input[/[A-Z]{2,4}/] + + return unless computed_currency + + computed_currency if self.class.currency_symbols.value?(computed_currency) end - def compute_currency - match = input.match(CURRENCY_SYMBOL_REGEX) - CURRENCY_SYMBOLS[match.to_s] if match + def compute_currency_from_symbol + match = input.match(self.class.currency_symbol_regex) + + self.class.currency_symbols[match.to_s] if match end - def extract_major_minor(num, currency) + def extract_major_minor(num) used_delimiters = num.scan(/[^\d]/).uniq case used_delimiters.length @@ -115,33 +149,32 @@ def extract_major_minor(num, currency) thousands_separator, decimal_mark = used_delimiters split_major_minor(num.gsub(thousands_separator, ''), decimal_mark) when 1 - extract_major_minor_with_single_delimiter(num, currency, used_delimiters.first) + extract_major_minor_with_single_delimiter(num, used_delimiters.first) else fail ParseError, 'Invalid amount' end end - def minor_has_correct_dp_for_currency_subunit?(minor, currency) + def minor_has_correct_dp_for_currency_subunit?(minor) minor.length == currency.subunit_to_unit.to_s.length - 1 end - def extract_major_minor_with_single_delimiter(num, currency, delimiter) + def extract_major_minor_with_single_delimiter(num, delimiter) if expect_whole_subunits? - _possible_major, possible_minor = split_major_minor(num, delimiter) - if minor_has_correct_dp_for_currency_subunit?(possible_minor, currency) - split_major_minor(num, delimiter) - else - extract_major_minor_with_tentative_delimiter(num, delimiter) + possible_major, possible_minor = split_major_minor(num, delimiter) + + if minor_has_correct_dp_for_currency_subunit?(possible_minor) + return [possible_major, possible_minor] end else - if delimiter == currency.decimal_mark - split_major_minor(num, delimiter) - elsif Monetize.enforce_currency_delimiters && delimiter == currency.thousands_separator - [num.gsub(delimiter, ''), 0] - else - extract_major_minor_with_tentative_delimiter(num, delimiter) + return split_major_minor(num, delimiter) if delimiter == currency.decimal_mark + + if Monetize.enforce_currency_delimiters && delimiter == currency.thousands_separator + return [num.gsub(delimiter, ''), 0] end end + + extract_major_minor_with_tentative_delimiter(num, delimiter) end def extract_major_minor_with_tentative_delimiter(num, delimiter) @@ -166,7 +199,9 @@ def extract_major_minor_with_tentative_delimiter(num, delimiter) end def extract_multiplier - if (matches = MULTIPLIER_REGEXP.match(input)) + matches = MULTIPLIER_REGEXP.match(input) + + if matches multiplier_suffix = matches[2].upcase [MULTIPLIER_SUFFIXES[multiplier_suffix], "#{$1}#{$3}"] else @@ -175,16 +210,19 @@ def extract_multiplier end def extract_sign(input) - result = (input =~ /^-+(.*)$/ || input =~ /^(.*)-+$/) ? [true, $1] : [false, input] + result = (input =~ /^-+(.*)$/ || input =~ /^(.*)-+$/) ? [-1, $1] : [1, input] fail ParseError, 'Invalid amount (hyphen)' if result[1].include?('-') result end def split_major_minor(num, delimiter) splits = num.split(delimiter) + fail ParseError, 'Invalid amount (multiple delimiters)' if splits.length > 2 - [splits[0], splits[1] || '00'] + splits[1] = '00' if splits.length == 1 + + splits end end end diff --git a/spec/monetize_spec.rb b/spec/monetize_spec.rb index 00aad8002..2f02d4150 100644 --- a/spec/monetize_spec.rb +++ b/spec/monetize_spec.rb @@ -56,10 +56,13 @@ Monetize.assume_from_symbol = false end - Monetize::Parser::CURRENCY_SYMBOLS.each_pair do |symbol, iso_code| + Monetize::Parser.currency_symbols.each_pair do |symbol, iso_code| context iso_code do let(:currency) { Money::Currency.find(iso_code) } - let(:amount) { 5_95 } + let(:amount) do + # FIXME: The exponent > 3 (e.g. BTC) causes problems when converting to string from float + (currency.exponent > 3)? (595 * currency.subunit_to_unit) : 595 + end let(:amount_in_units) { amount.to_f / currency.subunit_to_unit } it 'ensures correct amount calculations for test' do @@ -109,13 +112,21 @@ end it 'parses formatted inputs without currency detection when overridden' do - expect(Monetize.parse("#{symbol}5.95", nil, assume_from_symbol: false)).to eq Money.new(amount, 'USD') + if Monetize::Parser.currency_symbols.value?(symbol) + currency_iso_code = symbol + amount_str = currency.exponent == 0 ? '595' : '5.95' + else + currency_iso_code = 'USD' + amount_str = '5.95' + end + + expect(Monetize.parse("#{symbol}#{amount_str}", nil, assume_from_symbol: false)).to eq Money.new(595, currency_iso_code) end end end it 'should assume default currency if not a recognised symbol' do - expect(Monetize.parse('L9.99')).to eq Money.new(999, 'USD') + expect(Monetize.parse('NRS9.99')).to eq Money.new(999, 'USD') end it 'should use provided currency over symbol' do