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 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