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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b83ca37936b92cf689a865d0173dafd36e354f5e058fab07983efa436796b5d6
4
- data.tar.gz: 3071b5b4bb06f46511a8dc45ec8d0f888b0863d61cfdbda363b313d806fb2f8a
3
+ metadata.gz: 01ce6cdf1c52884bc594317df4afa1debc6cfd90c1a6cbef0177e0ed8e2efe63
4
+ data.tar.gz: f796211c1f43214d5f8dc6f72d63715b4c901d96c9a4bed30e05220743740fee
5
5
  SHA512:
6
- metadata.gz: 398502b610a4e35059307a3f2e895f119ba8456bdfeb22cf95a0976d37b21fcbbedc8f0ff201b90efa6faf5b9cb1f18e9dae2209cc838f8f6cc3929ea73d4d81
7
- data.tar.gz: d26be5a7c85fd002ddc3435880d25bebae7b7a29007295b064efb2bcfe0eacb0d3959044c499db1621e7e8e1b238b311c546dc24c591e8ccfc02b61e4865e281
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
  [![Gem Version](https://img.shields.io/gem/v/amounts)](https://rubygems.org/gems/amounts)
4
9
  [![CI](https://github.com/zarpay/amounts/actions/workflows/ci.yml/badge.svg)](https://github.com/zarpay/amounts/actions/workflows/ci.yml)
5
10
  [![Release](https://img.shields.io/github/v/release/zarpay/amounts)](https://github.com/zarpay/amounts/releases)
6
- [![License](https://img.shields.io/github/license/zarpay/amounts)](https://github.com/zarpay/amounts/blob/main/LICENSE.txt)
11
+ [![License](https://img.shields.io/github/license/zarpay/amounts)](https://github.com/zarpay/amounts/blob/main/gem/LICENSE.txt)
7
12
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.1-red)](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
@@ -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
- def ui(unit: nil, direction: :floor, decorated: true)
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 = format("%.#{@entry.ui_decimals}f", rounded)
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 = format("%.#{decimals}f", rounded)
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Amount
4
- VERSION = "0.0.5"
4
+ VERSION = "0.0.6"
5
5
  end
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