amounts 0.0.4 → 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: 8b7620b025be07f1166e655108c2b9cea0232acb60d814cf91b71ecb47ddf1fb
4
- data.tar.gz: 8f8ab43608f4ced54ab5020c7cbcf69b0741c9203e549558b27356766187efb5
3
+ metadata.gz: 01ce6cdf1c52884bc594317df4afa1debc6cfd90c1a6cbef0177e0ed8e2efe63
4
+ data.tar.gz: f796211c1f43214d5f8dc6f72d63715b4c901d96c9a4bed30e05220743740fee
5
5
  SHA512:
6
- metadata.gz: 466350a8fbfff01e84bd044f23ced9d63288eb4147f13d70df2a3db9b37745cafee46e03cb3d1d9b0b805fff8ece3ca13df029c19786204f21e2fe1336b1e564
7
- data.tar.gz: 807ba283121158491de02a78084dae5ce12c6360ac106a7d72a1bde6a7f536ac3009fdf329b9e75972fe3631ade8bdae3dd0f2d1f2d2e74f9a023f28d46f5cb1
6
+ metadata.gz: ebbf7e4ff014146a29243ae8dbd857a1833e09e6c5f3e4f124da382663ef0941346a4467a30815aa99ea0b1aa2fa9ace12c799dbf8456b63dbdf4b2f6ab6e5c1
7
+ data.tar.gz: 7e177014e0ad23a42cee97c6ae29f1d5c858426b211c1f63ebb3f23871c2e5aceb06edab03feee2efe26b4cf075c3d214e967d18882d3c93aff41226157ee6b9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
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
+
20
+ ## 0.0.5 - 2026-04-26
21
+
22
+ ### Added
23
+
24
+ - `Amount#ui(decorated: false)` returns the rounded UI value as a plain
25
+ numeric string without the `display_symbol` prefix or suffix. Useful when
26
+ the caller renders the currency label separately (e.g. in a column header
27
+ or chip). Composes with `unit:` and `direction:` — for example,
28
+ `Amount.gold("1").ui(unit: :gram, decorated: false)` returns `"31.10"`.
29
+ Default remains `decorated: true`, so existing callers see no change.
30
+
3
31
  ## 0.0.4 - 2026-04-26
4
32
 
5
33
  ### Changed (breaking)
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
@@ -18,9 +18,26 @@ class Amount
18
18
 
19
19
  # @param unit [Symbol, nil]
20
20
  # @param direction [Symbol]
21
+ # @param decorated [Boolean] when `false`, omit the display symbol and
22
+ # return just the rounded number. Useful when the caller renders the
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.
21
28
  # @return [String]
22
- def ui(unit: nil, direction: :floor)
23
- unit ? render_display_unit(unit, direction) : render_default(direction)
29
+ # @example
30
+ # Amount.usdc("1.50").ui # => "$1.50"
31
+ # Amount.usdc("1.50").ui(decorated: false) # => "1.50"
32
+ # Amount.gold("1").ui(unit: :gram) # => "31.10 g"
33
+ # Amount.gold("1").ui(unit: :gram, decorated: false) # => "31.10"
34
+ # Amount.sol("2.5").ui(trim_zeros: true) # => "2.5 SOL"
35
+ def ui(unit: nil, direction: :floor, decorated: true, trim_zeros: nil)
36
+ if unit
37
+ render_display_unit(unit, direction, decorated:, trim_zeros:)
38
+ else
39
+ render_default(direction, decorated:, trim_zeros:)
40
+ end
24
41
  end
25
42
 
26
43
  # @return [String]
@@ -37,19 +54,24 @@ class Amount
37
54
 
38
55
  private
39
56
 
40
- def render_default(direction)
57
+ def render_default(direction, decorated:, trim_zeros:)
41
58
  rounded = round(@amount.decimal, @entry.ui_decimals, direction)
42
- apply_symbol(format("%.#{@entry.ui_decimals}f", rounded), @entry.display_symbol, @entry.display_position)
59
+ formatted = format_number(rounded, @entry.ui_decimals, resolve_trim(trim_zeros))
60
+ return formatted unless decorated
61
+
62
+ apply_symbol(formatted, @entry.display_symbol, @entry.display_position)
43
63
  end
44
64
 
45
- def render_display_unit(unit, direction)
65
+ def render_display_unit(unit, direction, decorated:, trim_zeros:)
46
66
  spec = fetch_display_unit(unit)
47
67
  scaled = @amount.decimal * Amount.coerce_decimal(spec[:scale])
48
68
  decimals = spec[:ui_decimals] || @entry.ui_decimals
49
69
  rounded = round(scaled, decimals, direction)
70
+ formatted = format_number(rounded, decimals, resolve_trim(trim_zeros, spec))
71
+ return formatted unless decorated
50
72
 
51
73
  apply_symbol(
52
- format("%.#{decimals}f", rounded),
74
+ formatted,
53
75
  spec[:symbol] || @entry.display_symbol,
54
76
  spec[:position] || @entry.display_position
55
77
  )
@@ -73,6 +95,18 @@ class Amount
73
95
  truncated / factor
74
96
  end
75
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
+
76
110
  def apply_symbol(str, symbol, position)
77
111
  return str if symbol.nil? || symbol.empty?
78
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.4"
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