shopify-money 1.0.1.pre → 1.0.2.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|