shopify-money 0.16.0 → 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/README.md +8 -2
- data/UPGRADING.md +59 -0
- data/config/currency_historic.yml +14 -0
- data/config/currency_iso.yml +22 -4
- data/lib/money/allocator.rb +3 -1
- data/lib/money/config.rb +25 -0
- data/lib/money/core_extensions.rb +1 -1
- data/lib/money/helpers.rb +8 -4
- data/lib/money/money.rb +48 -23
- 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/rails/job_argument_serializer.rb +22 -0
- data/lib/money/railtie.rb +19 -0
- data/lib/money/version.rb +1 -1
- data/lib/money.rb +7 -2
- data/lib/money_column/active_record_hooks.rb +12 -7
- data/spec/config_spec.rb +48 -0
- data/spec/helpers_spec.rb +11 -5
- data/spec/money_column_spec.rb +54 -33
- data/spec/money_spec.rb +129 -62
- data/spec/{accounting_money_parser_spec.rb → parser/accounting_spec.rb} +9 -15
- data/spec/{money_parser_spec.rb → parser/fuzzy_spec.rb} +25 -13
- data/spec/parser/locale_aware_spec.rb +208 -0
- data/spec/parser/simple_spec.rb +62 -0
- data/spec/rails/job_argument_serializer_spec.rb +20 -0
- data/spec/rails_spec_helper.rb +11 -0
- data/spec/spec_helper.rb +15 -0
- metadata +27 -11
- data/lib/money/accounting_money_parser.rb +0 -8
- data/lib/money/money_parser.rb +0 -156
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/README.md
CHANGED
@@ -18,7 +18,11 @@ money_column expects a DECIMAL(21,3) database field.
|
|
18
18
|
|
19
19
|
## Installation
|
20
20
|
|
21
|
-
gem 'shopify-money'
|
21
|
+
gem 'shopify-money'
|
22
|
+
|
23
|
+
## Upgrading to v1.0
|
24
|
+
|
25
|
+
see instructions and breaking changes: https://github.com/Shopify/money/blob/master/UPGRADING.md
|
22
26
|
|
23
27
|
## Usage
|
24
28
|
|
@@ -77,7 +81,9 @@ By default `Money` defaults to Money::NullCurrency as its currency. This is a
|
|
77
81
|
global variable that can be changed using:
|
78
82
|
|
79
83
|
``` ruby
|
80
|
-
Money.
|
84
|
+
Money.configure do |config|
|
85
|
+
config.default_currency = Money::Currency.new("USD")
|
86
|
+
end
|
81
87
|
```
|
82
88
|
|
83
89
|
In web apps you might want to set the default currency on a per request basis.
|
data/UPGRADING.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
## Upgrading to v1.0
|
2
|
+
|
3
|
+
In an initializer add the following
|
4
|
+
```ruby
|
5
|
+
Money.configure do |config|
|
6
|
+
config.legacy_default_currency!
|
7
|
+
config.legacy_deprecations!
|
8
|
+
config.legacy_json_format!
|
9
|
+
#...
|
10
|
+
end
|
11
|
+
```
|
12
|
+
|
13
|
+
Remove each legacy setting making sure your app functions as expected.
|
14
|
+
|
15
|
+
Replace `Money.parse` with `Money::Parser::Fuzzy.parse`.
|
16
|
+
|
17
|
+
### Legacy support
|
18
|
+
|
19
|
+
#### legacy_default_currency!
|
20
|
+
|
21
|
+
By enabling this setting your app will accept money object that are missing a currency
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
Money.new(1) #=> value: 1, currency: XXX
|
25
|
+
```
|
26
|
+
|
27
|
+
#### legacy_deprecations!
|
28
|
+
|
29
|
+
invalid money values return zero
|
30
|
+
```ruby
|
31
|
+
Money.new('a', 'USD') #=> Money.new(0, 'USD')
|
32
|
+
```
|
33
|
+
|
34
|
+
invalid currency is ignored
|
35
|
+
```ruby
|
36
|
+
Money.new(1, 'ABCD') #=> Money.new(1)
|
37
|
+
```
|
38
|
+
|
39
|
+
mathematical operations between objects are allowed
|
40
|
+
```ruby
|
41
|
+
Money.new(1, 'USD') + Money.new(1, 'CAD') #=> Money.new(2, 'USD')
|
42
|
+
```
|
43
|
+
|
44
|
+
parsing a string with invalid delimiters
|
45
|
+
```ruby
|
46
|
+
Money.parse('123*12') #=> Money.new(123)
|
47
|
+
```
|
48
|
+
|
49
|
+
#### legacy_json_format!
|
50
|
+
|
51
|
+
to_json will return only the value (no currency)
|
52
|
+
```ruby
|
53
|
+
# with legacy_json_format!
|
54
|
+
money.to_json #=> "1"
|
55
|
+
|
56
|
+
# without
|
57
|
+
money.to_json #=> { value: 1, currency: 'USD' }
|
58
|
+
```
|
59
|
+
|
@@ -73,6 +73,20 @@ mtl:
|
|
73
73
|
thousands_separator: ","
|
74
74
|
iso_numeric: '470'
|
75
75
|
smallest_denomination: 1
|
76
|
+
std:
|
77
|
+
priority: 100
|
78
|
+
iso_code: STD
|
79
|
+
name: São Tomé and Príncipe Dobra
|
80
|
+
symbol: Db
|
81
|
+
alternate_symbols: []
|
82
|
+
subunit: Cêntimo
|
83
|
+
subunit_to_unit: 100
|
84
|
+
symbol_first: false
|
85
|
+
html_entity: ''
|
86
|
+
decimal_mark: "."
|
87
|
+
thousands_separator: ","
|
88
|
+
iso_numeric: '678'
|
89
|
+
smallest_denomination: 10000
|
76
90
|
tmm:
|
77
91
|
priority: 100
|
78
92
|
iso_code: TMM
|
data/config/currency_iso.yml
CHANGED
@@ -2029,9 +2029,9 @@ ssp:
|
|
2029
2029
|
thousands_separator: ","
|
2030
2030
|
iso_numeric: '728'
|
2031
2031
|
smallest_denomination: 5
|
2032
|
-
|
2032
|
+
stn:
|
2033
2033
|
priority: 100
|
2034
|
-
iso_code:
|
2034
|
+
iso_code: STN
|
2035
2035
|
name: São Tomé and Príncipe Dobra
|
2036
2036
|
symbol: Db
|
2037
2037
|
alternate_symbols: []
|
@@ -2041,8 +2041,8 @@ std:
|
|
2041
2041
|
html_entity: ''
|
2042
2042
|
decimal_mark: "."
|
2043
2043
|
thousands_separator: ","
|
2044
|
-
iso_numeric: '
|
2045
|
-
smallest_denomination:
|
2044
|
+
iso_numeric: '930'
|
2045
|
+
smallest_denomination: 1
|
2046
2046
|
svc:
|
2047
2047
|
priority: 100
|
2048
2048
|
iso_code: SVC
|
@@ -2304,6 +2304,24 @@ uzs:
|
|
2304
2304
|
thousands_separator: ","
|
2305
2305
|
iso_numeric: '860'
|
2306
2306
|
smallest_denomination: 100
|
2307
|
+
ved:
|
2308
|
+
priority: 100
|
2309
|
+
iso_code: VED
|
2310
|
+
name: Venezuelan Bolívar soberano
|
2311
|
+
symbol: Bs.D.
|
2312
|
+
alternate_symbols: []
|
2313
|
+
subunit: Céntimo
|
2314
|
+
subunit_to_unit: 100
|
2315
|
+
symbol_first: true
|
2316
|
+
html_entity: ''
|
2317
|
+
decimal_mark: ","
|
2318
|
+
thousands_separator: "."
|
2319
|
+
iso_numeric: '926'
|
2320
|
+
# "On 1 Oct 2021, [...] another (redenomination) happened, but called 'Nueva expresión monetaria',
|
2321
|
+
# or new monetary expression, which removed 6 zeroes from the currency without affecting its denomination."
|
2322
|
+
# The VED has banknotes in denominations of 5, 10, 20, 50, and 100, and coins in 50 céntimos and 1 Bs.D.
|
2323
|
+
# Source: https://en.wikipedia.org/wiki/Venezuelan_bol%C3%ADvar
|
2324
|
+
smallest_denomination: 1
|
2307
2325
|
ves:
|
2308
2326
|
priority: 100
|
2309
2327
|
iso_code: VES
|
data/lib/money/allocator.rb
CHANGED
@@ -7,6 +7,8 @@ class Money
|
|
7
7
|
super
|
8
8
|
end
|
9
9
|
|
10
|
+
ONE = BigDecimal("1")
|
11
|
+
|
10
12
|
# Allocates money between different parties without losing pennies.
|
11
13
|
# After the mathematically split has been performed, left over pennies will
|
12
14
|
# be distributed round-robin amongst the parties. This means that parties
|
@@ -43,7 +45,7 @@ class Money
|
|
43
45
|
splits.map!(&:to_r)
|
44
46
|
allocations = splits.inject(0, :+)
|
45
47
|
|
46
|
-
if (allocations -
|
48
|
+
if (allocations - ONE) > Float::EPSILON
|
47
49
|
raise ArgumentError, "splits add to more than 100%"
|
48
50
|
end
|
49
51
|
|
data/lib/money/config.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Money
|
4
|
+
class Config
|
5
|
+
attr_accessor :default_currency, :legacy_json_format, :legacy_deprecations
|
6
|
+
|
7
|
+
def legacy_default_currency!
|
8
|
+
@default_currency ||= Money::NULL_CURRENCY
|
9
|
+
end
|
10
|
+
|
11
|
+
def legacy_deprecations!
|
12
|
+
@legacy_deprecations = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def legacy_json_format!
|
16
|
+
@legacy_json_format = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@default_currency = nil
|
21
|
+
@legacy_json_format = false
|
22
|
+
@legacy_deprecations = false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/money/helpers.rb
CHANGED
@@ -28,7 +28,7 @@ class Money
|
|
28
28
|
when Rational
|
29
29
|
BigDecimal(num, MAX_DECIMAL)
|
30
30
|
when String
|
31
|
-
decimal = BigDecimal(num, exception:
|
31
|
+
decimal = BigDecimal(num, exception: !Money.config.legacy_deprecations)
|
32
32
|
return decimal if decimal
|
33
33
|
|
34
34
|
Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
|
@@ -46,7 +46,7 @@ class Money
|
|
46
46
|
currency
|
47
47
|
when nil, ''
|
48
48
|
default = Money.current_currency || Money.default_currency
|
49
|
-
raise(
|
49
|
+
raise(Money::Currency::UnknownCurrency, 'missing currency') if default.nil? || default == ''
|
50
50
|
value_to_currency(default)
|
51
51
|
when 'xxx', 'XXX'
|
52
52
|
Money::NULL_CURRENCY
|
@@ -54,8 +54,12 @@ class Money
|
|
54
54
|
begin
|
55
55
|
Currency.find!(currency)
|
56
56
|
rescue Money::Currency::UnknownCurrency => error
|
57
|
-
Money.
|
58
|
-
|
57
|
+
if Money.config.legacy_deprecations
|
58
|
+
Money.deprecate(error.message)
|
59
|
+
Money::NULL_CURRENCY
|
60
|
+
else
|
61
|
+
raise error
|
62
|
+
end
|
59
63
|
end
|
60
64
|
else
|
61
65
|
raise ArgumentError, "could not parse as currency #{currency.inspect}"
|
data/lib/money/money.rb
CHANGED
@@ -11,7 +11,14 @@ class Money
|
|
11
11
|
def_delegators :@value, :zero?, :nonzero?, :positive?, :negative?, :to_i, :to_f, :hash
|
12
12
|
|
13
13
|
class << self
|
14
|
-
|
14
|
+
extend Forwardable
|
15
|
+
attr_accessor :config
|
16
|
+
def_delegators :@config, :default_currency, :default_currency=
|
17
|
+
|
18
|
+
def configure
|
19
|
+
self.config ||= Config.new
|
20
|
+
yield(config) if block_given?
|
21
|
+
end
|
15
22
|
|
16
23
|
def new(value = 0, currency = nil)
|
17
24
|
value = Helpers.value_to_decimal(value)
|
@@ -26,10 +33,6 @@ class Money
|
|
26
33
|
end
|
27
34
|
alias_method :from_amount, :new
|
28
35
|
|
29
|
-
def parse(*args, **kwargs)
|
30
|
-
parser.parse(*args, **kwargs)
|
31
|
-
end
|
32
|
-
|
33
36
|
def from_subunits(subunits, currency_iso, format: :iso4217)
|
34
37
|
currency = Helpers.value_to_currency(currency_iso)
|
35
38
|
|
@@ -73,13 +76,8 @@ class Money
|
|
73
76
|
Money.current_currency = old_currency
|
74
77
|
end
|
75
78
|
end
|
76
|
-
|
77
|
-
def default_settings
|
78
|
-
self.parser = MoneyParser
|
79
|
-
self.default_currency = Money::NULL_CURRENCY
|
80
|
-
end
|
81
79
|
end
|
82
|
-
|
80
|
+
configure
|
83
81
|
|
84
82
|
def initialize(value, currency)
|
85
83
|
raise ArgumentError if value.nan?
|
@@ -126,20 +124,27 @@ class Money
|
|
126
124
|
|
127
125
|
def +(other)
|
128
126
|
arithmetic(other) do |money|
|
127
|
+
return self if money.value == 0 && !no_currency?
|
129
128
|
Money.new(value + money.value, calculated_currency(money.currency))
|
130
129
|
end
|
131
130
|
end
|
132
131
|
|
133
132
|
def -(other)
|
134
133
|
arithmetic(other) do |money|
|
134
|
+
return self if money.value == 0 && !no_currency?
|
135
135
|
Money.new(value - money.value, calculated_currency(money.currency))
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
139
139
|
def *(numeric)
|
140
140
|
unless numeric.is_a?(Numeric)
|
141
|
-
|
141
|
+
if Money.config.legacy_deprecations
|
142
|
+
Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
|
143
|
+
else
|
144
|
+
raise ArgumentError, "Money objects can only be multiplied by a Numeric"
|
145
|
+
end
|
142
146
|
end
|
147
|
+
return self if numeric == 1
|
143
148
|
Money.new(value.to_r * numeric, currency)
|
144
149
|
end
|
145
150
|
|
@@ -198,8 +203,12 @@ class Money
|
|
198
203
|
|
199
204
|
curr = Helpers.value_to_currency(curr)
|
200
205
|
unless currency.compatible?(curr)
|
201
|
-
|
202
|
-
|
206
|
+
msg = "mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}"
|
207
|
+
if Money.config.legacy_deprecations
|
208
|
+
Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
|
209
|
+
else
|
210
|
+
raise Money::IncompatibleCurrencyError, msg
|
211
|
+
end
|
203
212
|
end
|
204
213
|
|
205
214
|
self
|
@@ -209,14 +218,14 @@ class Money
|
|
209
218
|
value
|
210
219
|
end
|
211
220
|
|
212
|
-
def
|
221
|
+
def to_fs(style = nil)
|
213
222
|
units = case style
|
214
223
|
when :legacy_dollars
|
215
224
|
2
|
216
225
|
when :amount, nil
|
217
226
|
currency.minor_units
|
218
227
|
else
|
219
|
-
raise ArgumentError, "Unexpected
|
228
|
+
raise ArgumentError, "Unexpected format: #{style}"
|
220
229
|
end
|
221
230
|
|
222
231
|
rounded_value = value.round(units)
|
@@ -228,25 +237,41 @@ class Money
|
|
228
237
|
sprintf("%s%d.%0#{units}d", sign, rounded_value.truncate, rounded_value.frac * (10 ** units))
|
229
238
|
end
|
230
239
|
end
|
240
|
+
alias_method :to_s, :to_fs
|
241
|
+
alias_method :to_formatted_s, :to_fs
|
231
242
|
|
232
|
-
def to_json(options =
|
233
|
-
|
243
|
+
def to_json(options = nil)
|
244
|
+
if (options.is_a?(Hash) && options.delete(:legacy_format)) || Money.config.legacy_json_format
|
245
|
+
to_s
|
246
|
+
else
|
247
|
+
as_json(options).to_json
|
248
|
+
end
|
234
249
|
end
|
235
250
|
|
236
|
-
def as_json(
|
237
|
-
|
251
|
+
def as_json(options = nil)
|
252
|
+
if (options.is_a?(Hash) && options.delete(:legacy_format)) || Money.config.legacy_json_format
|
253
|
+
to_s
|
254
|
+
else
|
255
|
+
{ value: to_s(:amount), currency: currency.to_s }
|
256
|
+
end
|
238
257
|
end
|
239
258
|
|
240
259
|
def abs
|
241
|
-
|
260
|
+
abs = value.abs
|
261
|
+
return self if value == abs
|
262
|
+
Money.new(abs, currency)
|
242
263
|
end
|
243
264
|
|
244
265
|
def floor
|
245
|
-
|
266
|
+
floor = value.floor
|
267
|
+
return self if floor == value
|
268
|
+
Money.new(floor, currency)
|
246
269
|
end
|
247
270
|
|
248
271
|
def round(ndigits=0)
|
249
|
-
|
272
|
+
round = value.round(ndigits)
|
273
|
+
return self if round == value
|
274
|
+
Money.new(round, currency)
|
250
275
|
end
|
251
276
|
|
252
277
|
def fraction(rate)
|
@@ -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
|