lib/monetize.rb in monetize-1.3.1 vs lib/monetize.rb in monetize-1.4.0

- old
+ new

@@ -1,39 +1,44 @@ # encoding: utf-8 -require "money" -require "monetize/core_extensions" -require "monetize/version" -require "collection" +require 'money' +require 'monetize/core_extensions' +require 'monetize/version' +require 'collection' module Monetize - CURRENCY_SYMBOLS = { - "$" => "USD", - "€" => "EUR", - "£" => "GBP", - "₤" => "GBP", - "R$" => "BRL", - "R" => "ZAR", - "¥" => "JPY", - "C$" => "CAD" + '$' => 'USD', + '€' => 'EUR', + '£' => 'GBP', + '₤' => 'GBP', + 'R$' => 'BRL', + 'R' => 'ZAR', + '¥' => 'JPY', + 'C$' => 'CAD' } MULTIPLIER_SUFFIXES = { - "K" => 3, - "M" => 6, - "B" => 9, - "T" => 12 + 'K' => 3, + 'M' => 6, + 'B' => 9, + 'T' => 12 } MULTIPLIER_SUFFIXES.default = 0 - MULTIPLIER_REGEXP = Regexp.new('\d(%s)\b[^\d]*$' % MULTIPLIER_SUFFIXES.keys.join('|'), 'i') + MULTIPLIER_REGEXP = Regexp.new(format('^(.*?\d)(%s)\b([^\d]*)$', MULTIPLIER_SUFFIXES.keys.join('|')), 'i') # Class methods class << self # @attr_accessor [true, false] assume_from_symbol Use this to enable the # ability to assume the currency from a passed symbol attr_accessor :assume_from_symbol + + # Monetize uses the delimiters set in the currency to separate integers from + # decimals, and to ignore thousands separators. In some corner cases, + # though, it will try to determine the correct separator by itself. Set this + # to true to enforce the delimiters set in the currency all the time. + attr_accessor :enforce_currency_delimiters end def self.parse(input, currency = Money.default_currency, options = {}) input = input.to_s.strip @@ -59,22 +64,22 @@ from_bigdecimal(value, currency) end def self.from_fixnum(value, currency = Money.default_currency) currency = Money::Currency.wrap(currency) - value = value * currency.subunit_to_unit + value *= currency.subunit_to_unit Money.new(value, currency) end def self.from_float(value, currency = Money.default_currency) value = BigDecimal.new(value.to_s) from_bigdecimal(value, currency) end def self.from_bigdecimal(value, currency = Money.default_currency) currency = Money::Currency.wrap(currency) - value = value * currency.subunit_to_unit + value *= currency.subunit_to_unit value = value.round unless Money.infinite_precision Money.new(value, currency) end def self.from_numeric(value, currency = Money.default_currency) @@ -83,103 +88,51 @@ from_fixnum(value, currency) when Numeric value = BigDecimal.new(value.to_s) from_bigdecimal(value, currency) else - raise ArgumentError, "'value' should be a type of Numeric" + fail ArgumentError, "'value' should be a type of Numeric" end end def self.extract_cents(input, currency = Money.default_currency) - multiplier_suffix = (matches = MULTIPLIER_REGEXP.match(input)) ? matches[1].upcase : nil - multiplier_exp = MULTIPLIER_SUFFIXES[multiplier_suffix] + multiplier_exp, input = extract_multiplier(input) num = input.gsub(/[^\d.,'-]/, '') - negative = num =~ /^-|-$/ ? true : false + negative, num = extract_sign(num) - decimal_char = currency.decimal_mark - - num = num.sub(/^-|-$/, '') if negative - - if num.include?('-') - raise ArgumentError, "Invalid currency amount (hyphen)" - end - num.chop! if num.match(/[\.|,]$/) - used_delimiters = num.scan(/[^\d]/) + major, minor = extract_major_minor(num, currency) - case used_delimiters.uniq.length - when 0 - major, minor = num, 0 - when 2 - thousands_separator, decimal_mark = used_delimiters.uniq + cents = major.to_i * currency.subunit_to_unit - major, minor = num.gsub(thousands_separator, '').split(decimal_mark) - min = 0 unless min - when 1 - decimal_mark = used_delimiters.first + cents, minor = apply_multiplier(multiplier_exp, cents, minor) - if decimal_char == decimal_mark - major, minor = num.split(decimal_char) - else - if num.scan(decimal_mark).length > 1 # multiple matches; treat as decimal_mark - major, minor = num.gsub(decimal_mark, ''), 0 - else - possible_major, possible_minor = num.split(decimal_mark) - possible_major ||= "0" - possible_minor ||= "00" + cents += set_minor_precision(minor, currency) - if possible_minor.length != 3 # thousands_separator - major, minor = possible_major, possible_minor - else - if possible_major.length > 3 - major, minor = possible_major, possible_minor - else - if decimal_mark == '.' - major, minor = possible_major, possible_minor - else - major, minor = "#{possible_major}#{possible_minor}", 0 - end - end - end - end - end - else - raise ArgumentError, "Invalid currency amount" - end + apply_sign(negative, cents) + end - cents = major.to_i * currency.subunit_to_unit + private - cents *= (10 ** multiplier_exp) + def self.apply_multiplier(multiplier_exp, cents, minor) + cents *= (10**multiplier_exp) minor = minor.to_s + ('0' * multiplier_exp) - shift = minor[0 ... multiplier_exp].to_i * 100 + shift = minor[0...multiplier_exp].to_i * 100 cents += shift - minor = (minor[multiplier_exp .. -1] || '') + minor = (minor[multiplier_exp..-1] || '') + [cents, minor] + end - minor = if minor.size < currency.decimal_places - (minor + ("0" * currency.decimal_places))[0,currency.decimal_places].to_i - elsif minor.size > currency.decimal_places - if minor[currency.decimal_places,1].to_i >= 5 - minor[0,currency.decimal_places].to_i+1 - else - minor[0,currency.decimal_places].to_i - end - else - minor.to_i - end - - cents += minor - + def self.apply_sign(negative, cents) negative ? cents * -1 : cents end - private - def self.contains_currency_symbol?(amount) - currency_symbol_regex === amount + amount =~ currency_symbol_regex end def self.compute_currency(amount) if contains_currency_symbol?(amount) matches = amount.match(currency_symbol_regex) @@ -187,13 +140,90 @@ else amount[/[A-Z]{2,3}/] end end + def self.extract_major_minor(num, currency) + used_delimiters = num.scan(/[^\d]/).uniq + + case used_delimiters.length + when 0 + [num, 0] + when 2 + 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) + else + fail ArgumentError, 'Invalid currency amount' + end + end + + def self.extract_major_minor_with_single_delimiter(num, currency, delimiter) + if delimiter == currency.decimal_mark + split_major_minor(num, delimiter) + elsif enforce_currency_delimiters and delimiter == currency.thousands_separator + [num.gsub(delimiter, ''), 0] + else + extract_major_minor_with_tentative_delimiter(num, delimiter) + end + end + + def self.extract_major_minor_with_tentative_delimiter(num, delimiter) + if num.scan(delimiter).length > 1 + # Multiple matches; treat as thousands separator + [num.gsub(delimiter, ''), '00'] + else + possible_major, possible_minor = split_major_minor(num, delimiter) + + if possible_minor.length != 3 or possible_major.length > 3 or delimiter == '.' + # Doesn't look like thousands separator + [possible_major, possible_minor] + else + ["#{possible_major}#{possible_minor}", '00'] + end + end + end + + def self.extract_multiplier(input) + if (matches = MULTIPLIER_REGEXP.match(input)) + multiplier_suffix = matches[2].upcase + [MULTIPLIER_SUFFIXES[multiplier_suffix], "#{$1}#{$3}"] + else + [0, input] + end + end + + def self.extract_sign(input) + result = (input =~ /^-+(.*)$/ or input =~ /^(.*)-+$/) ? [true, $1] : [false, input] + fail ArgumentError, 'Invalid currency amount (hyphen)' if result[1].include?('-') + result + end + def self.regex_safe_symbols - CURRENCY_SYMBOLS.keys.map { |key| - Regexp.escape(key) - }.join('|') + CURRENCY_SYMBOLS.keys.map { |key| Regexp.escape(key) }.join('|') + end + + def self.set_minor_precision(minor, currency) + if Money.infinite_precision + (BigDecimal.new(minor) / (10**minor.size)) * currency.subunit_to_unit + elsif minor.size < currency.decimal_places + (minor + ('0' * currency.decimal_places))[0, currency.decimal_places].to_i + elsif minor.size > currency.decimal_places + if minor[currency.decimal_places, 1].to_i >= 5 + minor[0, currency.decimal_places].to_i + 1 + else + minor[0, currency.decimal_places].to_i + end + else + minor.to_i + end + end + + def self.split_major_minor(num, delimiter) + major, minor = num.split(delimiter) + minor = '00' unless minor + [major, minor] end def self.currency_symbol_regex /\A[\+|\-]?(?<symbol>#{regex_safe_symbols})/ end