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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9919cf2d14833f204c05a9642efb7a48bb23822c70c48cf523ccbc68c3e7491
4
- data.tar.gz: d73bd65e6cb49ee8f5be7add57683274bb006fbfd0ec61aec566385fd92d50f6
3
+ metadata.gz: 23562c524ba9f0d8219d01779505a75309d8825ed8f8839f1efdd71fbf5a1e89
4
+ data.tar.gz: ca1b6ea90e7a556970d135bbebc74c9cf6eb417cd475607ef58923751e2969b3
5
5
  SHA512:
6
- metadata.gz: b3dc0c02cee6598d2369ce472a5df01dfb3528cfb45f517394ab7c29b059338b8d65d10c03ae397a4ecdff98a7987a61e0312b894947b728e1ed32078b726086
7
- data.tar.gz: 1e7cec90da29c1327fdf64984f34e7b6fa01282eb7a0c2e3eda6482d688b06d148b7b7bff760555e3a7d7302ec8b20079ad468cd7c02dabb83c7e9a0aeb0a1f2
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', require: '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.default_currency = Money::Currency.new("USD")
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
@@ -2029,9 +2029,9 @@ ssp:
2029
2029
  thousands_separator: ","
2030
2030
  iso_numeric: '728'
2031
2031
  smallest_denomination: 5
2032
- std:
2032
+ stn:
2033
2033
  priority: 100
2034
- iso_code: STD
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: '678'
2045
- smallest_denomination: 10000
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
@@ -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 - BigDecimal("1")) > Float::EPSILON
48
+ if (allocations - ONE) > Float::EPSILON
47
49
  raise ArgumentError, "splits add to more than 100%"
48
50
  end
49
51
 
@@ -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
@@ -14,6 +14,6 @@ end
14
14
  # '100.37'.to_money => #<Money @cents=10037>
15
15
  class String
16
16
  def to_money(currency = nil)
17
- Money.parse(self, currency)
17
+ Money::Parser::Fuzzy.parse(self, currency)
18
18
  end
19
19
  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: false)
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(ArgumentError, 'missing currency') if default.nil? || default == ''
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.deprecate(error.message)
58
- Money::NULL_CURRENCY
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
- attr_accessor :parser, :default_currency
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
- default_settings
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
- Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
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
- Money.deprecate("mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}. " \
202
- "A Money::IncompatibleCurrencyError will raise in the next major release")
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 to_s(style = nil)
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 style: #{style}"
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
- to_s
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(*args)
237
- to_s
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
- Money.new(value.abs, currency)
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
- Money.new(value.floor, currency)
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
- Money.new(value.round(ndigits), currency)
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