shopify-money 1.0.1.pre → 1.0.2.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/UPGRADING.md +2 -0
- data/lib/money/config.rb +1 -2
- data/lib/money/core_extensions.rb +1 -1
- data/lib/money/money.rb +1 -5
- data/lib/money/parser/accounting.rb +11 -0
- data/lib/money/parser/fuzzy.rb +174 -0
- data/lib/money/parser/locale_aware.rb +52 -0
- data/lib/money/parser/simple.rb +30 -0
- data/lib/money/railtie.rb +7 -0
- data/lib/money/version.rb +1 -1
- data/lib/money.rb +4 -2
- data/spec/config_spec.rb +0 -12
- data/spec/money_spec.rb +0 -16
- data/spec/{accounting_money_parser_spec.rb → parser/accounting_spec.rb} +5 -13
- data/spec/{money_parser_spec.rb → parser/fuzzy_spec.rb} +6 -6
- data/spec/parser/locale_aware_spec.rb +208 -0
- data/spec/parser/simple_spec.rb +62 -0
- data/spec/spec_helper.rb +2 -2
- metadata +14 -8
- data/lib/money/accounting_money_parser.rb +0 -8
- data/lib/money/money_parser.rb +0 -162
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23562c524ba9f0d8219d01779505a75309d8825ed8f8839f1efdd71fbf5a1e89
|
4
|
+
data.tar.gz: ca1b6ea90e7a556970d135bbebc74c9cf6eb417cd475607ef58923751e2969b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81f2eb5e04d1cf082cf3eaadd5d609d4abcdef1841d1dde9b2eee68c0b8e570b7c0c72dd0a2a05c7213b0c611a267a2b6fbca044d2a86711765aa91f8e2ea763
|
7
|
+
data.tar.gz: cf7864860eb1fab370902861200b23150d08b1d135f614896a9c32fe20ea9e1edc7070563325c4a102a4af64c986c308892e13aefe9ad2590e4ac75d17f30a22
|
data/UPGRADING.md
CHANGED
data/lib/money/config.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
class Money
|
4
4
|
class Config
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :default_currency, :legacy_json_format, :legacy_deprecations
|
6
6
|
|
7
7
|
def legacy_default_currency!
|
8
8
|
@default_currency ||= Money::NULL_CURRENCY
|
@@ -17,7 +17,6 @@ class Money
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def initialize
|
20
|
-
@parser = MoneyParser
|
21
20
|
@default_currency = nil
|
22
21
|
@legacy_json_format = false
|
23
22
|
@legacy_deprecations = false
|
data/lib/money/money.rb
CHANGED
@@ -13,7 +13,7 @@ class Money
|
|
13
13
|
class << self
|
14
14
|
extend Forwardable
|
15
15
|
attr_accessor :config
|
16
|
-
def_delegators :@config, :
|
16
|
+
def_delegators :@config, :default_currency, :default_currency=
|
17
17
|
|
18
18
|
def configure
|
19
19
|
self.config ||= Config.new
|
@@ -33,10 +33,6 @@ class Money
|
|
33
33
|
end
|
34
34
|
alias_method :from_amount, :new
|
35
35
|
|
36
|
-
def parse(*args, **kwargs)
|
37
|
-
parser.parse(*args, **kwargs)
|
38
|
-
end
|
39
|
-
|
40
36
|
def from_subunits(subunits, currency_iso, format: :iso4217)
|
41
37
|
currency = Helpers.value_to_currency(currency_iso)
|
42
38
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Money
|
3
|
+
module Parser
|
4
|
+
class Accounting < Fuzzy
|
5
|
+
def parse(input, currency = nil, **options)
|
6
|
+
# set () to mean negativity. ignore $
|
7
|
+
super(input.gsub(/\(\$?(.*?)\)/, '-\1'), currency, **options)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Money
|
3
|
+
module Parser
|
4
|
+
class Fuzzy
|
5
|
+
class MoneyFormatError < ArgumentError; end
|
6
|
+
|
7
|
+
MARKS = %w[. , · ’ ˙ '] + [' ']
|
8
|
+
|
9
|
+
ESCAPED_MARKS = Regexp.escape(MARKS.join)
|
10
|
+
ESCAPED_NON_SPACE_MARKS = Regexp.escape((MARKS - [' ']).join)
|
11
|
+
ESCAPED_NON_DOT_MARKS = Regexp.escape((MARKS - ['.']).join)
|
12
|
+
ESCAPED_NON_COMMA_MARKS = Regexp.escape((MARKS - [',']).join)
|
13
|
+
|
14
|
+
NUMERIC_REGEX = /(
|
15
|
+
[\+\-]?
|
16
|
+
[\d#{ESCAPED_NON_SPACE_MARKS}][\d#{ESCAPED_MARKS}]*
|
17
|
+
)/ix
|
18
|
+
|
19
|
+
# 1,234,567.89
|
20
|
+
DOT_DECIMAL_REGEX = /\A
|
21
|
+
[\+\-]?
|
22
|
+
(?:
|
23
|
+
(?:\d+)
|
24
|
+
(?:[#{ESCAPED_NON_DOT_MARKS}]\d{3})+
|
25
|
+
(?:\.\d{2,})?
|
26
|
+
)
|
27
|
+
\z/ix
|
28
|
+
|
29
|
+
# 1.234.567,89
|
30
|
+
COMMA_DECIMAL_REGEX = /\A
|
31
|
+
[\+\-]?
|
32
|
+
(?:
|
33
|
+
(?:\d+)
|
34
|
+
(?:[#{ESCAPED_NON_COMMA_MARKS}]\d{3})+
|
35
|
+
(?:\,\d{2,})?
|
36
|
+
)
|
37
|
+
\z/ix
|
38
|
+
|
39
|
+
# 12,34,567.89
|
40
|
+
INDIAN_NUMERIC_REGEX = /\A
|
41
|
+
[\+\-]?
|
42
|
+
(?:
|
43
|
+
(?:\d+)
|
44
|
+
(?:\,\d{2})+
|
45
|
+
(?:\,\d{3})
|
46
|
+
(?:\.\d{2})?
|
47
|
+
)
|
48
|
+
\z/ix
|
49
|
+
|
50
|
+
# 1,1123,4567.89
|
51
|
+
CHINESE_NUMERIC_REGEX = /\A
|
52
|
+
[\+\-]?
|
53
|
+
(?:
|
54
|
+
(?:\d+)
|
55
|
+
(?:\,\d{4})+
|
56
|
+
(?:\.\d{2})?
|
57
|
+
)
|
58
|
+
\z/ix
|
59
|
+
|
60
|
+
def self.parse(input, currency = nil, **options)
|
61
|
+
new.parse(input, currency, **options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Parses an input string and attempts to find the decimal separator based on certain heuristics, like the amount
|
65
|
+
# decimals for the fractional part a currency has or the incorrect notion a currency has a defined decimal
|
66
|
+
# separator (this is a property of the locale). While these heuristics can lead to the expected result for some
|
67
|
+
# cases, the other cases can lead to surprising results such as parsed amounts being 1000x larger than intended.
|
68
|
+
# @deprecated Use {LocaleAware.parse} or {Simple.parse} instead.
|
69
|
+
# @param input [String]
|
70
|
+
# @param currency [String, Money::Currency, nil]
|
71
|
+
# @param strict [Boolean]
|
72
|
+
# @return [Money]
|
73
|
+
# @raise [MoneyFormatError]
|
74
|
+
def parse(input, currency = nil, strict: false)
|
75
|
+
currency = Money::Helpers.value_to_currency(currency)
|
76
|
+
amount = extract_amount_from_string(input, currency, strict)
|
77
|
+
Money.new(amount, currency)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def extract_amount_from_string(input, currency, strict)
|
83
|
+
unless input.is_a?(String)
|
84
|
+
return input
|
85
|
+
end
|
86
|
+
|
87
|
+
if input.strip.empty?
|
88
|
+
return '0'
|
89
|
+
end
|
90
|
+
|
91
|
+
number = input.scan(NUMERIC_REGEX).flatten.first
|
92
|
+
number = number.to_s.strip
|
93
|
+
|
94
|
+
if number.empty?
|
95
|
+
if Money.config.legacy_deprecations && !strict
|
96
|
+
Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
|
97
|
+
return '0'
|
98
|
+
else
|
99
|
+
raise MoneyFormatError, "invalid money string: #{input}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
marks = number.scan(/[#{ESCAPED_MARKS}]/).flatten
|
104
|
+
if marks.empty?
|
105
|
+
return number
|
106
|
+
end
|
107
|
+
|
108
|
+
if marks.size == 1
|
109
|
+
return normalize_number(number, marks, currency)
|
110
|
+
end
|
111
|
+
|
112
|
+
# remove end of string mark
|
113
|
+
number.sub!(/[#{ESCAPED_MARKS}]\z/, '')
|
114
|
+
|
115
|
+
if amount = number[DOT_DECIMAL_REGEX] || number[INDIAN_NUMERIC_REGEX] || number[CHINESE_NUMERIC_REGEX]
|
116
|
+
return amount.tr(ESCAPED_NON_DOT_MARKS, '')
|
117
|
+
end
|
118
|
+
|
119
|
+
if amount = number[COMMA_DECIMAL_REGEX]
|
120
|
+
return amount.tr(ESCAPED_NON_COMMA_MARKS, '').sub(',', '.')
|
121
|
+
end
|
122
|
+
|
123
|
+
if Money.config.legacy_deprecations && !strict
|
124
|
+
Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
|
125
|
+
else
|
126
|
+
raise MoneyFormatError, "invalid money string: #{input}"
|
127
|
+
end
|
128
|
+
|
129
|
+
normalize_number(number, marks, currency)
|
130
|
+
end
|
131
|
+
|
132
|
+
def normalize_number(number, marks, currency)
|
133
|
+
digits = number.rpartition(marks.last)
|
134
|
+
digits.first.tr!(ESCAPED_MARKS, '')
|
135
|
+
|
136
|
+
if last_digits_decimals?(digits, marks, currency)
|
137
|
+
"#{digits.first}.#{digits.last}"
|
138
|
+
else
|
139
|
+
"#{digits.first}#{digits.last}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def last_digits_decimals?(digits, marks, currency)
|
144
|
+
# Grouping marks are always different from decimal marks
|
145
|
+
# Example: 1,234,456
|
146
|
+
*other_marks, last_mark = marks
|
147
|
+
other_marks.uniq!
|
148
|
+
if other_marks.size == 1
|
149
|
+
return other_marks.first != last_mark
|
150
|
+
end
|
151
|
+
|
152
|
+
# Thousands always have more than 2 digits
|
153
|
+
# Example: 1,23 must be 1 dollar and 23 cents
|
154
|
+
if digits.last.size < 3
|
155
|
+
return !digits.last.empty?
|
156
|
+
end
|
157
|
+
|
158
|
+
# 0 before the final mark indicates last digits are decimals
|
159
|
+
# Example: 0,23
|
160
|
+
if digits.first.to_i.zero?
|
161
|
+
return true
|
162
|
+
end
|
163
|
+
|
164
|
+
# legacy support for 1.000 USD
|
165
|
+
if digits.last.size == 3 && digits.first.size <= 3 && currency.minor_units < 3
|
166
|
+
return false
|
167
|
+
end
|
168
|
+
|
169
|
+
# The last mark matches the one used by the provided currency to delimiter decimals
|
170
|
+
currency.decimal_mark == last_mark
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Money
|
3
|
+
module Parser
|
4
|
+
class LocaleAware
|
5
|
+
@decimal_separator_resolver = nil
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# The +Proc+ called to get the current locale decimal separator. In Rails apps this defaults to the same lookup
|
9
|
+
# ActionView's +number_to_currency+ helper will use to format the monetary amount for display.
|
10
|
+
def decimal_separator_resolver
|
11
|
+
@decimal_separator_resolver
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set the default +Proc+ to determine the current locale decimal separator.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# Money::Parser::LocaleAware.decimal_separator_resolver =
|
18
|
+
# ->() { MyFormattingLibrary.current_locale.decimal.separator }
|
19
|
+
def decimal_separator_resolver=(proc)
|
20
|
+
@decimal_separator_resolver = proc
|
21
|
+
end
|
22
|
+
|
23
|
+
# Parses an input string, normalizing some non-ASCII characters to their equivalent ASCII, then discarding any
|
24
|
+
# character that is not a digit, hyphen-minus or the decimal separator. To prevent user confusion, make sure
|
25
|
+
# that formatted Money strings can be parsed back into equivalent Money objects.
|
26
|
+
#
|
27
|
+
# @param input [String]
|
28
|
+
# @param currency [String, Money::Currency]
|
29
|
+
# @param strict [Boolean]
|
30
|
+
# @param decimal_separator [String]
|
31
|
+
# @return [Money, nil]
|
32
|
+
def parse(input, currency, strict: false, decimal_separator: decimal_separator_resolver&.call)
|
33
|
+
raise ArgumentError, "decimal separator cannot be nil" unless decimal_separator
|
34
|
+
|
35
|
+
currency = Money::Helpers.value_to_currency(currency)
|
36
|
+
return unless currency
|
37
|
+
|
38
|
+
normalized_input = input
|
39
|
+
.tr('-0-9.,、、', '-0-9.,,,')
|
40
|
+
.gsub(/[^\d\-#{Regexp.escape(decimal_separator)}]/, '')
|
41
|
+
.gsub(decimal_separator, '.')
|
42
|
+
amount = BigDecimal(normalized_input, exception: false)
|
43
|
+
if amount
|
44
|
+
Money.new(amount, currency)
|
45
|
+
elsif strict
|
46
|
+
raise ArgumentError, "unable to parse input=\"#{input}\" currency=\"#{currency}\""
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Money
|
3
|
+
module Parser
|
4
|
+
class Simple
|
5
|
+
SIGNED_DECIMAL_MATCHER = /\A-?\d*(?:\.\d*)?\z/.freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# Parses an input string using BigDecimal, it always expects a dot character as a decimal separator and
|
9
|
+
# generally does not accept other characters other than minus-hyphen and digits. It is useful for APIs, interop
|
10
|
+
# with other languages and other use cases where you expect well-formatted input and do not need to take user
|
11
|
+
# locale into consideration.
|
12
|
+
# @param input [String]
|
13
|
+
# @param currency [String, Money::Currency]
|
14
|
+
# @param strict [Boolean]
|
15
|
+
# @return [Money, nil]
|
16
|
+
def parse(input, currency, strict: false)
|
17
|
+
currency = Money::Helpers.value_to_currency(currency)
|
18
|
+
return unless currency
|
19
|
+
|
20
|
+
coerced = input.to_s
|
21
|
+
if SIGNED_DECIMAL_MATCHER.match?(coerced) && (amount = BigDecimal(coerced, exception: false))
|
22
|
+
Money.new(amount, currency)
|
23
|
+
elsif strict
|
24
|
+
raise ArgumentError, "unable to parse input=\"#{input}\" currency=\"#{currency}\""
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/money/railtie.rb
CHANGED
@@ -8,5 +8,12 @@ class Money
|
|
8
8
|
ActiveJob::Serializers.add_serializers ::Money::Rails::JobArgumentSerializer
|
9
9
|
end
|
10
10
|
end
|
11
|
+
|
12
|
+
initializer "shopify-money.setup_locale_aware_parser" do
|
13
|
+
ActiveSupport.on_load(:action_view) do
|
14
|
+
Money::Parser::LocaleAware.decimal_separator_resolver =
|
15
|
+
-> { ::I18n.translate("number.currency.format.separator") }
|
16
|
+
end
|
17
|
+
end
|
11
18
|
end
|
12
19
|
end
|
data/lib/money/version.rb
CHANGED
data/lib/money.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require_relative 'money/version'
|
3
|
-
require_relative 'money/
|
3
|
+
require_relative 'money/parser/fuzzy'
|
4
4
|
require_relative 'money/helpers'
|
5
5
|
require_relative 'money/currency'
|
6
6
|
require_relative 'money/null_currency'
|
@@ -9,7 +9,9 @@ require_relative 'money/config'
|
|
9
9
|
require_relative 'money/money'
|
10
10
|
require_relative 'money/errors'
|
11
11
|
require_relative 'money/deprecations'
|
12
|
-
require_relative 'money/
|
12
|
+
require_relative 'money/parser/accounting'
|
13
|
+
require_relative 'money/parser/locale_aware'
|
14
|
+
require_relative 'money/parser/simple'
|
13
15
|
require_relative 'money/core_extensions'
|
14
16
|
require_relative 'money_column' if defined?(ActiveRecord)
|
15
17
|
require_relative 'money/railtie' if defined?(Rails::Railtie)
|
data/spec/config_spec.rb
CHANGED
@@ -32,18 +32,6 @@ RSpec.describe "Money::Config" do
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
describe 'parser' do
|
36
|
-
it 'defaults to MoneyParser' do
|
37
|
-
expect(Money::Config.new.parser).to eq(MoneyParser)
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'can be set to a new parser' do
|
41
|
-
configure(parser: AccountingMoneyParser) do
|
42
|
-
expect(Money.config.parser).to eq(AccountingMoneyParser)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
35
|
describe 'default_currency' do
|
48
36
|
it 'defaults to nil' do
|
49
37
|
configure do
|
data/spec/money_spec.rb
CHANGED
@@ -794,22 +794,6 @@ RSpec.describe "Money" do
|
|
794
794
|
end
|
795
795
|
end
|
796
796
|
|
797
|
-
describe "parser dependency injection" do
|
798
|
-
around(:each) { |test| configure(parser: AccountingMoneyParser, default_currency: 'CAD') { test.run }}
|
799
|
-
|
800
|
-
it "keeps AccountingMoneyParser class on new money objects" do
|
801
|
-
expect(Money.new.class.parser).to eq(AccountingMoneyParser)
|
802
|
-
end
|
803
|
-
|
804
|
-
it "supports parenthesis from AccountingMoneyParser" do
|
805
|
-
expect(Money.parse("($5.00)")).to eq(Money.new(-5))
|
806
|
-
end
|
807
|
-
|
808
|
-
it "supports parenthesis from AccountingMoneyParser for .to_money" do
|
809
|
-
expect("($5.00)".to_money).to eq(Money.new(-5))
|
810
|
-
end
|
811
|
-
end
|
812
|
-
|
813
797
|
describe "round" do
|
814
798
|
|
815
799
|
it "rounds to 0 decimal places by default" do
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'spec_helper'
|
3
3
|
|
4
|
-
RSpec.describe
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
4
|
+
RSpec.describe Money::Parser::Accounting do
|
5
|
+
before(:each) do
|
6
|
+
@parser = described_class
|
7
|
+
end
|
9
8
|
|
9
|
+
describe "parsing of amounts with period decimal separator" do
|
10
10
|
it "parses parenthesis as a negative amount eg (99.00)" do
|
11
11
|
expect(@parser.parse("(99.00)")).to eq(Money.new(-99.00))
|
12
12
|
end
|
@@ -104,10 +104,6 @@ RSpec.describe AccountingMoneyParser do
|
|
104
104
|
end
|
105
105
|
|
106
106
|
describe "parsing of amounts with comma decimal separator" do
|
107
|
-
before(:each) do
|
108
|
-
@parser = AccountingMoneyParser.new
|
109
|
-
end
|
110
|
-
|
111
107
|
it "parses dollar amount $1,00 with leading $" do
|
112
108
|
expect(@parser.parse("$1,00")).to eq(Money.new(1.00))
|
113
109
|
end
|
@@ -156,10 +152,6 @@ RSpec.describe AccountingMoneyParser do
|
|
156
152
|
end
|
157
153
|
|
158
154
|
describe "parsing of decimal cents amounts from 0 to 10" do
|
159
|
-
before(:each) do
|
160
|
-
@parser = AccountingMoneyParser.new
|
161
|
-
end
|
162
|
-
|
163
155
|
it "parses 50.0" do
|
164
156
|
expect(@parser.parse("50.0")).to eq(Money.new(50.00))
|
165
157
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'spec_helper'
|
3
3
|
|
4
|
-
RSpec.describe
|
4
|
+
RSpec.describe Money::Parser::Fuzzy do
|
5
5
|
before(:each) do
|
6
|
-
@parser =
|
6
|
+
@parser = described_class
|
7
7
|
end
|
8
8
|
|
9
9
|
describe "parsing of amounts with period decimal separator" do
|
@@ -20,8 +20,8 @@ RSpec.describe MoneyParser do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "parses raise with an invalid string and strict option" do
|
23
|
-
expect { @parser.parse("no money", strict: true) }.to raise_error(
|
24
|
-
expect { @parser.parse("1..1", strict: true) }.to raise_error(
|
23
|
+
expect { @parser.parse("no money", strict: true) }.to raise_error(described_class::MoneyFormatError)
|
24
|
+
expect { @parser.parse("1..1", strict: true) }.to raise_error(described_class::MoneyFormatError)
|
25
25
|
end
|
26
26
|
|
27
27
|
it "parses raise with an invalid when a currency is missing" do
|
@@ -159,7 +159,7 @@ RSpec.describe MoneyParser do
|
|
159
159
|
end
|
160
160
|
|
161
161
|
it "parses raises with multiple inconsistent thousands delimiters and strict option" do
|
162
|
-
expect { @parser.parse("1.1.11.111", strict: true) }.to raise_error(
|
162
|
+
expect { @parser.parse("1.1.11.111", strict: true) }.to raise_error(described_class::MoneyFormatError)
|
163
163
|
end
|
164
164
|
end
|
165
165
|
|
@@ -233,7 +233,7 @@ RSpec.describe MoneyParser do
|
|
233
233
|
end
|
234
234
|
|
235
235
|
it "parses raises with multiple inconsistent thousands delimiters and strict option" do
|
236
|
-
expect { @parser.parse("1,1,11,111", strict: true) }.to raise_error(
|
236
|
+
expect { @parser.parse("1,1,11,111", strict: true) }.to raise_error(described_class::MoneyFormatError)
|
237
237
|
end
|
238
238
|
end
|
239
239
|
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe Money::Parser::LocaleAware do
|
5
|
+
context "parsing amounts with period decimal separator" do
|
6
|
+
around(:example) do |example|
|
7
|
+
with_decimal_separator(".") { example.run }
|
8
|
+
end
|
9
|
+
|
10
|
+
it "parses an empty string to nil" do
|
11
|
+
expect(described_class.parse("", "CAD")).to be_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "parses an invalid string when not strict" do
|
15
|
+
expect(described_class.parse("no money", "CAD")).to be_nil
|
16
|
+
expect(described_class.parse("1..", "CAD")).to be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "raises with an invalid string and strict option" do
|
20
|
+
expect { described_class.parse("no money", "CAD", strict: true) }.to raise_error(ArgumentError)
|
21
|
+
expect { described_class.parse("1..1", "CAD", strict: true) }.to raise_error(ArgumentError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "parses an integer string amount" do
|
25
|
+
expect(described_class.parse("1", "CAD")).to eq(Money.new(1.00, "CAD"))
|
26
|
+
expect(described_class.parse("-1", "CAD")).to eq(Money.new(-1.00, "CAD"))
|
27
|
+
end
|
28
|
+
|
29
|
+
it "parses a float string amount" do
|
30
|
+
expect(described_class.parse("1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
31
|
+
expect(described_class.parse("-1.37", "CAD")).to eq(Money.new(-1.37, "CAD"))
|
32
|
+
end
|
33
|
+
|
34
|
+
it "parses a float string with 3 decimals" do
|
35
|
+
expect(described_class.parse("1.378", "JOD")).to eq(Money.new(1.378, "JOD"))
|
36
|
+
expect(described_class.parse("-1.378", "JOD")).to eq(Money.new(-1.378, "JOD"))
|
37
|
+
expect(described_class.parse("123.456", "USD")).to eq(Money.new(123.46, "USD"))
|
38
|
+
end
|
39
|
+
|
40
|
+
it "parses a float string amount with a leading random character" do
|
41
|
+
expect(described_class.parse("$1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
42
|
+
expect(described_class.parse(",1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
43
|
+
expect(described_class.parse("€1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
44
|
+
end
|
45
|
+
|
46
|
+
it "parses a float string amount with a trailing random character" do
|
47
|
+
expect(described_class.parse("1.37$", "CAD")).to eq(Money.new(1.37, "CAD"))
|
48
|
+
expect(described_class.parse("1.37,", "CAD")).to eq(Money.new(1.37, "CAD"))
|
49
|
+
expect(described_class.parse("1.37€", "CAD")).to eq(Money.new(1.37, "CAD"))
|
50
|
+
end
|
51
|
+
|
52
|
+
it "parses an amount with one or more thousands separators" do
|
53
|
+
expect(described_class.parse("100,000", "CAD")).to eq(Money.new(100_000.00, "CAD"))
|
54
|
+
expect(described_class.parse("-100,000", "CAD")).to eq(Money.new(-100_000.00, "CAD"))
|
55
|
+
expect(described_class.parse("100,000.01", "CAD")).to eq(Money.new(100_000.01, "CAD"))
|
56
|
+
expect(described_class.parse("1,00,000.01", "CAD")).to eq(Money.new(100_000.01, "CAD"))
|
57
|
+
expect(described_class.parse("1,00,000.001", "JOD")).to eq(Money.new(100_000.001, "JOD"))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "parsing amounts with comma decimal separator" do
|
62
|
+
around(:example) do |example|
|
63
|
+
with_decimal_separator(",") { example.run }
|
64
|
+
end
|
65
|
+
|
66
|
+
it "parses an empty string to nil" do
|
67
|
+
expect(described_class.parse("", "CAD")).to be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
it "parses an invalid string when not strict" do
|
71
|
+
expect(described_class.parse("no money", "CAD")).to be_nil
|
72
|
+
expect(described_class.parse("1,,", "CAD")).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "raises with an invalid string and strict option" do
|
76
|
+
expect { described_class.parse("no money", "CAD", strict: true) }.to raise_error(ArgumentError)
|
77
|
+
expect { described_class.parse("1,,1", "CAD", strict: true) }.to raise_error(ArgumentError)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "parses an integer string amount" do
|
81
|
+
expect(described_class.parse("1", "CAD")).to eq(Money.new(1.00, "CAD"))
|
82
|
+
expect(described_class.parse("-1", "CAD")).to eq(Money.new(-1.00, "CAD"))
|
83
|
+
end
|
84
|
+
|
85
|
+
it "parses a float string amount" do
|
86
|
+
expect(described_class.parse("1,37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
87
|
+
expect(described_class.parse("-1,37", "CAD")).to eq(Money.new(-1.37, "CAD"))
|
88
|
+
end
|
89
|
+
|
90
|
+
it "parses a float string with 3 decimals" do
|
91
|
+
expect(described_class.parse("1,378", "JOD")).to eq(Money.new(1.378, "JOD"))
|
92
|
+
expect(described_class.parse("-1,378", "JOD")).to eq(Money.new(-1.378, "JOD"))
|
93
|
+
end
|
94
|
+
|
95
|
+
it "parses a float string amount with a leading random character" do
|
96
|
+
expect(described_class.parse("$1,37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
97
|
+
expect(described_class.parse(".1,37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
98
|
+
expect(described_class.parse("€1,37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
99
|
+
end
|
100
|
+
|
101
|
+
it "parses a float string amount with a trailing random character" do
|
102
|
+
expect(described_class.parse("1,37$", "CAD")).to eq(Money.new(1.37, "CAD"))
|
103
|
+
expect(described_class.parse("1,37.", "CAD")).to eq(Money.new(1.37, "CAD"))
|
104
|
+
expect(described_class.parse("1,37€", "CAD")).to eq(Money.new(1.37, "CAD"))
|
105
|
+
end
|
106
|
+
|
107
|
+
it "parses an amount with one or more thousands separators" do
|
108
|
+
expect(described_class.parse("100.000", "CAD")).to eq(Money.new(100_000.00, "CAD"))
|
109
|
+
expect(described_class.parse("-100.000", "CAD")).to eq(Money.new(-100_000.00, "CAD"))
|
110
|
+
expect(described_class.parse("100.000,01", "CAD")).to eq(Money.new(100_000.01, "CAD"))
|
111
|
+
expect(described_class.parse("1.00.000,01", "CAD")).to eq(Money.new(100_000.01, "CAD"))
|
112
|
+
expect(described_class.parse("1.00.000,001", "JOD")).to eq(Money.new(100_000.001, "JOD"))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "parsing amounts with fullwidth characters" do
|
117
|
+
around(:example) do |example|
|
118
|
+
with_decimal_separator(".") { example.run }
|
119
|
+
end
|
120
|
+
|
121
|
+
it "parses an empty string to nil" do
|
122
|
+
expect(described_class.parse("", "CAD")).to be_nil
|
123
|
+
end
|
124
|
+
|
125
|
+
it "parses an invalid string when not strict" do
|
126
|
+
expect(described_class.parse("no money", "CAD")).to be_nil
|
127
|
+
expect(described_class.parse("1..", "CAD")).to be_nil
|
128
|
+
end
|
129
|
+
|
130
|
+
it "raises with an invalid string and strict option" do
|
131
|
+
expect { described_class.parse("no money", "CAD", strict: true) }.to raise_error(ArgumentError)
|
132
|
+
expect { described_class.parse("1..1", "CAD", strict: true) }.to raise_error(ArgumentError)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "parses an integer string amount" do
|
136
|
+
expect(described_class.parse("1", "CAD")).to eq(Money.new(1.00, "CAD"))
|
137
|
+
expect(described_class.parse("-1", "CAD")).to eq(Money.new(-1.00, "CAD"))
|
138
|
+
end
|
139
|
+
|
140
|
+
it "parses a float string amount" do
|
141
|
+
expect(described_class.parse("1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
142
|
+
expect(described_class.parse("-1.37", "CAD")).to eq(Money.new(-1.37, "CAD"))
|
143
|
+
end
|
144
|
+
|
145
|
+
it "parses a float string with 3 decimals" do
|
146
|
+
expect(described_class.parse("1.378", "JOD")).to eq(Money.new(1.378, "JOD"))
|
147
|
+
expect(described_class.parse("-1.378", "JOD")).to eq(Money.new(-1.378, "JOD"))
|
148
|
+
end
|
149
|
+
|
150
|
+
it "parses a float string amount with a leading random character" do
|
151
|
+
expect(described_class.parse("$1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
152
|
+
expect(described_class.parse(",1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
153
|
+
expect(described_class.parse("€1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
154
|
+
end
|
155
|
+
|
156
|
+
it "parses a float string amount with a trailing random character" do
|
157
|
+
expect(described_class.parse("1.37$", "CAD")).to eq(Money.new(1.37, "CAD"))
|
158
|
+
expect(described_class.parse("1.37,", "CAD")).to eq(Money.new(1.37, "CAD"))
|
159
|
+
expect(described_class.parse("1.37€", "CAD")).to eq(Money.new(1.37, "CAD"))
|
160
|
+
end
|
161
|
+
|
162
|
+
it "parses an amount with one or more thousands separators" do
|
163
|
+
expect(described_class.parse("100,000", "CAD")).to eq(Money.new(100_000.00, "CAD"))
|
164
|
+
expect(described_class.parse("-100,000", "CAD")).to eq(Money.new(-100_000.00, "CAD"))
|
165
|
+
expect(described_class.parse("100,000.01", "CAD")).to eq(Money.new(100_000.01, "CAD"))
|
166
|
+
expect(described_class.parse("1,00,000.01", "CAD")).to eq(Money.new(100_000.01, "CAD"))
|
167
|
+
expect(described_class.parse("1,00,000.001", "JOD")).to eq(Money.new(100_000.001, "JOD"))
|
168
|
+
expect(described_class.parse("100,000、000", "JPY")).to eq(Money.new(100_000_000, "JPY"))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "bad decimal_separator_proc" do
|
173
|
+
context "raises when called" do
|
174
|
+
around(:example) do |example|
|
175
|
+
with_decimal_separator_proc(->() { raise NoMethodError }) { example.run }
|
176
|
+
end
|
177
|
+
|
178
|
+
it "raises the original error" do
|
179
|
+
expect { described_class.parse("1", "CAD") }.to raise_error(NoMethodError)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context "returning nil" do
|
184
|
+
around(:example) do |example|
|
185
|
+
with_decimal_separator(nil) { example.run }
|
186
|
+
end
|
187
|
+
|
188
|
+
it "raises the original error" do
|
189
|
+
expect { described_class.parse("1", "CAD") }.to raise_error(ArgumentError)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def with_decimal_separator(character)
|
197
|
+
with_decimal_separator_proc(->() { character }) do
|
198
|
+
yield
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def with_decimal_separator_proc(proc)
|
203
|
+
old = described_class.decimal_separator_resolver
|
204
|
+
described_class.decimal_separator_resolver = proc
|
205
|
+
yield
|
206
|
+
described_class.decimal_separator_resolver = old
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe Money::Parser::Simple do
|
5
|
+
context "parsing amounts with period decimal separator" do
|
6
|
+
it "parses an empty string to nil" do
|
7
|
+
expect(described_class.parse("", "CAD")).to be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it "parses an invalid string when not strict" do
|
11
|
+
expect(described_class.parse("no money", "CAD")).to be_nil
|
12
|
+
expect(described_class.parse("1..", "CAD")).to be_nil
|
13
|
+
expect(described_class.parse("10.", "CAD")).to be_nil
|
14
|
+
expect(described_class.parse("10.1E2", "CAD")).to be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises with an invalid string and strict option" do
|
18
|
+
expect { described_class.parse("no money", "CAD", strict: true) }.to raise_error(ArgumentError)
|
19
|
+
expect { described_class.parse("1..1", "CAD", strict: true) }.to raise_error(ArgumentError)
|
20
|
+
expect { described_class.parse("10.", "CAD", strict: true) }.to raise_error(ArgumentError)
|
21
|
+
expect { described_class.parse("10.1E2", "CAD", strict: true) }.to raise_error(ArgumentError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "parses an integer string amount" do
|
25
|
+
expect(described_class.parse("1", "CAD")).to eq(Money.new(1.00, "CAD"))
|
26
|
+
expect(described_class.parse("-1", "CAD")).to eq(Money.new(-1.00, "CAD"))
|
27
|
+
end
|
28
|
+
|
29
|
+
it "parses a float string amount" do
|
30
|
+
expect(described_class.parse("1.37", "CAD")).to eq(Money.new(1.37, "CAD"))
|
31
|
+
expect(described_class.parse("-1.37", "CAD")).to eq(Money.new(-1.37, "CAD"))
|
32
|
+
end
|
33
|
+
|
34
|
+
it "parses a float string with 3 decimals" do
|
35
|
+
expect(described_class.parse("1.378", "JOD")).to eq(Money.new(1.378, "JOD"))
|
36
|
+
expect(described_class.parse("-1.378", "JOD")).to eq(Money.new(-1.378, "JOD"))
|
37
|
+
expect(described_class.parse("123.456", "USD")).to eq(Money.new(123.46, "USD"))
|
38
|
+
end
|
39
|
+
|
40
|
+
it "does not parse a float string amount with a leading random character" do
|
41
|
+
expect(described_class.parse("$1.37", "CAD")).to be_nil
|
42
|
+
expect(described_class.parse(",1.37", "CAD")).to be_nil
|
43
|
+
expect(described_class.parse("€1.37", "CAD")).to be_nil
|
44
|
+
expect(described_class.parse(" 1.37", "CAD")).to be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it "does not parse a float string amount with a trailing random character" do
|
48
|
+
expect(described_class.parse("1.37$", "CAD")).to be_nil
|
49
|
+
expect(described_class.parse("1.37,", "CAD")).to be_nil
|
50
|
+
expect(described_class.parse("1.37€", "CAD")).to be_nil
|
51
|
+
expect(described_class.parse("1.37 ", "CAD")).to be_nil
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does not parse an amount with one or more thousands separators" do
|
55
|
+
expect(described_class.parse("100,000", "CAD")).to be_nil
|
56
|
+
expect(described_class.parse("-100,000", "CAD")).to be_nil
|
57
|
+
expect(described_class.parse("100,000.01", "CAD")).to be_nil
|
58
|
+
expect(described_class.parse("1,00,000.01", "CAD")).to be_nil
|
59
|
+
expect(described_class.parse("1,00,000.001", "JOD")).to be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,6 +3,7 @@ require 'simplecov'
|
|
3
3
|
SimpleCov.minimum_coverage 100
|
4
4
|
SimpleCov.start do
|
5
5
|
add_filter "/spec/"
|
6
|
+
add_filter "/lib/money/railtie"
|
6
7
|
end
|
7
8
|
|
8
9
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
@@ -70,11 +71,10 @@ RSpec::Matchers.define :quack_like do
|
|
70
71
|
end
|
71
72
|
|
72
73
|
|
73
|
-
def configure(default_currency: nil, legacy_json_format: nil, legacy_deprecations: nil, legacy_default_currency: nil
|
74
|
+
def configure(default_currency: nil, legacy_json_format: nil, legacy_deprecations: nil, legacy_default_currency: nil)
|
74
75
|
old_config = Money.config
|
75
76
|
Money.config = Money::Config.new.tap do |config|
|
76
77
|
config.default_currency = default_currency if default_currency
|
77
|
-
config.parser = parser if parser
|
78
78
|
config.legacy_json_format! if legacy_json_format
|
79
79
|
config.legacy_deprecations! if legacy_deprecations
|
80
80
|
config.legacy_default_currency! if legacy_default_currency
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify-money
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-04-
|
11
|
+
date: 2022-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -117,7 +117,6 @@ files:
|
|
117
117
|
- config/currency_non_iso.yml
|
118
118
|
- dev.yml
|
119
119
|
- lib/money.rb
|
120
|
-
- lib/money/accounting_money_parser.rb
|
121
120
|
- lib/money/allocator.rb
|
122
121
|
- lib/money/config.rb
|
123
122
|
- lib/money/core_extensions.rb
|
@@ -127,8 +126,11 @@ files:
|
|
127
126
|
- lib/money/errors.rb
|
128
127
|
- lib/money/helpers.rb
|
129
128
|
- lib/money/money.rb
|
130
|
-
- lib/money/money_parser.rb
|
131
129
|
- lib/money/null_currency.rb
|
130
|
+
- lib/money/parser/accounting.rb
|
131
|
+
- lib/money/parser/fuzzy.rb
|
132
|
+
- lib/money/parser/locale_aware.rb
|
133
|
+
- lib/money/parser/simple.rb
|
132
134
|
- lib/money/rails/job_argument_serializer.rb
|
133
135
|
- lib/money/railtie.rb
|
134
136
|
- lib/money/version.rb
|
@@ -141,7 +143,6 @@ files:
|
|
141
143
|
- lib/rubocop/cop/money/zero_money.rb
|
142
144
|
- lib/shopify-money.rb
|
143
145
|
- money.gemspec
|
144
|
-
- spec/accounting_money_parser_spec.rb
|
145
146
|
- spec/allocator_spec.rb
|
146
147
|
- spec/config_spec.rb
|
147
148
|
- spec/core_extensions_spec.rb
|
@@ -149,9 +150,12 @@ files:
|
|
149
150
|
- spec/currency_spec.rb
|
150
151
|
- spec/helpers_spec.rb
|
151
152
|
- spec/money_column_spec.rb
|
152
|
-
- spec/money_parser_spec.rb
|
153
153
|
- spec/money_spec.rb
|
154
154
|
- spec/null_currency_spec.rb
|
155
|
+
- spec/parser/accounting_spec.rb
|
156
|
+
- spec/parser/fuzzy_spec.rb
|
157
|
+
- spec/parser/locale_aware_spec.rb
|
158
|
+
- spec/parser/simple_spec.rb
|
155
159
|
- spec/rails/job_argument_serializer_spec.rb
|
156
160
|
- spec/rails_spec_helper.rb
|
157
161
|
- spec/rubocop/cop/money/missing_currency_spec.rb
|
@@ -184,7 +188,6 @@ signing_key:
|
|
184
188
|
specification_version: 4
|
185
189
|
summary: Shopify's money gem
|
186
190
|
test_files:
|
187
|
-
- spec/accounting_money_parser_spec.rb
|
188
191
|
- spec/allocator_spec.rb
|
189
192
|
- spec/config_spec.rb
|
190
193
|
- spec/core_extensions_spec.rb
|
@@ -192,9 +195,12 @@ test_files:
|
|
192
195
|
- spec/currency_spec.rb
|
193
196
|
- spec/helpers_spec.rb
|
194
197
|
- spec/money_column_spec.rb
|
195
|
-
- spec/money_parser_spec.rb
|
196
198
|
- spec/money_spec.rb
|
197
199
|
- spec/null_currency_spec.rb
|
200
|
+
- spec/parser/accounting_spec.rb
|
201
|
+
- spec/parser/fuzzy_spec.rb
|
202
|
+
- spec/parser/locale_aware_spec.rb
|
203
|
+
- spec/parser/simple_spec.rb
|
198
204
|
- spec/rails/job_argument_serializer_spec.rb
|
199
205
|
- spec/rails_spec_helper.rb
|
200
206
|
- spec/rubocop/cop/money/missing_currency_spec.rb
|
data/lib/money/money_parser.rb
DELETED
@@ -1,162 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Parse an amount from a string
|
4
|
-
class MoneyParser
|
5
|
-
class MoneyFormatError < ArgumentError; end
|
6
|
-
|
7
|
-
MARKS = %w[. , · ’ ˙ '] + [' ']
|
8
|
-
|
9
|
-
ESCAPED_MARKS = Regexp.escape(MARKS.join)
|
10
|
-
ESCAPED_NON_SPACE_MARKS = Regexp.escape((MARKS - [' ']).join)
|
11
|
-
ESCAPED_NON_DOT_MARKS = Regexp.escape((MARKS - ['.']).join)
|
12
|
-
ESCAPED_NON_COMMA_MARKS = Regexp.escape((MARKS - [',']).join)
|
13
|
-
|
14
|
-
NUMERIC_REGEX = /(
|
15
|
-
[\+\-]?
|
16
|
-
[\d#{ESCAPED_NON_SPACE_MARKS}][\d#{ESCAPED_MARKS}]*
|
17
|
-
)/ix
|
18
|
-
|
19
|
-
# 1,234,567.89
|
20
|
-
DOT_DECIMAL_REGEX = /\A
|
21
|
-
[\+\-]?
|
22
|
-
(?:
|
23
|
-
(?:\d+)
|
24
|
-
(?:[#{ESCAPED_NON_DOT_MARKS}]\d{3})+
|
25
|
-
(?:\.\d{2,})?
|
26
|
-
)
|
27
|
-
\z/ix
|
28
|
-
|
29
|
-
# 1.234.567,89
|
30
|
-
COMMA_DECIMAL_REGEX = /\A
|
31
|
-
[\+\-]?
|
32
|
-
(?:
|
33
|
-
(?:\d+)
|
34
|
-
(?:[#{ESCAPED_NON_COMMA_MARKS}]\d{3})+
|
35
|
-
(?:\,\d{2,})?
|
36
|
-
)
|
37
|
-
\z/ix
|
38
|
-
|
39
|
-
# 12,34,567.89
|
40
|
-
INDIAN_NUMERIC_REGEX = /\A
|
41
|
-
[\+\-]?
|
42
|
-
(?:
|
43
|
-
(?:\d+)
|
44
|
-
(?:\,\d{2})+
|
45
|
-
(?:\,\d{3})
|
46
|
-
(?:\.\d{2})?
|
47
|
-
)
|
48
|
-
\z/ix
|
49
|
-
|
50
|
-
# 1,1123,4567.89
|
51
|
-
CHINESE_NUMERIC_REGEX = /\A
|
52
|
-
[\+\-]?
|
53
|
-
(?:
|
54
|
-
(?:\d+)
|
55
|
-
(?:\,\d{4})+
|
56
|
-
(?:\.\d{2})?
|
57
|
-
)
|
58
|
-
\z/ix
|
59
|
-
|
60
|
-
def self.parse(input, currency = nil, **options)
|
61
|
-
new.parse(input, currency, **options)
|
62
|
-
end
|
63
|
-
|
64
|
-
def parse(input, currency = nil, strict: false)
|
65
|
-
currency = Money::Helpers.value_to_currency(currency)
|
66
|
-
amount = extract_amount_from_string(input, currency, strict)
|
67
|
-
Money.new(amount, currency)
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def extract_amount_from_string(input, currency, strict)
|
73
|
-
unless input.is_a?(String)
|
74
|
-
return input
|
75
|
-
end
|
76
|
-
|
77
|
-
if input.strip.empty?
|
78
|
-
return '0'
|
79
|
-
end
|
80
|
-
|
81
|
-
number = input.scan(NUMERIC_REGEX).flatten.first
|
82
|
-
number = number.to_s.strip
|
83
|
-
|
84
|
-
if number.empty?
|
85
|
-
if Money.config.legacy_deprecations && !strict
|
86
|
-
Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
|
87
|
-
return '0'
|
88
|
-
else
|
89
|
-
raise MoneyFormatError, "invalid money string: #{input}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
marks = number.scan(/[#{ESCAPED_MARKS}]/).flatten
|
94
|
-
if marks.empty?
|
95
|
-
return number
|
96
|
-
end
|
97
|
-
|
98
|
-
if marks.size == 1
|
99
|
-
return normalize_number(number, marks, currency)
|
100
|
-
end
|
101
|
-
|
102
|
-
# remove end of string mark
|
103
|
-
number.sub!(/[#{ESCAPED_MARKS}]\z/, '')
|
104
|
-
|
105
|
-
if amount = number[DOT_DECIMAL_REGEX] || number[INDIAN_NUMERIC_REGEX] || number[CHINESE_NUMERIC_REGEX]
|
106
|
-
return amount.tr(ESCAPED_NON_DOT_MARKS, '')
|
107
|
-
end
|
108
|
-
|
109
|
-
if amount = number[COMMA_DECIMAL_REGEX]
|
110
|
-
return amount.tr(ESCAPED_NON_COMMA_MARKS, '').sub(',', '.')
|
111
|
-
end
|
112
|
-
|
113
|
-
if Money.config.legacy_deprecations && !strict
|
114
|
-
Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
|
115
|
-
else
|
116
|
-
raise MoneyFormatError, "invalid money string: #{input}"
|
117
|
-
end
|
118
|
-
|
119
|
-
normalize_number(number, marks, currency)
|
120
|
-
end
|
121
|
-
|
122
|
-
def normalize_number(number, marks, currency)
|
123
|
-
digits = number.rpartition(marks.last)
|
124
|
-
digits.first.tr!(ESCAPED_MARKS, '')
|
125
|
-
|
126
|
-
if last_digits_decimals?(digits, marks, currency)
|
127
|
-
"#{digits.first}.#{digits.last}"
|
128
|
-
else
|
129
|
-
"#{digits.first}#{digits.last}"
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def last_digits_decimals?(digits, marks, currency)
|
134
|
-
# Thousands marks are always different from decimal marks
|
135
|
-
# Example: 1,234,456
|
136
|
-
*other_marks, last_mark = marks
|
137
|
-
other_marks.uniq!
|
138
|
-
if other_marks.size == 1
|
139
|
-
return other_marks.first != last_mark
|
140
|
-
end
|
141
|
-
|
142
|
-
# Thousands always have more than 2 digits
|
143
|
-
# Example: 1,23 must be 1 dollar and 23 cents
|
144
|
-
if digits.last.size < 3
|
145
|
-
return !digits.last.empty?
|
146
|
-
end
|
147
|
-
|
148
|
-
# 0 before the final mark indicates last digits are decimals
|
149
|
-
# Example: 0,23
|
150
|
-
if digits.first.to_i.zero?
|
151
|
-
return true
|
152
|
-
end
|
153
|
-
|
154
|
-
# legacy support for 1.000 USD
|
155
|
-
if digits.last.size == 3 && digits.first.size <= 3 && currency.minor_units < 3
|
156
|
-
return false
|
157
|
-
end
|
158
|
-
|
159
|
-
# The last mark matches the one used by the provided currency to delimiter decimals
|
160
|
-
currency.decimal_mark == last_mark
|
161
|
-
end
|
162
|
-
end
|