minting 1.6.3 → 1.7.2

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -113
  3. data/Rakefile +25 -3
  4. data/bin/bench_check +46 -0
  5. data/doc/Mint/Currency.html +1139 -0
  6. data/doc/Mint/CurrencyRegistry.html +511 -0
  7. data/doc/Mint/Money.html +3859 -0
  8. data/doc/Mint/RangeStepPatch.html +277 -0
  9. data/doc/Mint/UnknownCurrency.html +136 -0
  10. data/doc/Mint.html +911 -0
  11. data/doc/Minting.html +142 -0
  12. data/doc/_index.html +173 -0
  13. data/doc/class_list.html +54 -0
  14. data/doc/css/common.css +1 -0
  15. data/doc/css/full_list.css +206 -0
  16. data/doc/css/style.css +1089 -0
  17. data/doc/file.README.html +275 -0
  18. data/doc/file_list.html +59 -0
  19. data/doc/frames.html +22 -0
  20. data/doc/index.html +275 -0
  21. data/doc/js/app.js +801 -0
  22. data/doc/js/full_list.js +334 -0
  23. data/doc/js/jquery.js +4 -0
  24. data/doc/method_list.html +518 -0
  25. data/doc/top-level-namespace.html +151 -0
  26. data/lib/minting/{mint/currency → currency}/currency.rb +8 -0
  27. data/lib/minting/{mint/currency → currency}/currency_registry.rb +1 -1
  28. data/lib/minting/{mint/currency → currency}/world_currencies.rb +1 -1
  29. data/lib/minting/mint/aliases.rb +3 -0
  30. data/lib/minting/mint/dsl/numeric.rb +23 -0
  31. data/lib/minting/mint/dsl/range.rb +67 -0
  32. data/lib/minting/mint/dsl/string.rb +12 -0
  33. data/lib/minting/mint/{dsl.rb → dsl/top_level.rb} +3 -18
  34. data/lib/minting/mint/mint.rb +17 -3
  35. data/lib/minting/mint/{parser.rb → parser/parser.rb} +17 -29
  36. data/lib/minting/mint/parser/separators.rb +39 -0
  37. data/lib/minting/mint.rb +19 -8
  38. data/lib/minting/money/allocation/allocation.rb +25 -0
  39. data/lib/minting/money/allocation/split.rb +41 -0
  40. data/lib/minting/money/arithmetics/methods.rb +27 -0
  41. data/lib/minting/money/{arithmetics.rb → arithmetics/operators.rb} +5 -26
  42. data/lib/minting/money/clamp.rb +66 -0
  43. data/lib/minting/money/coercion.rb +18 -11
  44. data/lib/minting/money/comparable.rb +6 -0
  45. data/lib/minting/money/constructors.rb +13 -3
  46. data/lib/minting/money/format/formatting.rb +44 -0
  47. data/lib/minting/money/{formatting.rb → format/to_s.rb} +5 -42
  48. data/lib/minting/money/money.rb +0 -58
  49. data/lib/minting/version.rb +1 -1
  50. data/minting.gemspec +5 -2
  51. metadata +42 -11
  52. data/lib/minting/money/allocation.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66e576bf9b9ddb1ff390e5e5ecda0f73800b3ab8577baec9b03cc6f9962915d3
4
- data.tar.gz: f43875174cd1ab47a0da044e4f877dd6618f5850e363d1f505484a6874346f7e
3
+ metadata.gz: 849f4b46ee6f731be672f7e6adb274174233294d7be293c8c875eaa0ace5b744
4
+ data.tar.gz: b3f3a822c55e7fd9fdddcfcf7be59069b66ea441d73ab61e2e202563173a8675
5
5
  SHA512:
6
- metadata.gz: 586c212f24ddfac8b7fb2a619aac93d02af67588c05d2ff222aca680970b80b3aa65998e175d68250d0352843794feee751dcd5da1edb7965318079bbf21d9ab
7
- data.tar.gz: b289c844b36dc08099f1ae3ea963ef689e6cd876060d372e185a7f10cd1f3a0f6ec59ae943aacc03598becd869eb00a0402ecebad02d175e1070d3817d13b770
6
+ metadata.gz: 5f4d42d03f31c941fcd591e27c463341be2c31498c7d854c3b98f85445a8ca55dfa692c3124f187000c83b97b0abc21e6e7276ea5bb3cd1090e82619e6fcc451
7
+ data.tar.gz: '0854273f42d05dee2b2d91830110d9f59074821abd81c76a14860a0969c4bd2cd48c56164c5fd2b65fcdfe32f537860958b108cd53eadfc53a2c33215f35c5cd'
data/README.md CHANGED
@@ -3,18 +3,10 @@
3
3
  Fast, precise, and developer-friendly money handling for Ruby.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/minting.svg)](https://badge.fury.io/rb/minting)
6
-
7
- ## Why Minting?
8
-
9
- **Tired of floating-point errors in financial calculations?** Minting uses Rational numbers for perfect precision.
10
-
11
- **Need performance?** Minting is 2× faster than alternatives for high-volume operations (often 10×+ for formatting). See the [Performance](https://github.com/gferraz/minting/blob/master/test/performance/README.md) section for full benchmarks.
12
-
13
- **Want a clean API?** Minting provides an intuitive interface with helpful error messages.
14
-
15
- **Looking for a proven alternative?** Check out the established [Money gem](https://github.com/RubyMoney/money) with thousands of stars on GitHub.
16
-
17
- **Rails**? Use the [minting-rails](https://github.com/gferraz/minting-rails) companion gem
6
+ [![CI](https://github.com/gferraz/minting/actions/workflows/ci.yml/badge.svg)](https://github.com/gferraz/minting/actions/workflows/ci.yml)
7
+ [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/gferraz/minting)
8
+ [![RubyCritic](https://img.shields.io/badge/RubyCritic-94gem /100-brightgreen)](https://github.com/gferraz/minting)
9
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue)](https://www.rubydoc.info/gems/minting/frames)
18
10
 
19
11
  ## Quick start
20
12
 
@@ -29,16 +21,44 @@ total.to_s #=> "$21.59"
29
21
  total.currency_code #=> "USD"
30
22
  ```
31
23
 
32
- ## Features
24
+ ## Why Minting?
25
+
26
+ | | Minting |
27
+ |--------------------|-------------------------------------------|
28
+ | **Precision** | Rational-based, zero floating-point error |
29
+ | **Performance** | **2× faster** (10×+ formatting) |
30
+ | **Ruby support** | 3.3+ (including Ruby 4.0) |
31
+ | **Rails** | Dedicated companion gem |
32
+ | **Code quality** | 100% coverage, 93/100 RubyCritic |
33
+
34
+ ### 🎯 Exact precision
35
+ Amounts are stored as `Rational` and rounded to the currency subunit. No floating-point surprises, ever.
36
+
37
+ ### ⚡ Blazing performance
38
+ Minting is **2× faster** than the Money gem for everyday operations and **over 10× faster for formatting**. See full benchmarks in the [Performance Guide](test/performance/README.md).
39
+
40
+ ### 🧼 Clean, modern API
41
+ Intuitive interface, descriptive error messages, and sensible defaults. Works the way you expect.
42
+
43
+ ### 🚆 Rails-ready
44
+ Use with the [minting-rails](https://github.com/gferraz/minting-rails) companion gem for drop-in ActiveRecord type casting, validators, and form helpers.
45
+
46
+ ### 🏆 Quality you can trust
47
+ - **100% test coverage** — every line exercised
48
+ - **93/100 RubyCritic score** — clean, maintainable code
49
+ - **CI-tested on Ruby 3.3 and 4.0**
50
+
51
+ ## Installation
52
+
53
+ ```shell
54
+ bundle add minting
55
+ ```
56
+
57
+ Or add to your Gemfile:
33
58
 
34
- - Arithmetic: `+ - * /`, unary minus, `abs`
35
- - Comparisons: `==`, `<=>`, `zero?`, `nonzero?`, `positive?`, `negative?`
36
- - Formatting: `to_s` with custom formats, thousand delimiters and decimal separators
37
- - Serialization: `to_json`, `to_i`, `to_f`, `to_r`, `to_d`
38
- - Allocation utilities: `split(quantity)`, `allocate([ratios])`,
39
- - Utilities: `clamp(min, max)`
40
- - Numeric Refinements for ergonomics: `10.dollars`, `3.euros`, `4.to_money('USD')`
41
- - Currency registry with 117+ currencies and custom registration
59
+ ```ruby
60
+ gem 'minting'
61
+ ```
42
62
 
43
63
  ## Usage
44
64
 
@@ -61,16 +81,15 @@ ten == Mint.money(10, 'EUR') #=> false
61
81
  ten > Mint.money(9.99, 'USD') #=> true
62
82
 
63
83
  # Zero equality semantics
64
- # Any zero amount is treated as equal, regardless of currency
65
- Mint.money(0, 'USD') == Mint.money(0, 'EUR') #=> true
84
+ # Any zero amount is treated as equal, regardless of currency
85
+ Mint.money(0, 'USD') == Mint.money(0, 'EUR') #=> true
66
86
  Mint.money(0, 'USD') == 0 #=> true
67
87
  Mint.money(0, 'USD') == 0.0 #=> true
68
- Mint.money(0, 'USD') == 0r #=> true
69
88
 
70
89
  # Non-zero numerics are not equal to Money objects
71
90
  Mint.money(10, 'USD') == 10 #=> false
72
91
 
73
- # Format (uses Kernel.format internally)
92
+ # Format (uses Kernel.format syntax)
74
93
  price = Mint.money(9.99, 'USD')
75
94
 
76
95
  price.to_s #=> "$9.99",
@@ -136,128 +155,67 @@ price.clamp(min_price, 100) #=> [USD 75.00]
136
155
 
137
156
  ```
138
157
 
139
- ## API notes
140
-
141
- **Module names** — Require the `minting` gem; the public API lives under `Mint`.
142
-
143
- **Exact amounts** — Amounts are stored as `Rational` and rounded to the currency subunit.
144
-
145
- **Refinements** — `10.dollars` and similar helpers require `using Mint` in the current scope (see Usage above).
146
-
147
- **Division** — `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
148
-
149
- **Zero equality** — Any zero amount is considered equal across currencies and to numeric zero `Mint.money(0, 'USD') == Mint.money(0, 'EUR')` is intentionally `true`. Non-zero amounts must match currency and value.
150
-
151
- **Custom currencies** — `Mint.register_currency`, Only registered currency codes and symbolos are recoginized by the parser.
152
-
153
- **Built-in currencies** — ISO-style codes ship in `lib/minting/data/currencies.yaml` and load when the registry is first accessed.
154
-
155
- ## Installation
156
-
157
- Option 1: Via bundler command
158
-
159
- ```shell
160
- bundle add minting
161
- bundle install
162
- ```
163
-
164
- Option 2: add the line below to your application's Gemfile:
165
-
166
- ```ruby
167
- gem 'minting'
168
- ```
169
-
170
- or, if you want latest development version from Github
158
+ ## Parsing strings
171
159
 
172
160
  ```ruby
173
- gem 'minting', git: 'https://github.com/gferraz/minting.git'
174
- ```
175
-
176
- and execute:
177
-
178
- ```shell
179
- bundle install
161
+ Mint.parse('$19.99') #=> [USD 19.99]
162
+ Mint.parse('19,99 €') #=> [EUR 19.99]
163
+ Mint.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
164
+ Mint.parse('USD 1,234.56') #=> [USD 1234.56]
180
165
  ```
181
166
 
182
- Option 3: Install it yourself with:
167
+ Notes:
168
+ - Pass a currency code when the string has no symbol or code.
169
+ - `1,234` means 1234, not 1.234 and `1,23` means 1.23, not 123
170
+ - `1,234.00` is unambiguous (thousands + decimal).
171
+ - Accounting negatives like `($1.23)` are unsupported for now.
172
+ - Ambiguous symbols like `$` resolve by currency priority (currently USD).
173
+ - The parser scans all uppercase words for registered codes, so spurious non-currency words before the real code are correctly ignored: `Mint.parse("MAX 10.00 USD")` yields `[USD 10.00]`.
183
174
 
184
- ```shell
185
- gem install minting
186
- ```
175
+ ## API notes
187
176
 
188
- ## Configuration
177
+ **Exact amounts** — Amounts are stored as `Rational` and rounded to the currency subunit.
189
178
 
190
- ### Optional top‑level `Money` and `Currency`
179
+ **Refinements** `10.dollars` and similar helpers require `using Mint` in the current scope (see Usage above).
191
180
 
192
- By default, Minting keeps everything namespaced under `Mint`:
181
+ **Division** `money / 5` returns new `Money`; `money / other_money` returns a numeric ratio, not money.
193
182
 
194
- ```ruby
195
- price = Mint::Money.create(10, "USD")
196
- currency = Mint::Currency.new(code: "USD", symbol: "$", subunit: 2, priority: 0)
197
- ```
183
+ **Zero equality** — Any zero amount is considered equal across currencies and to numeric zero (`Mint.money(0, 'USD') == Mint.money(0, 'EUR')` is intentionally `true`). Non-zero amounts must match currency and value.
198
184
 
199
- To avoid polluting the global namespace (and to coexist nicely with other gems), **Minting dont automatically defines `Money` or `Currency` at the top level automatically**.
185
+ **Registered currencies** `Mint.register_currency`. Only registered currency codes and symbols are recognized by the parser.
200
186
 
201
- If you prefer the shorter `Money` / `Currency` constants in your application code, you can opt in explicitly.
187
+ **Built-in currencies** 150+ ISO-4217 world currencies ship in `lib/minting/data/currencies.yaml` and load when the registry is first accessed.
202
188
 
203
- There are two ways to enable shorter constants:
189
+ ## Optional top-level `Money` and `Currency`
204
190
 
205
- 1. Require dsl in your app
191
+ By default, Minting keeps everything namespaced under `Mint` to coexist nicely with other gems. If you prefer shorter constants, opt in:
206
192
 
207
193
  ```ruby
208
194
  require "minting"
209
195
  require "minting/dsl" # opt‑in top‑level Money / Currency
210
196
  ```
211
197
 
212
- 2. Call a configuration method
198
+ Or at runtime:
213
199
 
214
200
  ```ruby
215
201
  Minting.use_top_level_constants!
216
202
  ```
217
203
 
218
- After this, you can use:
204
+ After opting in:
219
205
 
220
206
  ```ruby
221
207
  price = Money.create(10, "USD") # equivalent to Mint::Money.create
222
- tax = Money.money(2.50, "USD") # via Mint.money, still available
208
+ tax = Money.money(2.50, "USD")
223
209
  cur = Currency.new(code: "EUR", symbol: "€", subunit: 2, priority: 0)
224
210
  ```
225
211
 
226
- #### When to use this
227
-
228
- - **Good fit:** application code, especially in Rails apps, where `Money` reads nicely in models and views.
229
- - **Not recommended:** reusable gems/libraries. In that case, stick to the namespaced API (`Mint::Money`, `Mint::Currency`) to avoid conflicts with other libraries.
230
-
231
- ## Parsing strings
232
-
233
- ```ruby
234
- Mint.parse('$19.99') #=> [USD 19.99]
235
- Mint.parse('19,99 €') #=> [EUR 19.99]
236
- Mint.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
237
- Mint.parse('USD 1,234.56') #=> [USD 1234.56]
238
- ```
239
-
240
- - Pass a currency code when the string has no symbol or code.
241
- - 1,234 means 1.234, not 1234, because one comma is treated as decimal.
242
- - 1,234.00 is unambiguous thousands-plus-decimal.
243
- - accounting negatives like ($1.23) are unsupported.
244
- - ambiguous symbols like $ resolve by priority, currently USD.
212
+ **Good fit:** Application code, especially Rails apps.
213
+ **Not recommended:** Reusable gems/libraries — stick to `Mint::Money` to avoid conflicts.
245
214
 
246
215
  ## Roadmap
247
216
 
248
- - Improve formatting features
249
217
  - Localization (I18n-aware formatting)
250
- - Basic exchange-rate conversions
251
-
252
- ## Contributing
253
-
254
- Bug reports and pull requests are welcome on GitHub at <https://github.com/gferraz/minting>.
255
-
256
- 1. Fork and create a feature branch
257
- 2. Run the test suite: `rake`
258
- 3. Run performance suites as needed: `rake bench:performance`
259
- 4. Open a PR with a clear description and benchmarks if relevant
260
-
218
+ - Exchange-rate conversion infrastructure
261
219
 
262
220
  ## License
263
221
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
+ require 'bundler/audit/task'
1
2
  require 'bundler/gem_tasks'
2
- require 'rubocop/rake_task'
3
3
  require 'rake/testtask'
4
+ require 'rubocop/rake_task'
5
+ require 'rubycritic/rake_task'
4
6
  require 'yard'
5
7
 
6
8
  CLOBBER.include %w[doc/css doc/js doc/Mint doc/*.html tmp .yardoc]
@@ -37,11 +39,31 @@ Rake::TestTask.new('bench:competitive') do |t|
37
39
  t.pattern = 'test/performance/competitive/**/*_benchmark.rb'
38
40
  end
39
41
 
40
- RuboCop::RakeTask.new(:cop)
42
+ desc 'Run core benchmarks and update the baseline'
43
+ task 'bench:baseline' do
44
+ platform = RUBY_PLATFORM
45
+ baseline = "test/performance/check/results/baseline-#{platform}.json"
46
+ sh "ruby test/performance/check/runner.rb #{baseline}"
47
+ puts "Baseline updated for #{platform}."
48
+ end
49
+
50
+ desc 'Run core benchmarks and check for regressions against the baseline'
51
+ task 'bench:check' do
52
+ ruby 'bin/bench_check'
53
+ end
54
+
55
+ Bundler::Audit::Task.new
56
+
57
+ RuboCop::RakeTask.new(:cop) do |task|
58
+ task.patterns = ['lib']
59
+ end
60
+
61
+ RubyCritic::RakeTask.new do |task|
62
+ task.name = 'critic'
63
+ end
41
64
 
42
65
  YARD::Rake::YardocTask.new do |t|
43
66
  t.files = ['lib/**/*.rb']
44
- t.options = ['-o doc/api'] # Place the documentos in doc/api
45
67
  t.stats_options = ['--list-undoc']
46
68
  end
47
69
 
data/bin/bench_check ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require 'tempfile'
6
+
7
+ platform = RUBY_PLATFORM
8
+ results_dir = File.expand_path('../test/performance/check/results', __dir__)
9
+ baseline_path = File.join(results_dir, "baseline-#{platform}.json")
10
+ threshold = (ARGV[0] || 0.80).to_f
11
+
12
+ unless File.exist?(baseline_path)
13
+ puts "No baseline for #{platform} at #{baseline_path}. Skipping check."
14
+ exit 0
15
+ end
16
+
17
+ baseline = JSON.parse(File.read(baseline_path))
18
+ current_json = Tempfile.new(%w[bench_current .json])
19
+
20
+ unless system("bundle exec ruby test/performance/check/runner.rb #{current_json.path}")
21
+ puts 'Benchmark runner failed.'
22
+ exit 1
23
+ end
24
+
25
+ current = JSON.parse(File.read(current_json.path))
26
+
27
+ failures = []
28
+ baseline['results'].each do |key, b|
29
+ c = current.dig('results', key)
30
+ next unless c
31
+
32
+ ratio = c['ips'].to_f / b['ips']
33
+ if ratio < threshold
34
+ failures << format('%<key>-15s baseline: %<b>10d ips current: %<c>10d ips ratio: %<ratio>5.1f%%',
35
+ key:, b: b['ips'], c: c['ips'], ratio: ratio * 100)
36
+ end
37
+ end
38
+
39
+ puts "\n--- Benchmark Regression Check ---"
40
+ if failures.empty?
41
+ puts 'All benchmarks within 20% of baseline.'
42
+ else
43
+ puts 'Regressions detected:'
44
+ failures.each { |f| puts " FAIL #{f}" }
45
+ exit 1
46
+ end