amounts 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +26 -1
- data/lib/amount/active_record/railtie.rb +15 -0
- data/lib/amount/display.rb +24 -7
- data/lib/amount/registry.rb +5 -2
- data/lib/amount/version.rb +1 -1
- data/lib/amounts.rb +12 -0
- data/lib/generators/amount/active_record/registry_generator.rb +60 -0
- data/lib/generators/amount/active_record/templates/presets/all.fragment +494 -0
- data/lib/generators/amount/active_record/templates/presets/crypto.fragment +180 -0
- data/lib/generators/amount/active_record/templates/presets/fiat.fragment +163 -0
- data/lib/generators/amount/active_record/templates/presets/metals.fragment +151 -0
- data/lib/generators/amount/active_record/templates/registry.rb.tt +33 -0
- data/test/test_amount.rb +49 -0
- data/test/test_registry_generator.rb +104 -0
- metadata +14 -6
- data/test/dummy/log/development.log +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01ce6cdf1c52884bc594317df4afa1debc6cfd90c1a6cbef0177e0ed8e2efe63
|
|
4
|
+
data.tar.gz: f796211c1f43214d5f8dc6f72d63715b4c901d96c9a4bed30e05220743740fee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebbf7e4ff014146a29243ae8dbd857a1833e09e6c5f3e4f124da382663ef0941346a4467a30815aa99ea0b1aa2fa9ace12c799dbf8456b63dbdf4b2f6ab6e5c1
|
|
7
|
+
data.tar.gz: 7e177014e0ad23a42cee97c6ae29f1d5c858426b211c1f63ebb3f23871c2e5aceb06edab03feee2efe26b4cf075c3d214e967d18882d3c93aff41226157ee6b9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.6 - 2026-04-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `trim_zeros` option for `Amount.register` strips trailing zeros from `ui`
|
|
8
|
+
output. Defaults to `false` so fiat currencies keep fixed decimals (`$1.50`).
|
|
9
|
+
Set `true` for tokens with high precision to get clean output (`1.5 SOL`
|
|
10
|
+
instead of `1.5000 SOL`). Display units can override via
|
|
11
|
+
`display_units: { gram: { ..., trim_zeros: false } }`.
|
|
12
|
+
- `Amount#ui(trim_zeros:)` call-site override. Pass `true` or `false` to
|
|
13
|
+
override the registry and display-unit settings for a single render.
|
|
14
|
+
Precedence: call-site > display unit spec > registry default.
|
|
15
|
+
- Railtie `initializer` block that auto-requires `amount/active_record` in
|
|
16
|
+
Rails apps. `has_amount` and `t.amount` are now available without manual
|
|
17
|
+
`require` statements — Bundler's auto-require of the `amounts` gem handles
|
|
18
|
+
everything.
|
|
19
|
+
|
|
3
20
|
## 0.0.5 - 2026-04-26
|
|
4
21
|
|
|
5
22
|
### Added
|
data/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
> **You are reading the gem package readme.** The repository also contains
|
|
2
|
+
> [`site/`](../site) (the docs site) and [`demo/`](../demo) (a Rails
|
|
3
|
+
> case-study app that exercises every feature). See the
|
|
4
|
+
> [root README](../README.md) for the layout overview.
|
|
5
|
+
|
|
1
6
|
# amounts
|
|
2
7
|
|
|
3
8
|
[](https://rubygems.org/gems/amounts)
|
|
4
9
|
[](https://github.com/zarpay/amounts/actions/workflows/ci.yml)
|
|
5
10
|
[](https://github.com/zarpay/amounts/releases)
|
|
6
|
-
[](https://github.com/zarpay/amounts/blob/main/LICENSE.txt)
|
|
11
|
+
[](https://github.com/zarpay/amounts/blob/main/gem/LICENSE.txt)
|
|
7
12
|
[](https://rubygems.org/gems/amounts)
|
|
8
13
|
|
|
9
14
|
`amounts` is a Ruby gem for precise quantities of fungible things: money, crypto tokens, commodities, inventory units, points, and similar value-like amounts. It stores every value as an arbitrary-precision atomic `Integer`, keeps type identity in a registry, rejects accidental cross-type math unless an explicit directional rate exists, and offers an optional ActiveRecord adapter without making Rails part of the core runtime.
|
|
@@ -32,6 +37,10 @@ Load the Rails adapter only when needed:
|
|
|
32
37
|
require "amount/active_record"
|
|
33
38
|
```
|
|
34
39
|
|
|
40
|
+
If the gem is bundled into a Rails app, the Rails-only generator hooks are
|
|
41
|
+
loaded through the adapter railtie. You do not need a separate top-level
|
|
42
|
+
generator require.
|
|
43
|
+
|
|
35
44
|
## Quickstart
|
|
36
45
|
|
|
37
46
|
```ruby
|
|
@@ -235,6 +244,22 @@ Load the adapter explicitly:
|
|
|
235
244
|
require "amount/active_record"
|
|
236
245
|
```
|
|
237
246
|
|
|
247
|
+
### Preset registry generator
|
|
248
|
+
|
|
249
|
+
In a Rails app, you can generate a starter initializer with curated baskets of
|
|
250
|
+
registered symbols:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
bin/rails generate amounts:registry fiat
|
|
254
|
+
bin/rails generate amounts:registry metals
|
|
255
|
+
bin/rails generate amounts:registry crypto
|
|
256
|
+
bin/rails generate amounts:registry all
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The generator writes `config/initializers/amounts.rb` with registration
|
|
260
|
+
metadata only. It intentionally does not register default conversion rates,
|
|
261
|
+
because those are directional and application-specific.
|
|
262
|
+
|
|
238
263
|
### Migration DSL
|
|
239
264
|
|
|
240
265
|
```ruby
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Amount
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
initializer "amount.active_record" do
|
|
7
|
+
require "amount/active_record"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
generators do
|
|
11
|
+
require_relative "../../../generators/amount/active_record/registry_generator"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/amount/display.rb
CHANGED
|
@@ -21,17 +21,22 @@ class Amount
|
|
|
21
21
|
# @param decorated [Boolean] when `false`, omit the display symbol and
|
|
22
22
|
# return just the rounded number. Useful when the caller renders the
|
|
23
23
|
# currency label separately (e.g. in a column header or a chip).
|
|
24
|
+
# @param trim_zeros [Boolean, nil] strip trailing zeros from the formatted
|
|
25
|
+
# number. When `nil` (the default), falls back to the display unit's
|
|
26
|
+
# setting, then the registry entry's setting. An explicit `true` or
|
|
27
|
+
# `false` overrides both.
|
|
24
28
|
# @return [String]
|
|
25
29
|
# @example
|
|
26
30
|
# Amount.usdc("1.50").ui # => "$1.50"
|
|
27
31
|
# Amount.usdc("1.50").ui(decorated: false) # => "1.50"
|
|
28
32
|
# Amount.gold("1").ui(unit: :gram) # => "31.10 g"
|
|
29
33
|
# Amount.gold("1").ui(unit: :gram, decorated: false) # => "31.10"
|
|
30
|
-
|
|
34
|
+
# Amount.sol("2.5").ui(trim_zeros: true) # => "2.5 SOL"
|
|
35
|
+
def ui(unit: nil, direction: :floor, decorated: true, trim_zeros: nil)
|
|
31
36
|
if unit
|
|
32
|
-
render_display_unit(unit, direction, decorated:)
|
|
37
|
+
render_display_unit(unit, direction, decorated:, trim_zeros:)
|
|
33
38
|
else
|
|
34
|
-
render_default(direction, decorated:)
|
|
39
|
+
render_default(direction, decorated:, trim_zeros:)
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
|
|
@@ -49,20 +54,20 @@ class Amount
|
|
|
49
54
|
|
|
50
55
|
private
|
|
51
56
|
|
|
52
|
-
def render_default(direction, decorated:)
|
|
57
|
+
def render_default(direction, decorated:, trim_zeros:)
|
|
53
58
|
rounded = round(@amount.decimal, @entry.ui_decimals, direction)
|
|
54
|
-
formatted =
|
|
59
|
+
formatted = format_number(rounded, @entry.ui_decimals, resolve_trim(trim_zeros))
|
|
55
60
|
return formatted unless decorated
|
|
56
61
|
|
|
57
62
|
apply_symbol(formatted, @entry.display_symbol, @entry.display_position)
|
|
58
63
|
end
|
|
59
64
|
|
|
60
|
-
def render_display_unit(unit, direction, decorated:)
|
|
65
|
+
def render_display_unit(unit, direction, decorated:, trim_zeros:)
|
|
61
66
|
spec = fetch_display_unit(unit)
|
|
62
67
|
scaled = @amount.decimal * Amount.coerce_decimal(spec[:scale])
|
|
63
68
|
decimals = spec[:ui_decimals] || @entry.ui_decimals
|
|
64
69
|
rounded = round(scaled, decimals, direction)
|
|
65
|
-
formatted =
|
|
70
|
+
formatted = format_number(rounded, decimals, resolve_trim(trim_zeros, spec))
|
|
66
71
|
return formatted unless decorated
|
|
67
72
|
|
|
68
73
|
apply_symbol(
|
|
@@ -90,6 +95,18 @@ class Amount
|
|
|
90
95
|
truncated / factor
|
|
91
96
|
end
|
|
92
97
|
|
|
98
|
+
def format_number(value, decimals, trim)
|
|
99
|
+
str = format("%.#{decimals}f", value)
|
|
100
|
+
trim ? str.sub(/(\.\d*?)0+\z/, '\1').chomp('.') : str
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def resolve_trim(call_site, spec = nil)
|
|
104
|
+
return call_site unless call_site.nil?
|
|
105
|
+
return spec[:trim_zeros] if spec&.key?(:trim_zeros)
|
|
106
|
+
|
|
107
|
+
@entry.trim_zeros
|
|
108
|
+
end
|
|
109
|
+
|
|
93
110
|
def apply_symbol(str, symbol, position)
|
|
94
111
|
return str if symbol.nil? || symbol.empty?
|
|
95
112
|
|
data/lib/amount/registry.rb
CHANGED
|
@@ -35,6 +35,7 @@ class Amount
|
|
|
35
35
|
:display_units,
|
|
36
36
|
:default_display,
|
|
37
37
|
:amount_class,
|
|
38
|
+
:trim_zeros,
|
|
38
39
|
keyword_init: true
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -60,6 +61,7 @@ class Amount
|
|
|
60
61
|
# @param ui_decimals [Integer] decimals displayed by default UI formatting
|
|
61
62
|
# @param display_units [Hash, nil] optional display-only scaling definitions
|
|
62
63
|
# @param default_display [Symbol, nil] optional default display unit key
|
|
64
|
+
# @param trim_zeros [Boolean] strip trailing zeros from UI output
|
|
63
65
|
# @param class [Class, nil] optional custom `Amount` subclass
|
|
64
66
|
# @return [void]
|
|
65
67
|
# @raise [AlreadyRegistered] if the symbol is already registered or the
|
|
@@ -73,7 +75,7 @@ class Amount
|
|
|
73
75
|
# ui_decimals: 2
|
|
74
76
|
def register(symbol, decimals:, display_symbol: symbol.to_s, display_position: :suffix,
|
|
75
77
|
ui_decimals: decimals, display_units: nil, default_display: nil,
|
|
76
|
-
class: nil)
|
|
78
|
+
trim_zeros: false, class: nil)
|
|
77
79
|
raise ArgumentError, "symbol must not be blank" if symbol.nil? || symbol.to_s.empty?
|
|
78
80
|
|
|
79
81
|
symbol = symbol.to_sym
|
|
@@ -92,7 +94,8 @@ class Amount
|
|
|
92
94
|
ui_decimals:,
|
|
93
95
|
display_units:,
|
|
94
96
|
default_display:,
|
|
95
|
-
amount_class: binding.local_variable_get(:class) || Amount
|
|
97
|
+
amount_class: binding.local_variable_get(:class) || Amount,
|
|
98
|
+
trim_zeros:
|
|
96
99
|
)
|
|
97
100
|
|
|
98
101
|
@entries[symbol] = entry
|
data/lib/amount/version.rb
CHANGED
data/lib/amounts.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Thin gem-name entrypoint for Bundler auto-require.
|
|
4
|
+
# Core users should still require "amount" directly.
|
|
5
|
+
require_relative "amount"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require "rails/railtie"
|
|
9
|
+
require_relative "amount/active_record/railtie"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
class Amount
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
class RegistryGenerator < ::Rails::Generators::Base
|
|
8
|
+
namespace "amounts:registry"
|
|
9
|
+
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
VALID_PRESETS = %i[fiat metals crypto all].freeze
|
|
12
|
+
|
|
13
|
+
argument :preset, type: :string, banner: "fiat|metals|crypto|all"
|
|
14
|
+
|
|
15
|
+
def create_initializer
|
|
16
|
+
template "registry.rb.tt", "config/initializers/amounts.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def preset_key
|
|
22
|
+
@preset_key ||= begin
|
|
23
|
+
key = preset.to_s.downcase.to_sym
|
|
24
|
+
if VALID_PRESETS.include?(key)
|
|
25
|
+
key
|
|
26
|
+
else
|
|
27
|
+
raise ::Thor::Error,
|
|
28
|
+
"unknown preset #{preset.inspect}; choose one of: fiat, metals, crypto, all"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def preset_categories
|
|
34
|
+
preset_key == :all ? %i[fiat metals crypto] : [preset_key]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def preset_name
|
|
38
|
+
preset_key.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def registered_symbols
|
|
42
|
+
@registered_symbols ||= preset_body.scan(/Amount\.register :([A-Z0-9_]+),/).flatten
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def registry_guard_symbol
|
|
46
|
+
registered_symbols.first
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def preset_body
|
|
50
|
+
@preset_body ||= File.read(
|
|
51
|
+
File.join(self.class.source_root, "presets", "#{preset_name}.fragment")
|
|
52
|
+
).chomp
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def preset_source_label
|
|
56
|
+
preset_name
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|