shopify-money 0.16.0 → 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/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
|