From 8ec7c519953d950ee296b6e3a4e52dc161a36420 Mon Sep 17 00:00:00 2001 From: antstorm Date: Wed, 14 Aug 2024 12:51:48 +0100 Subject: [PATCH 1/4] Rename current default parser to OptimisticParser --- lib/monetize.rb | 4 ++-- lib/monetize/{parser.rb => optimistic_parser.rb} | 4 ++-- spec/monetize_spec.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename lib/monetize/{parser.rb => optimistic_parser.rb} (97%) diff --git a/lib/monetize.rb b/lib/monetize.rb index dfedaba7d..5946048c8 100644 --- a/lib/monetize.rb +++ b/lib/monetize.rb @@ -4,7 +4,7 @@ require 'monetize/core_extensions' require 'monetize/errors' require 'monetize/version' -require 'monetize/parser' +require 'monetize/optimistic_parser' require 'monetize/collection' module Monetize @@ -36,7 +36,7 @@ def parse!(input, currency = Money.default_currency, options = {}) return input if input.is_a?(Money) return from_numeric(input, currency) if input.is_a?(Numeric) - parser = Monetize::Parser.new(input, currency, options) + parser = Monetize::OptimisticParser.new(input, currency, options) amount, currency = parser.parse Money.from_amount(amount, currency) diff --git a/lib/monetize/parser.rb b/lib/monetize/optimistic_parser.rb similarity index 97% rename from lib/monetize/parser.rb rename to lib/monetize/optimistic_parser.rb index 8d88c09a6..29b07a3b0 100644 --- a/lib/monetize/parser.rb +++ b/lib/monetize/optimistic_parser.rb @@ -1,7 +1,7 @@ # encoding: utf-8 module Monetize - class Parser + class OptimisticParser CURRENCY_SYMBOLS = { '$' => 'USD', '€' => 'EUR', @@ -76,7 +76,7 @@ def to_big_decimal(value) 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 = nil unless Monetize::OptimisticParser::CURRENCY_SYMBOLS.value?(computed_currency) computed_currency ||= compute_currency if assume_from_symbol? diff --git a/spec/monetize_spec.rb b/spec/monetize_spec.rb index 00aad8002..f41a07d1e 100644 --- a/spec/monetize_spec.rb +++ b/spec/monetize_spec.rb @@ -56,7 +56,7 @@ Monetize.assume_from_symbol = false end - Monetize::Parser::CURRENCY_SYMBOLS.each_pair do |symbol, iso_code| + Monetize::OptimisticParser::CURRENCY_SYMBOLS.each_pair do |symbol, iso_code| context iso_code do let(:currency) { Money::Currency.find(iso_code) } let(:amount) { 5_95 } From 20c1ce0485ded689308417fb32484bea9141090d Mon Sep 17 00:00:00 2001 From: antstorm Date: Wed, 14 Aug 2024 13:00:10 +0100 Subject: [PATCH 2/4] Extract abstract Monetize::Parser class --- lib/monetize/optimistic_parser.rb | 37 ++++--------------------------- lib/monetize/parser.rb | 37 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 lib/monetize/parser.rb diff --git a/lib/monetize/optimistic_parser.rb b/lib/monetize/optimistic_parser.rb index 29b07a3b0..bceb4e99b 100644 --- a/lib/monetize/optimistic_parser.rb +++ b/lib/monetize/optimistic_parser.rb @@ -1,38 +1,9 @@ # encoding: utf-8 +require 'monetize/parser' + module Monetize - class OptimisticParser - CURRENCY_SYMBOLS = { - '$' => 'USD', - '€' => 'EUR', - '£' => 'GBP', - '₤' => 'GBP', - 'R$' => 'BRL', - 'RM' => 'MYR', - 'Rp' => 'IDR', - 'R' => 'ZAR', - '¥' => 'JPY', - 'C$' => 'CAD', - '₼' => 'AZN', - '元' => 'CNY', - 'Kč' => 'CZK', - 'Ft' => 'HUF', - '₹' => 'INR', - '₽' => 'RUB', - '₺' => 'TRY', - '₴' => 'UAH', - 'Fr' => 'CHF', - 'zł' => 'PLN', - '₸' => 'KZT', - "₩" => 'KRW', - 'S$' => 'SGD', - 'HK$'=> 'HKD', - 'NT$'=> 'TWD', - '₱' => 'PHP', - } - - MULTIPLIER_SUFFIXES = { 'K' => 3, 'M' => 6, 'B' => 9, 'T' => 12 } - MULTIPLIER_SUFFIXES.default = 0 + class OptimisticParser < Parser MULTIPLIER_REGEXP = Regexp.new(format('^(.*?\d)(%s)\b([^\d]*)$', MULTIPLIER_SUFFIXES.keys.join('|')), 'i') DEFAULT_DECIMAL_MARK = '.'.freeze @@ -76,7 +47,7 @@ def to_big_decimal(value) def parse_currency computed_currency = nil computed_currency = input[/[A-Z]{2,3}/] - computed_currency = nil unless Monetize::OptimisticParser::CURRENCY_SYMBOLS.value?(computed_currency) + computed_currency = nil unless CURRENCY_SYMBOLS.value?(computed_currency) computed_currency ||= compute_currency if assume_from_symbol? diff --git a/lib/monetize/parser.rb b/lib/monetize/parser.rb new file mode 100644 index 000000000..85b5d7140 --- /dev/null +++ b/lib/monetize/parser.rb @@ -0,0 +1,37 @@ +# encoding: utf-8 + +module Monetize + class Parser + CURRENCY_SYMBOLS = { + '$' => 'USD', + '€' => 'EUR', + '£' => 'GBP', + '₤' => 'GBP', + 'R$' => 'BRL', + 'RM' => 'MYR', + 'Rp' => 'IDR', + 'R' => 'ZAR', + '¥' => 'JPY', + 'C$' => 'CAD', + '₼' => 'AZN', + '元' => 'CNY', + 'Kč' => 'CZK', + 'Ft' => 'HUF', + '₹' => 'INR', + '₽' => 'RUB', + '₺' => 'TRY', + '₴' => 'UAH', + 'Fr' => 'CHF', + 'zł' => 'PLN', + '₸' => 'KZT', + "₩" => 'KRW', + 'S$' => 'SGD', + 'HK$'=> 'HKD', + 'NT$'=> 'TWD', + '₱' => 'PHP', + } + + MULTIPLIER_SUFFIXES = { 'K' => 3, 'M' => 6, 'B' => 9, 'T' => 12 } + MULTIPLIER_SUFFIXES.default = 0 + end +end From 9d9bff61d728e8a73a136c2f770bc1a61ab9eb61 Mon Sep 17 00:00:00 2001 From: antstorm Date: Wed, 14 Aug 2024 15:58:26 +0100 Subject: [PATCH 3/4] Specify Monetize::Parser interface --- lib/monetize/optimistic_parser.rb | 6 ++++-- lib/monetize/parser.rb | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/monetize/optimistic_parser.rb b/lib/monetize/optimistic_parser.rb index bceb4e99b..f64cb79f5 100644 --- a/lib/monetize/optimistic_parser.rb +++ b/lib/monetize/optimistic_parser.rb @@ -36,14 +36,16 @@ def parse private + private + + attr_reader :input, :fallback_currency, :options + def to_big_decimal(value) BigDecimal(value) rescue ::ArgumentError => err fail ParseError, err.message end - attr_reader :input, :fallback_currency, :options - def parse_currency computed_currency = nil computed_currency = input[/[A-Z]{2,3}/] diff --git a/lib/monetize/parser.rb b/lib/monetize/parser.rb index 85b5d7140..d961fffbc 100644 --- a/lib/monetize/parser.rb +++ b/lib/monetize/parser.rb @@ -33,5 +33,13 @@ class Parser MULTIPLIER_SUFFIXES = { 'K' => 3, 'M' => 6, 'B' => 9, 'T' => 12 } MULTIPLIER_SUFFIXES.default = 0 + + def initialize(input, fallback_currency = Money.default_currency, options = {}) + raise NotImplementedError, 'Monetize::Parser subclasses must implement #initialize' + end + + def parse + raise NotImplementedError, 'Monetize::Parser subclasses must implement #parse' + end end end From 1b9e607612e8f68343f8a96aac790a41ae6cea9b Mon Sep 17 00:00:00 2001 From: antstorm Date: Wed, 14 Aug 2024 16:55:41 +0100 Subject: [PATCH 4/4] Support registering multiple parsers --- lib/monetize.rb | 29 ++++++++++++++- lib/monetize/optimistic_parser.rb | 2 +- lib/monetize/parser.rb | 2 +- spec/monetize_spec.rb | 60 +++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/monetize.rb b/lib/monetize.rb index 5946048c8..28dfbfa2f 100644 --- a/lib/monetize.rb +++ b/lib/monetize.rb @@ -26,6 +26,10 @@ class << self # human text that we're dealing with fractions of cents. attr_accessor :expect_whole_subunits + # Specify which of the previously registered parsers should be used when parsing an input + # unless overriden using the :parser keyword option for the .parse and parse! methods. + attr_accessor :default_parser + def parse(input, currency = Money.default_currency, options = {}) parse! input, currency, options rescue Error @@ -36,7 +40,7 @@ def parse!(input, currency = Money.default_currency, options = {}) return input if input.is_a?(Money) return from_numeric(input, currency) if input.is_a?(Numeric) - parser = Monetize::OptimisticParser.new(input, currency, options) + parser = fetch_parser(input, currency, options) amount, currency = parser.parse Money.from_amount(amount, currency) @@ -77,5 +81,28 @@ def extract_cents(input, currency = Money.default_currency) money = parse(input, currency) money.cents if money end + + # Registers a new parser class along with the default options. It can then be used by + # providing a :parser option when parsing an input or by specifying a default parser + # using Monetize.default_parser=. + def register_parser(name, klass, options = {}) + @parsers ||= {} + @parsers[name] = [klass, options] + end + + private + + attr_reader :parsers + + def fetch_parser(input, currency, options) + parser_name = options[:parser] || default_parser + parser_klass, parser_options = parsers.fetch(parser_name) do + raise ArgumentError, "Parser not registered: #{parser_name}" + end + parser_klass.new(input, currency, parser_options.merge(options)) + end end end + +Monetize.register_parser(:optimistic, Monetize::OptimisticParser) +Monetize.default_parser = :optimistic diff --git a/lib/monetize/optimistic_parser.rb b/lib/monetize/optimistic_parser.rb index f64cb79f5..dd1c41e16 100644 --- a/lib/monetize/optimistic_parser.rb +++ b/lib/monetize/optimistic_parser.rb @@ -8,7 +8,7 @@ class OptimisticParser < Parser DEFAULT_DECIMAL_MARK = '.'.freeze - def initialize(input, fallback_currency = Money.default_currency, options = {}) + def initialize(input, fallback_currency, options) @input = input.to_s.strip @fallback_currency = fallback_currency @options = options diff --git a/lib/monetize/parser.rb b/lib/monetize/parser.rb index d961fffbc..c002d0b63 100644 --- a/lib/monetize/parser.rb +++ b/lib/monetize/parser.rb @@ -34,7 +34,7 @@ class Parser MULTIPLIER_SUFFIXES = { 'K' => 3, 'M' => 6, 'B' => 9, 'T' => 12 } MULTIPLIER_SUFFIXES.default = 0 - def initialize(input, fallback_currency = Money.default_currency, options = {}) + def initialize(input, fallback_currency, options) raise NotImplementedError, 'Monetize::Parser subclasses must implement #initialize' end diff --git a/spec/monetize_spec.rb b/spec/monetize_spec.rb index f41a07d1e..14af6b230 100644 --- a/spec/monetize_spec.rb +++ b/spec/monetize_spec.rb @@ -36,6 +36,17 @@ } JSON + # Dummy parser that always returns an amount and currency specified via options + class TestParser < Monetize::Parser + def initialize(input, currency, options) + @options = options + end + + def parse + [@options[:amount], @options[:currency]] + end + end + describe '.parse' do it 'parses european-formatted inputs under 10EUR' do expect(Monetize.parse('EUR 5,95')).to eq Money.new(595, 'EUR') @@ -390,6 +401,12 @@ expect(Monetize.parse('£10.00')).to eq Money.new(10_00, 'GBP') end end + + context 'when specified parser does not exist' do + it 'returns nil' do + expect(Monetize.parse('100 USD', nil, parser: :foo)).to eq(nil) + end + end end describe '.parse!' do @@ -406,6 +423,14 @@ it 'raises ArgumentError with invalid format' do expect { Monetize.parse!('11..0') }.to raise_error Monetize::ParseError end + + context 'when specified parser does not exist' do + it 'raises ArgumentError' do + expect do + Monetize.parse!('100 USD', nil, parser: :foo) + end.to raise_error(Monetize::ArgumentError, 'Parser not registered: foo') + end + end end describe '.parse_collection' do @@ -630,4 +655,39 @@ expect(4.635.to_money).to eq '4.635'.to_money end end + + describe '.register_parser' do + it 'registers a new parser with a provided name' do + Monetize.register_parser(:test, TestParser, amount: 42, currency: 'GBP') + + expect(Monetize.parse!('test', nil, parser: :test)).to eq(Money.new(42_00, 'GBP')) + end + + it 'registers the same parser with a different name' do + Monetize.register_parser(:test_1, TestParser, amount: 1, currency: 'GBP') + Monetize.register_parser(:test_2, TestParser, amount: 2, currency: 'USD') + + expect(Monetize.parse!('test', nil, parser: :test_1)).to eq(Money.new(1_00, 'GBP')) + expect(Monetize.parse!('test', nil, parser: :test_2)).to eq(Money.new(2_00, 'USD')) + end + + it 'overrides existing parser with the same name' do + Monetize.register_parser(:test, TestParser, amount: 42, currency: 'GBP') + Monetize.register_parser(:test, TestParser, amount: 99, currency: 'USD') + + expect(Monetize.parse!('test', nil, parser: :test)).to eq(Money.new(99_00, 'USD')) + end + end + + describe '.default_parser=' do + before { Monetize.register_parser(:test, TestParser, amount: 1, currency: 'USD') } + after { Monetize.default_parser = :optimistic } + + it 'specifies which parser to use by default' do + expect(Monetize.parse!('99 GBP')).to eq(Money.new(99_00, 'GBP')) + + Monetize.default_parser = :test + expect(Monetize.parse!('99 GBP')).to eq(Money.new(1_00, 'USD')) + end + end end