minting 1.0.0 → 1.1.0

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: 4dfd2f98840b009208b9cccab86e9ca5d093300351e9e2031e70e03b93315638
4
- data.tar.gz: 47a92df548558e20fd7bea3caa10a8966db91341d998e9712405af3d97fe12ae
3
+ metadata.gz: be78b384415345ebacec84c0fc7194c9cb5e8ae2c3f601db5871822987f7e95c
4
+ data.tar.gz: 0c852f37bca70dc4af732b824daca634a5b253d4497a10273cc9d041ccf327aa
5
5
  SHA512:
6
- metadata.gz: 175607c53124f16cac60c699a6ed6f042b3c3e960055bca1ccb7ba822a05b6cf8401fb66902e148a9ed50b43524ce6e708feaa72cac15f6ddd8b9cf3e9189cc6
7
- data.tar.gz: 4fc09433abd38014d0d99c16aed5f88eb36ad039e422b0ffa305d3335da1753a23bae2bfcf775ce6142edf844463ed3942710d87f034375f99e04ff1b7e92812
6
+ metadata.gz: 6e13eed34f93beb1a1e26cf89a89fb89a57a06529225610d8cc153c6eae28823368b47a993446e2e4deb3df30cecf31800253a7c55dd070bd6dcb074f615fb91
7
+ data.tar.gz: 8e2f204fbd592e93f143ed8756548d2013e909bca37d32a2fa70f41c070a3ede1946f668097faf49822a1e5e9df27cc9cf7865208faa94df47c310e06d96cd24
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gilson Ferraz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -33,7 +33,7 @@ total.currency_code #=> "USD"
33
33
 
34
34
  - Arithmetic: `+ - * /`, unary minus, `abs`
35
35
  - Comparisons: `==`, `<=>`, `zero?`, `nonzero?`, `positive?`, `negative?`
36
- - Formatting: `to_s` with custom formats, delimiters, separators
36
+ - Formatting: `to_s` with custom formats, thousand delimiters and decimal separators
37
37
  - Serialization: `to_json`, `to_i`, `to_f`, `to_r`, `to_d`
38
38
  - Allocation utilities: `split(quantity)`, `allocate([ratios])`
39
39
  - Numeric Refinements for ergonomics: `10.dollars`, `3.euros`, `4.to_money('USD')`
@@ -59,6 +59,16 @@ ten == 10.dollars #=> true
59
59
  ten == Mint.money(10, 'EUR') #=> false
60
60
  ten > Mint.money(9.99, 'USD') #=> true
61
61
 
62
+ # Zero equality semantics
63
+ # Any zero amount is treated as equal, regardless of currency
64
+ Mint.money(0, 'USD') == Mint.money(0, 'EUR') #=> true
65
+ Mint.money(0, 'USD') == 0 #=> true
66
+ Mint.money(0, 'USD') == 0.0 #=> true
67
+ Mint.money(0, 'USD') == 0r #=> true
68
+
69
+ # Non-zero numerics are not equal to Money objects
70
+ Mint.money(10, 'USD') == 10 #=> false
71
+
62
72
  # Format (uses Kernel.format internally)
63
73
  price = Mint.money(9.99, 'USD')
64
74
 
@@ -93,6 +103,22 @@ ten.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
93
103
 
94
104
  ```
95
105
 
106
+ ## API notes
107
+
108
+ **Module names** — Require the `minting` gem; the public API lives under `Mint` (the gem module is `Minting::VERSION`).
109
+
110
+ **Exact amounts** — Amounts are stored as `Rational` and rounded to the currency subunit. Prefer rationals or decimal strings for exact literals (`Mint.money(1999/100r, 'USD')`, `'19.99'.to_r`) instead of binary floats when precision matters.
111
+
112
+ **Refinements** — `10.dollars` and similar helpers require `using Mint` in the current scope (see Usage above).
113
+
114
+ **Division** — `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
115
+
116
+ **Zero equality** — `Mint.money(0, 'USD') == Mint.money(0, 'EUR')` is intentionally `true`. Non-zero amounts must match currency and value.
117
+
118
+ **Custom currencies** — `Mint.register_currency` returns the existing entry if the code is already registered; use `register_currency!` to detect duplicates.
119
+
120
+ **Built-in currencies** — ISO-style codes ship in `lib/minting/data/currencies.yaml` and load when the registry is first accessed.
121
+
96
122
  ## Installation
97
123
 
98
124
  Option 1: Via bundler command
@@ -126,11 +152,25 @@ Option 3: Install it yourself with:
126
152
  gem install minting
127
153
  ```
128
154
 
155
+ ## Parsing strings
156
+
157
+ ```ruby
158
+ Mint::Money.parse('$19.99') #=> [USD 19.99]
159
+ Mint::Money.parse('19,99 €') #=> [EUR 19.99]
160
+ Mint::Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
161
+ Mint::Money.parse('USD 1,234.56') #=> [USD 1234.56]
162
+ ```
163
+
164
+ - Pass a currency code when the string has no symbol or code.
165
+ - 1,234 means 1.234, not 1234, because one comma is treated as decimal.
166
+ - 1,234.00 is unambiguous thousands-plus-decimal.
167
+ - accounting negatives like ($1.23) are unsupported.
168
+ - ambiguous symbols like $ resolve by priority, currently USD.
169
+
129
170
  ## Roadmap
130
171
 
131
172
  - Improve formatting features
132
173
  - Localization (I18n-aware formatting)
133
- - `Mint.parse` for parsing human strings into money
134
174
  - Basic exchange-rate conversions
135
175
 
136
176
  ## Contributing
@@ -0,0 +1,469 @@
1
+ ---
2
+ USD:
3
+ subunit: 2
4
+ symbol: "$"
5
+ priority: 1000
6
+ EUR:
7
+ subunit: 2
8
+ symbol: "€"
9
+ priority: 950
10
+ JPY:
11
+ subunit: 0
12
+ symbol: "¥"
13
+ priority: 900
14
+ GBP:
15
+ subunit: 2
16
+ symbol: "£"
17
+ priority: 850
18
+ CNY:
19
+ subunit: 2
20
+ symbol: "¥"
21
+ priority: 800
22
+ INR:
23
+ subunit: 2
24
+ symbol: "₹"
25
+ priority: 760
26
+ CAD:
27
+ subunit: 2
28
+ symbol: "$"
29
+ priority: 740
30
+ AUD:
31
+ subunit: 2
32
+ symbol: "$"
33
+ priority: 730
34
+ CHF:
35
+ subunit: 2
36
+ symbol: Fr
37
+ priority: 720
38
+ HKD:
39
+ subunit: 2
40
+ symbol: HK$
41
+ priority: 710
42
+ SGD:
43
+ subunit: 2
44
+ symbol: S$
45
+ priority: 700
46
+ KRW:
47
+ subunit: 0
48
+ symbol: "₩"
49
+ priority: 690
50
+ SEK:
51
+ subunit: 2
52
+ symbol: kr
53
+ priority: 680
54
+ NOK:
55
+ subunit: 2
56
+ symbol: kr
57
+ priority: 670
58
+ DKK:
59
+ subunit: 2
60
+ symbol: kr
61
+ priority: 660
62
+ NZD:
63
+ subunit: 2
64
+ symbol: "$"
65
+ priority: 650
66
+ MXN:
67
+ subunit: 2
68
+ symbol: "$"
69
+ priority: 640
70
+ BRL:
71
+ subunit: 2
72
+ symbol: R$
73
+ priority: 630
74
+ ZAR:
75
+ subunit: 2
76
+ symbol: R
77
+ priority: 620
78
+ SAR:
79
+ subunit: 2
80
+ symbol: "﷼"
81
+ priority: 610
82
+ AED:
83
+ subunit: 2
84
+ symbol: د.إ
85
+ priority: 600
86
+ IDR:
87
+ subunit: 2
88
+ symbol: Rp
89
+ priority: 113
90
+ PKR:
91
+ subunit: 2
92
+ symbol: "₨"
93
+ priority: 112
94
+ NGN:
95
+ subunit: 2
96
+ symbol: "₦"
97
+ priority: 111
98
+ BDT:
99
+ subunit: 2
100
+ symbol: "৳"
101
+ priority: 109
102
+ XOF:
103
+ subunit: 0
104
+ symbol: CFA
105
+ priority: 108
106
+ RUB:
107
+ subunit: 2
108
+ symbol: "₽"
109
+ priority: 107
110
+ ETB:
111
+ subunit: 2
112
+ symbol: Br
113
+ priority: 105
114
+ PHP:
115
+ subunit: 2
116
+ symbol: "₱"
117
+ priority: 103
118
+ EGP:
119
+ subunit: 2
120
+ symbol: "£"
121
+ priority: 102
122
+ VND:
123
+ subunit: 0
124
+ symbol: "₫"
125
+ priority: 101
126
+ IRR:
127
+ subunit: 2
128
+ symbol: "﷼"
129
+ priority: 100
130
+ TRY:
131
+ subunit: 2
132
+ symbol: "₺"
133
+ priority: 99
134
+ THB:
135
+ subunit: 2
136
+ symbol: "฿"
137
+ priority: 98
138
+ TZS:
139
+ subunit: 2
140
+ symbol: TSh
141
+ priority: 96
142
+ XAF:
143
+ subunit: 0
144
+ symbol: FCFA
145
+ priority: 95
146
+ KES:
147
+ subunit: 2
148
+ symbol: KSh
149
+ priority: 93
150
+ MMK:
151
+ subunit: 2
152
+ symbol: K
153
+ priority: 92
154
+ COP:
155
+ subunit: 2
156
+ symbol: "$"
157
+ priority: 91
158
+ UGX:
159
+ subunit: 0
160
+ symbol: USh
161
+ priority: 89
162
+ ARS:
163
+ subunit: 2
164
+ symbol: "$"
165
+ priority: 88
166
+ DZD:
167
+ subunit: 2
168
+ symbol: د.ج
169
+ priority: 87
170
+ IQD:
171
+ subunit: 3
172
+ symbol: د.ع
173
+ priority: 86
174
+ AFN:
175
+ subunit: 2
176
+ symbol: "؋"
177
+ priority: 85
178
+ MAD:
179
+ subunit: 2
180
+ symbol: د.م.
181
+ priority: 83
182
+ PLN:
183
+ subunit: 2
184
+ symbol: zł
185
+ priority: 82
186
+ UAH:
187
+ subunit: 2
188
+ symbol: "₴"
189
+ priority: 80
190
+ AOA:
191
+ subunit: 2
192
+ symbol: Kz
193
+ priority: 79
194
+ UZS:
195
+ subunit: 2
196
+ symbol: лв
197
+ priority: 78
198
+ GHS:
199
+ subunit: 2
200
+ symbol: "¢"
201
+ priority: 77
202
+ MYR:
203
+ subunit: 2
204
+ symbol: RM
205
+ priority: 76
206
+ MZN:
207
+ subunit: 2
208
+ symbol: MT
209
+ priority: 75
210
+ PEN:
211
+ subunit: 2
212
+ symbol: S/.
213
+ priority: 74
214
+ NPR:
215
+ subunit: 2
216
+ symbol: "₨"
217
+ priority: 73
218
+ VES:
219
+ subunit: 2
220
+ symbol: Bs.
221
+ priority: 72
222
+ TWD:
223
+ subunit: 2
224
+ symbol: NT$
225
+ priority: 70
226
+ LKR:
227
+ subunit: 2
228
+ symbol: "₨"
229
+ priority: 69
230
+ MWK:
231
+ subunit: 2
232
+ symbol: MK
233
+ priority: 68
234
+ CLP:
235
+ subunit: 0
236
+ symbol: "$"
237
+ priority: 67
238
+ KZT:
239
+ subunit: 2
240
+ symbol: "₸"
241
+ priority: 66
242
+ ZMW:
243
+ subunit: 2
244
+ symbol: ZK
245
+ priority: 65
246
+ RON:
247
+ subunit: 2
248
+ symbol: lei
249
+ priority: 64
250
+ GTQ:
251
+ subunit: 2
252
+ symbol: Q
253
+ priority: 63
254
+ KHR:
255
+ subunit: 2
256
+ symbol: "៛"
257
+ priority: 62
258
+ RWF:
259
+ subunit: 0
260
+ symbol: R₣
261
+ priority: 61
262
+ BIF:
263
+ subunit: 0
264
+ symbol: FBu
265
+ priority: 60
266
+ BOB:
267
+ subunit: 2
268
+ symbol: "$b"
269
+ priority: 59
270
+ HTG:
271
+ subunit: 2
272
+ symbol: G
273
+ priority: 58
274
+ TND:
275
+ subunit: 3
276
+ symbol: د.ت
277
+ priority: 57
278
+ CZK:
279
+ subunit: 2
280
+ symbol: Kč
281
+ priority: 56
282
+ DOP:
283
+ subunit: 2
284
+ symbol: RD$
285
+ priority: 55
286
+ HNL:
287
+ subunit: 2
288
+ symbol: L
289
+ priority: 54
290
+ JOD:
291
+ subunit: 3
292
+ symbol: د.ا
293
+ priority: 53
294
+ AZN:
295
+ subunit: 2
296
+ symbol: "₼"
297
+ priority: 50
298
+ HUF:
299
+ subunit: 2
300
+ symbol: Ft
301
+ priority: 49
302
+ ILS:
303
+ subunit: 2
304
+ symbol: "₪"
305
+ priority: 48
306
+ PGK:
307
+ subunit: 2
308
+ symbol: K
309
+ priority: 47
310
+ TJS:
311
+ subunit: 2
312
+ symbol: SM
313
+ priority: 46
314
+ BYN:
315
+ subunit: 2
316
+ symbol: Br
317
+ priority: 45
318
+ LAK:
319
+ subunit: 2
320
+ symbol: "₭"
321
+ priority: 42
322
+ KGS:
323
+ subunit: 2
324
+ symbol: лв
325
+ priority: 41
326
+ LYD:
327
+ subunit: 3
328
+ symbol: ل.د
329
+ priority: 40
330
+ NIO:
331
+ subunit: 2
332
+ symbol: C$
333
+ priority: 39
334
+ PYG:
335
+ subunit: 0
336
+ symbol: Gs
337
+ priority: 38
338
+ RSD:
339
+ subunit: 2
340
+ symbol: Дин.
341
+ priority: 37
342
+ BGN:
343
+ subunit: 2
344
+ symbol: лв
345
+ priority: 36
346
+ CRC:
347
+ subunit: 2
348
+ symbol: "₡"
349
+ priority: 32
350
+ KWD:
351
+ subunit: 3
352
+ symbol: د.ك
353
+ priority: 30
354
+ OMR:
355
+ subunit: 3
356
+ symbol: "﷼"
357
+ priority: 29
358
+ PAB:
359
+ subunit: 2
360
+ symbol: B/.
361
+ priority: 28
362
+ HRK:
363
+ subunit: 2
364
+ symbol: kn
365
+ priority: 27
366
+ GEL:
367
+ subunit: 2
368
+ symbol: "₾"
369
+ priority: 26
370
+ UYU:
371
+ subunit: 2
372
+ symbol: "$U"
373
+ priority: 25
374
+ AMD:
375
+ subunit: 2
376
+ symbol: "֏"
377
+ priority: 24
378
+ JMD:
379
+ subunit: 2
380
+ symbol: J$
381
+ priority: 23
382
+ QAR:
383
+ subunit: 2
384
+ symbol: "﷼"
385
+ priority: 22
386
+ BWP:
387
+ subunit: 2
388
+ symbol: P
389
+ priority: 21
390
+ MDL:
391
+ subunit: 2
392
+ symbol: L
393
+ priority: 20
394
+ NAD:
395
+ subunit: 2
396
+ symbol: N$
397
+ priority: 19
398
+ LSL:
399
+ subunit: 2
400
+ symbol: L
401
+ priority: 18
402
+ BHD:
403
+ subunit: 3
404
+ symbol: ".د.ب"
405
+ priority: 17
406
+ TTD:
407
+ subunit: 2
408
+ symbol: TT$
409
+ priority: 16
410
+ SZL:
411
+ subunit: 2
412
+ symbol: L
413
+ priority: 15
414
+ FJD:
415
+ subunit: 2
416
+ symbol: FJ$
417
+ priority: 14
418
+ GYD:
419
+ subunit: 2
420
+ symbol: G$
421
+ priority: 13
422
+ SBD:
423
+ subunit: 2
424
+ symbol: SI$
425
+ priority: 12
426
+ SRD:
427
+ subunit: 2
428
+ symbol: Sr$
429
+ priority: 11
430
+ XCD:
431
+ subunit: 2
432
+ symbol: EC$
433
+ priority: 10
434
+ BND:
435
+ subunit: 2
436
+ symbol: B$
437
+ priority: 9
438
+ BSD:
439
+ subunit: 2
440
+ symbol: B$
441
+ priority: 8
442
+ BZD:
443
+ subunit: 2
444
+ symbol: BZ$
445
+ priority: 7
446
+ ISK:
447
+ subunit: 0
448
+ symbol: kr
449
+ priority: 6
450
+ BBD:
451
+ subunit: 2
452
+ symbol: Bds$
453
+ priority: 5
454
+ VUV:
455
+ subunit: 0
456
+ symbol: VT
457
+ priority: 4
458
+ XPF:
459
+ subunit: 0
460
+ symbol: "₣"
461
+ priority: 3
462
+ WST:
463
+ subunit: 2
464
+ symbol: WS$
465
+ priority: 2
466
+ TOP:
467
+ subunit: 2
468
+ symbol: T$
469
+ priority: 1
@@ -3,22 +3,21 @@ module Mint
3
3
  #
4
4
  # @see https://www.iso.org/iso-4217-currency-codes.html
5
5
  class Currency
6
- attr_reader :code, :subunit, :symbol
6
+ attr_reader :code, :subunit, :symbol, :priority, :minimum_amount
7
7
 
8
8
  def inspect
9
9
  "<Currency:(#{code} #{symbol} #{subunit})>"
10
10
  end
11
11
 
12
- def minimum_amount
13
- @minimum_amount ||= 10r**-subunit
14
- end
15
-
16
12
  private
17
13
 
18
- def initialize(code, subunit:, symbol:)
19
- @code = code
20
- @subunit = subunit
21
- @symbol = symbol
14
+ def initialize(code, subunit:, symbol:, priority: 0)
15
+ @code = code.to_s
16
+ @subunit = subunit.to_i
17
+ @symbol = symbol.to_s
18
+ @priority = priority.to_i
19
+ @minimum_amount = 10r**-subunit
20
+ freeze
22
21
  end
23
22
  end
24
23
  end
@@ -1,11 +1,12 @@
1
+ require 'yaml'
2
+
1
3
  # :nodoc
2
4
  module Mint
3
5
  def self.money(amount, currency_code)
4
6
  currency = currency(currency_code)
5
7
  return Money.new(amount, currency) if currency
6
8
 
7
- available = currencies.keys.join(', ')
8
- raise ArgumentError, "Currency [#{currency_code}] not registered. Available: #{available}"
9
+ raise ArgumentError, "Currency [#{currency_code}] not registered. Check Mint.currencies"
9
10
  end
10
11
 
11
12
  def self.currency(currency)
@@ -19,12 +20,13 @@ module Mint
19
20
  end
20
21
  end
21
22
 
22
- def self.register_currency(code, subunit: 2, symbol: '$')
23
+ def self.register_currency(code, subunit: 2, symbol: '$', priority: 0)
23
24
  code = code.to_s
24
- currencies[code] || register_currency!(code, subunit: subunit, symbol: symbol)
25
+ currencies[code] || register_currency!(code, subunit: subunit, symbol: symbol,
26
+ priority: priority)
25
27
  end
26
28
 
27
- def self.register_currency!(code, subunit:, symbol: '')
29
+ def self.register_currency!(code, subunit:, symbol: '', priority: 0)
28
30
  code = code.to_s
29
31
  unless code.match?(/^[A-Z_]+$/)
30
32
  raise ArgumentError,
@@ -36,141 +38,36 @@ module Mint
36
38
  end
37
39
 
38
40
  currencies[code] =
39
- Currency.new(code, subunit: subunit.to_i, symbol: symbol.to_s).freeze
41
+ Currency.new(code, subunit: subunit, symbol: symbol, priority: priority)
42
+ @currency_symbols = nil
43
+ currencies[code]
40
44
  end
41
45
 
42
46
  def self.currencies
43
- @currencies ||= {
44
- # Major Global Currencies
45
- 'USD' => Currency.new('USD', subunit: 2, symbol: '$'),
46
- 'EUR' => Currency.new('EUR', subunit: 2, symbol: '€'),
47
- 'GBP' => Currency.new('GBP', subunit: 2, symbol: '£'),
48
- 'JPY' => Currency.new('JPY', subunit: 0, symbol: '¥'),
49
- 'CHF' => Currency.new('CHF', subunit: 2, symbol: 'Fr'),
50
- 'CAD' => Currency.new('CAD', subunit: 2, symbol: '$'),
51
- 'AUD' => Currency.new('AUD', subunit: 2, symbol: '$'),
52
- 'CNY' => Currency.new('CNY', subunit: 2, symbol: '¥'),
53
- 'SEK' => Currency.new('SEK', subunit: 2, symbol: 'kr'),
54
- 'NZD' => Currency.new('NZD', subunit: 2, symbol: '$'),
55
-
56
- # Asia-Pacific
57
- 'HKD' => Currency.new('HKD', subunit: 2, symbol: 'HK$'),
58
- 'SGD' => Currency.new('SGD', subunit: 2, symbol: 'S$'),
59
- 'KRW' => Currency.new('KRW', subunit: 0, symbol: '₩'),
60
- 'INR' => Currency.new('INR', subunit: 2, symbol: '₹'),
61
- 'THB' => Currency.new('THB', subunit: 2, symbol: '฿'),
62
- 'MYR' => Currency.new('MYR', subunit: 2, symbol: 'RM'),
63
- 'IDR' => Currency.new('IDR', subunit: 2, symbol: 'Rp'),
64
- 'PHP' => Currency.new('PHP', subunit: 2, symbol: '₱'),
65
- 'VND' => Currency.new('VND', subunit: 0, symbol: '₫'),
66
- 'TWD' => Currency.new('TWD', subunit: 2, symbol: 'NT$'),
67
- 'PKR' => Currency.new('PKR', subunit: 2, symbol: '₨'),
68
- 'BDT' => Currency.new('BDT', subunit: 2, symbol: '৳'),
69
- 'LKR' => Currency.new('LKR', subunit: 2, symbol: '₨'),
70
- 'NPR' => Currency.new('NPR', subunit: 2, symbol: '₨'),
71
- 'MMK' => Currency.new('MMK', subunit: 2, symbol: 'K'),
72
- 'KHR' => Currency.new('KHR', subunit: 2, symbol: '៛'),
73
- 'LAK' => Currency.new('LAK', subunit: 2, symbol: '₭'),
74
- 'BND' => Currency.new('BND', subunit: 2, symbol: 'B$'),
75
-
76
- # Middle East & Central Asia
77
- 'AED' => Currency.new('AED', subunit: 2, symbol: 'د.إ'),
78
- 'SAR' => Currency.new('SAR', subunit: 2, symbol: '﷼'),
79
- 'QAR' => Currency.new('QAR', subunit: 2, symbol: '﷼'),
80
- 'KWD' => Currency.new('KWD', subunit: 3, symbol: 'د.ك'),
81
- 'BHD' => Currency.new('BHD', subunit: 3, symbol: '.د.ب'),
82
- 'OMR' => Currency.new('OMR', subunit: 3, symbol: '﷼'),
83
- 'JOD' => Currency.new('JOD', subunit: 3, symbol: 'د.ا'),
84
- 'ILS' => Currency.new('ILS', subunit: 2, symbol: '₪'),
85
- 'TRY' => Currency.new('TRY', subunit: 2, symbol: '₺'),
86
- 'IRR' => Currency.new('IRR', subunit: 2, symbol: '﷼'),
87
- 'IQD' => Currency.new('IQD', subunit: 3, symbol: 'د.ع'),
88
- 'AFN' => Currency.new('AFN', subunit: 2, symbol: '؋'),
89
- 'KZT' => Currency.new('KZT', subunit: 2, symbol: '₸'),
90
- 'UZS' => Currency.new('UZS', subunit: 2, symbol: 'лв'),
91
- 'KGS' => Currency.new('KGS', subunit: 2, symbol: 'лв'),
92
- 'TJS' => Currency.new('TJS', subunit: 2, symbol: 'SM'),
93
-
94
- # Europe
95
- 'NOK' => Currency.new('NOK', subunit: 2, symbol: 'kr'),
96
- 'DKK' => Currency.new('DKK', subunit: 2, symbol: 'kr'),
97
- 'ISK' => Currency.new('ISK', subunit: 0, symbol: 'kr'),
98
- 'PLN' => Currency.new('PLN', subunit: 2, symbol: 'zł'),
99
- 'CZK' => Currency.new('CZK', subunit: 2, symbol: 'Kč'),
100
- 'HUF' => Currency.new('HUF', subunit: 2, symbol: 'Ft'),
101
- 'RON' => Currency.new('RON', subunit: 2, symbol: 'lei'),
102
- 'BGN' => Currency.new('BGN', subunit: 2, symbol: 'лв'),
103
- 'HRK' => Currency.new('HRK', subunit: 2, symbol: 'kn'),
104
- 'RSD' => Currency.new('RSD', subunit: 2, symbol: 'Дин.'),
105
- 'RUB' => Currency.new('RUB', subunit: 2, symbol: '₽'),
106
- 'UAH' => Currency.new('UAH', subunit: 2, symbol: '₴'),
107
- 'BYN' => Currency.new('BYN', subunit: 2, symbol: 'Br'),
108
- 'MDL' => Currency.new('MDL', subunit: 2, symbol: 'L'),
109
- 'GEL' => Currency.new('GEL', subunit: 2, symbol: '₾'),
110
- 'AMD' => Currency.new('AMD', subunit: 2, symbol: '֏'),
111
- 'AZN' => Currency.new('AZN', subunit: 2, symbol: '₼'),
112
-
113
- # Africa
114
- 'ZAR' => Currency.new('ZAR', subunit: 2, symbol: 'R'),
115
- 'EGP' => Currency.new('EGP', subunit: 2, symbol: '£'),
116
- 'NGN' => Currency.new('NGN', subunit: 2, symbol: '₦'),
117
- 'KES' => Currency.new('KES', subunit: 2, symbol: 'KSh'),
118
- 'GHS' => Currency.new('GHS', subunit: 2, symbol: '¢'),
119
- 'UGX' => Currency.new('UGX', subunit: 0, symbol: 'USh'),
120
- 'TZS' => Currency.new('TZS', subunit: 2, symbol: 'TSh'),
121
- 'ETB' => Currency.new('ETB', subunit: 2, symbol: 'Br'),
122
- 'MAD' => Currency.new('MAD', subunit: 2, symbol: 'د.م.'),
123
- 'TND' => Currency.new('TND', subunit: 3, symbol: 'د.ت'),
124
- 'DZD' => Currency.new('DZD', subunit: 2, symbol: 'د.ج'),
125
- 'LYD' => Currency.new('LYD', subunit: 3, symbol: 'ل.د'),
126
- 'AOA' => Currency.new('AOA', subunit: 2, symbol: 'Kz'),
127
- 'BWP' => Currency.new('BWP', subunit: 2, symbol: 'P'),
128
- 'NAD' => Currency.new('NAD', subunit: 2, symbol: 'N$'),
129
- 'SZL' => Currency.new('SZL', subunit: 2, symbol: 'L'),
130
- 'LSL' => Currency.new('LSL', subunit: 2, symbol: 'L'),
131
- 'MZN' => Currency.new('MZN', subunit: 2, symbol: 'MT'),
132
- 'ZMW' => Currency.new('ZMW', subunit: 2, symbol: 'ZK'),
133
- 'MWK' => Currency.new('MWK', subunit: 2, symbol: 'MK'),
134
- 'RWF' => Currency.new('RWF', subunit: 0, symbol: 'R₣'),
135
- 'BIF' => Currency.new('BIF', subunit: 0, symbol: 'FBu'),
47
+ @currencies ||= load_currencies
48
+ end
136
49
 
137
- # Americas
138
- 'MXN' => Currency.new('MXN', subunit: 2, symbol: '$'),
139
- 'BRL' => Currency.new('BRL', subunit: 2, symbol: 'R$'),
140
- 'ARS' => Currency.new('ARS', subunit: 2, symbol: '$'),
141
- 'CLP' => Currency.new('CLP', subunit: 0, symbol: '$'),
142
- 'PEN' => Currency.new('PEN', subunit: 2, symbol: 'S/.'),
143
- 'COP' => Currency.new('COP', subunit: 2, symbol: '$'),
144
- 'VES' => Currency.new('VES', subunit: 2, symbol: 'Bs.'),
145
- 'UYU' => Currency.new('UYU', subunit: 2, symbol: '$U'),
146
- 'PYG' => Currency.new('PYG', subunit: 0, symbol: 'Gs'),
147
- 'BOB' => Currency.new('BOB', subunit: 2, symbol: '$b'),
148
- 'CRC' => Currency.new('CRC', subunit: 2, symbol: '₡'),
149
- 'GTQ' => Currency.new('GTQ', subunit: 2, symbol: 'Q'),
150
- 'HNL' => Currency.new('HNL', subunit: 2, symbol: 'L'),
151
- 'NIO' => Currency.new('NIO', subunit: 2, symbol: 'C$'),
152
- 'PAB' => Currency.new('PAB', subunit: 2, symbol: 'B/.'),
153
- 'DOP' => Currency.new('DOP', subunit: 2, symbol: 'RD$'),
154
- 'HTG' => Currency.new('HTG', subunit: 2, symbol: 'G'),
155
- 'JMD' => Currency.new('JMD', subunit: 2, symbol: 'J$'),
156
- 'TTD' => Currency.new('TTD', subunit: 2, symbol: 'TT$'),
157
- 'BBD' => Currency.new('BBD', subunit: 2, symbol: 'Bds$'),
158
- 'BSD' => Currency.new('BSD', subunit: 2, symbol: 'B$'),
159
- 'BZD' => Currency.new('BZD', subunit: 2, symbol: 'BZ$'),
160
- 'GYD' => Currency.new('GYD', subunit: 2, symbol: 'G$'),
161
- 'SRD' => Currency.new('SRD', subunit: 2, symbol: 'Sr$'),
50
+ # Registered symbols sorted for detection: longest match wins, then parser priority.
51
+ def self.currency_symbols
52
+ @currency_symbols ||= begin
53
+ currencies.values
54
+ .map { |currency| [currency.symbol, currency] }
55
+ .reject { |symbol, _| symbol.empty? }
56
+ .sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
57
+ end.freeze
58
+ end
162
59
 
163
- # Pacific & Others
164
- 'FJD' => Currency.new('FJD', subunit: 2, symbol: 'FJ$'),
165
- 'PGK' => Currency.new('PGK', subunit: 2, symbol: 'K'),
166
- 'SBD' => Currency.new('SBD', subunit: 2, symbol: 'SI$'),
167
- 'VUV' => Currency.new('VUV', subunit: 0, symbol: 'VT'),
168
- 'TOP' => Currency.new('TOP', subunit: 2, symbol: 'T$'),
169
- 'WST' => Currency.new('WST', subunit: 2, symbol: 'WS$'),
170
- 'XCD' => Currency.new('XCD', subunit: 2, symbol: 'EC$'),
171
- 'XOF' => Currency.new('XOF', subunit: 0, symbol: 'CFA'),
172
- 'XAF' => Currency.new('XAF', subunit: 0, symbol: 'FCFA'),
173
- 'XPF' => Currency.new('XPF', subunit: 0, symbol: '₣')
174
- }
60
+ def self.load_currencies
61
+ path = File.expand_path('../data/currencies.yaml', __dir__)
62
+ YAML.load_file(path).each_with_object({}) do |(code, attrs), registry|
63
+ registry[code] = Currency.new(
64
+ code,
65
+ subunit: attrs['subunit'],
66
+ symbol: attrs['symbol'],
67
+ priority: attrs['priority']
68
+ )
69
+ end
175
70
  end
71
+
72
+ private_class_method :load_currencies
176
73
  end
@@ -3,10 +3,11 @@ module Mint
3
3
  # split and allocation methods
4
4
  class Money
5
5
  def allocate(proportions)
6
+ whole = proportions.sum.to_r
6
7
  raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
8
+ raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
7
9
 
8
- whole = proportions.sum.to_r
9
- amounts = proportions.map! { |rate| (amount * rate.to_r / whole).round(currency.subunit) }
10
+ amounts = proportions.map { |rate| (amount * rate.to_r / whole).round(currency.subunit) }
10
11
  allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
11
12
  end
12
13
 
@@ -29,7 +30,7 @@ module Mint
29
30
  last_slot = (left_over / minimum).to_i - 1
30
31
  (0..last_slot).each { |slot| amounts[slot] += minimum }
31
32
  end
32
- amounts.map { mint(it) }
33
+ amounts.map { mint it }
33
34
  end
34
35
  end
35
36
  end
@@ -13,12 +13,12 @@ module Mint
13
13
  end
14
14
 
15
15
  # @example
16
- # two_usd == Mint.money(2r, 'USD']) #=> [$ 2.00]
17
- # two_usd > 0 #=> true
18
- # two_usd > Mint.money(2, 'USD']) #=> true
16
+ # two_usd == Mint.money(2r, 'USD') #=> [$ 2.00]
17
+ # two_usd > 0 #=> true
18
+ # two_usd > Mint.money(2, 'USD') #=> false
19
19
  # two_usd > 1
20
20
  # => TypeError: [$ 2.00] can't be compared to 1
21
- # two_usd > Mint.money(2, 'BRL'])
21
+ # two_usd > Mint.money(2, 'BRL')
22
22
  # => TypeError: [$ 2.00] can't be compared to [R$ 2.00]
23
23
  #
24
24
  def <=>(other)
@@ -1,7 +1,11 @@
1
+ require 'erb'
2
+
1
3
  module Mint
2
4
  # Conversion logic
3
5
  class Money
4
6
  def to_d
7
+ raise NoMethodError, 'decimal gem required' unless defined?(BigDecimal)
8
+
5
9
  amount.to_d 0
6
10
  end
7
11
 
@@ -11,7 +15,8 @@ module Mint
11
15
 
12
16
  def to_html(format = DEFAULT_FORMAT)
13
17
  title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
14
- %(<data class='money' title='#{title}'>#{to_s(format: format)}</data>)
18
+ body = to_s(format: format)
19
+ %(<data class='money' title='#{ERB::Util.html_escape(title)}'>#{ERB::Util.html_escape(body)}</data>)
15
20
  end
16
21
 
17
22
  def to_i
@@ -29,18 +34,5 @@ module Mint
29
34
  def to_r
30
35
  amount
31
36
  end
32
-
33
- def to_s(format: '%<symbol>s%<amount>f', delimiter: false, separator: '.')
34
- format = format.gsub(/%<amount>(\+?\d*)f/,
35
- "%<amount>\\1.#{currency.subunit}f")
36
- formatted = Kernel.format(format, amount: amount, currency: currency_code,
37
- symbol: currency.symbol)
38
- if delimiter
39
- # Thanks Money gem for the regular expression
40
- formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{delimiter}")
41
- end
42
- formatted.tr!('.', separator) if separator != '.'
43
- formatted
44
- end
45
37
  end
46
38
  end
@@ -0,0 +1,68 @@
1
+ module Mint
2
+ # Formatting functionality for Money objects
3
+ class Money
4
+ # Formats money as a string with customizable format, thousand delimiter, and decimal
5
+ #
6
+ # @param format [String] Format string with placeholders: %<symbol>s, %<amount>f, %<currency>s
7
+ # @param thousand [String, false] Thousands delimiter (e.g., ',' for 1,000)
8
+ # @param decimal [String] Decimal separator (e.g., '.' or ',')
9
+ # @return [String] Formatted money string
10
+ #
11
+ # @example Basic formatting
12
+ # money = Mint.money(1234.56, 'USD')
13
+ # money.to_s #=> "$1,234.56"
14
+ # money.to_s(thousand: '.', decimal: ',') #=> "$.1234,56"
15
+ # money.to_s(decimal: ',', thousand: '') #=> "$1234,56"
16
+ #
17
+ # @example Custom formats
18
+ # money.to_s(format: '%<amount>f') #=> "1234.56"
19
+ # money.to_s(format: '%<currency>s %<amount>f') #=> "USD 1234.56"
20
+ # money.to_s(format: '%<amount>f %<symbol>s') #=> "1234.56 $"
21
+ # money.to_s(format: '%<symbol>s%<amount>+f') #=> "$+1234.56"
22
+ #
23
+ # @example Padding and alignment
24
+ # money.to_s(format: '%<amount>10.2f') #=> " 1234.56"
25
+ # money.to_s(format: '%<symbol>s%<amount>010.2f') #=> "$0001234.56"
26
+ #
27
+ def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
28
+ raise ArgumentError, 'Invalid format' unless format.is_a?(String) || format.is_a?(Hash)
29
+
30
+ formatted = format_amount(format)
31
+
32
+ formatted.tr!('.', decimal) if decimal != '.'
33
+
34
+ unless thousand.empty?
35
+ # Regular expression courtesy of Money gem
36
+ # Matches digits followed by groups of 3 digits until non-digit or end
37
+ formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{thousand}")
38
+ end
39
+
40
+ formatted = formatted.rjust(width) if width
41
+ formatted
42
+ end
43
+
44
+ def format_amount(format)
45
+ format = { positive: format } if format.is_a?(String)
46
+ value = amount
47
+
48
+ if amount.negative? && format[:negative]
49
+ format = format[:negative]
50
+ value = -amount
51
+ elsif amount.zero? && format[:zero]
52
+ format = format[:zero]
53
+ else
54
+ format = format[:positive]
55
+ end
56
+ format ||= '%<symbol>s%<amount>f'
57
+
58
+ # Automatically adjust decimal places based on currency subunit
59
+ adjusted_format = format.gsub(/%<amount>(\+?\d*)f/,
60
+ "%<amount>\\1.#{currency.subunit}f")
61
+
62
+ Kernel.format(adjusted_format,
63
+ amount: value,
64
+ currency: currency_code,
65
+ symbol: currency.symbol)
66
+ end
67
+ end
68
+ end
@@ -18,6 +18,7 @@ module Mint
18
18
 
19
19
  @amount = amount.to_r.round(currency.subunit)
20
20
  @currency = currency
21
+ freeze
21
22
  end
22
23
 
23
24
  def currency_code
@@ -25,7 +26,7 @@ module Mint
25
26
  end
26
27
 
27
28
  def hash
28
- @hash ||= zero? ? 0.hash : [amount, currency.code].hash
29
+ zero? ? 0.hash : [amount, currency.code].hash
29
30
  end
30
31
 
31
32
  # Returns a new Money object with the specified amount, or self if unchanged
@@ -0,0 +1,82 @@
1
+ module Mint
2
+ # nodoc
3
+ class Money
4
+ # Parses a human-readable money string into a {Money} object.
5
+ #
6
+ # @param input [String] Amount input, optionally including a currency symbol or code
7
+ # @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
8
+ # @return [Money]
9
+ # @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
10
+ #
11
+ # @example With explicit currency
12
+ # Money.parse('19.99', 'USD') #=> [USD 19.99]
13
+ # Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
14
+ #
15
+ # @example With symbol or code in the string
16
+ # Money.parse('$19.99') #=> [USD 19.99]
17
+ # Money.parse('19,99 €') #=> [EUR 19.99]
18
+ # Money.parse('USD 1,234.56') #=> [USD 1234.56]
19
+ def self.parse(input, currency = nil)
20
+ raise ArgumentError, 'input must be a String' unless input.is_a?(String)
21
+
22
+ input = input.strip
23
+ raise ArgumentError, 'input cannot be empty' if input.empty?
24
+
25
+ currency = currency ? Mint.currency(currency) : parse_currency(input)
26
+ raise ArgumentError, "Currency [#{currency}] not registered" unless currency
27
+
28
+ amount = parse_amount(input)
29
+ new(amount, currency)
30
+ end
31
+
32
+ # Extracts a numeric value from input that should only contain an amount.
33
+ def self.parse_amount(input)
34
+ # Remove any charater that is not a digit, comma or period
35
+ numeric = input.scan(/[\d.\-,]/).join
36
+ numeric = normalize_separators(numeric)
37
+ Rational(numeric)
38
+ end
39
+
40
+ # Converts locale-specific decimal/thousand separators into a plain decimal string.
41
+ def self.normalize_separators(numeric)
42
+ case [numeric.count(','), numeric.count('.')]
43
+ in [0, 0] | [0, 1] # Nothing to normalize (e.g. "1500" or "34.21").
44
+ numeric
45
+ in [1, 0] # Only one comma: decimal (e.g. 19,99 or 1,234).
46
+ numeric.tr(',', '.')
47
+ in [c, p] if c > 1 && p > 1 # Both separators appear multiple times
48
+ raise ArgumentError, "could not distinguish decimal and thousand separators in '#{numeric}'"
49
+ in [c, p] if c > 0 && p > 0 # Commas and dots: the rightmost one is the decimal separator.
50
+ if numeric.rindex(',') > numeric.rindex('.')
51
+ numeric.delete('.').tr(',', '.')
52
+ else
53
+ numeric.delete(',')
54
+ end
55
+ else # Multiple of the same separator only (e.g. 1,234,567) — all are thousands.
56
+ numeric.delete(',.')
57
+ end
58
+ end
59
+
60
+ def self.parse_currency(input)
61
+ # Prefer an explicit ISO 4217 code (e.g. "USD 1,234.56") over symbol matching.
62
+ code = input[/\b([A-Z]{3})\b/, 1]
63
+ if code
64
+ currency = Mint.currency(code)
65
+ return currency if currency
66
+ end
67
+
68
+ # Fall back to registered symbols, longest first (HK$ before $).
69
+ Mint.currency_symbols.each do |symbol, currency|
70
+ next if symbol.empty?
71
+
72
+ return currency if input.include?(symbol)
73
+ end
74
+
75
+ raise ArgumentError,
76
+ 'currency could not be detected; pass a currency code as the second argument'
77
+ end
78
+
79
+ private_class_method :parse_amount, :normalize_separators,
80
+ :parse_currency
81
+ end
82
+ end
data/lib/minting/money.rb CHANGED
@@ -1,6 +1,8 @@
1
+ require 'minting/money/parse'
1
2
  require 'minting/money/allocation'
2
3
  require 'minting/money/arithmetics'
3
4
  require 'minting/money/coercion'
4
5
  require 'minting/money/comparable'
5
6
  require 'minting/money/conversion'
7
+ require 'minting/money/formatting'
6
8
  require 'minting/money/money'
@@ -1,3 +1,3 @@
1
1
  module Minting
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
data/minting.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.required_ruby_version = '>= 3.2.0'
30
30
 
31
31
  s.files = Dir.glob('{bin,doc,lib}/**/*')
32
- s.files += %w[minting.gemspec Rakefile README.md]
32
+ s.files += %w[minting.gemspec Rakefile README.md LICENSE]
33
33
 
34
34
  s.bindir = 'bin'
35
35
  s.require_paths = ['lib']
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minting
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
@@ -15,11 +15,13 @@ executables: []
15
15
  extensions: []
16
16
  extra_rdoc_files: []
17
17
  files:
18
+ - LICENSE
18
19
  - README.md
19
20
  - Rakefile
20
21
  - bin/console
21
22
  - bin/setup
22
23
  - lib/minting.rb
24
+ - lib/minting/data/currencies.yaml
23
25
  - lib/minting/mint.rb
24
26
  - lib/minting/mint/currency.rb
25
27
  - lib/minting/mint/refinements.rb
@@ -30,7 +32,9 @@ files:
30
32
  - lib/minting/money/coercion.rb
31
33
  - lib/minting/money/comparable.rb
32
34
  - lib/minting/money/conversion.rb
35
+ - lib/minting/money/formatting.rb
33
36
  - lib/minting/money/money.rb
37
+ - lib/minting/money/parse.rb
34
38
  - lib/minting/version.rb
35
39
  - minting.gemspec
36
40
  homepage: https://github.com/gferraz/minting
@@ -57,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
61
  - !ruby/object:Gem::Version
58
62
  version: '0'
59
63
  requirements: []
60
- rubygems_version: 3.7.1
64
+ rubygems_version: 4.0.9
61
65
  specification_version: 4
62
66
  summary: Library to manipulate currency values
63
67
  test_files: []