Class: Amount

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Allocation, Arithmetic, Comparison, Conversion, Serialization
Defined in:
lib/amount.rb,
lib/amount/parser.rb,
lib/amount/display.rb,
lib/amount/version.rb,
lib/amount/registry.rb,
lib/amount/allocation.rb,
lib/amount/arithmetic.rb,
lib/amount/comparison.rb,
lib/amount/conversion.rb,
lib/amount/active_record.rb,
lib/amount/rspec/support.rb,
lib/amount/serialization.rb,
lib/amount/rspec/matchers.rb,
lib/amount/active_record/type.rb,
lib/amount/active_record/model.rb,
lib/amount/active_record/rspec/matchers.rb,
lib/amount/active_record/amount_validator.rb,
lib/amount/active_record/migration_methods.rb,
lib/amount/registry/generated_constructors.rb,
lib/amount/active_record/attribute_definition.rb

Overview

Represents a precise quantity of a registered fungible type.

‘Amount` stores its value as an arbitrary-precision atomic `Integer` in the smallest unit configured for the registered symbol. UI values are parsed from strings or decimals, while integer inputs are treated as atomic counts unless `from:` overrides inference.

Behavior is composed from a set of focused mixins:

Examples:

Constructing from a UI value

Amount.register :USDC, decimals: 6

Amount.usdc("1.50").atomic
# => 1500000

Constructing from an atomic value

Amount.usdc(1_500_000, from: :atomic).decimal.to_s("F")
# => "1.5"

Defined Under Namespace

Modules: ActiveRecord, Allocation, Arithmetic, Comparison, Conversion, RSpec, Serialization Classes: Display, Error, InvalidInput, Parser, Registry, TypeMismatch, UnregisteredType

Constant Summary collapse

VERSION =
"0.0.5"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Serialization

included, #to_h

Methods included from Conversion

#to

Methods included from Comparison

#<=>, #==, #eql?, #hash, #negative?, #positive?, #same_type?, #zero?

Methods included from Allocation

#allocate, #split

Methods included from Arithmetic

#*, #+, #-, #-@, #/, #abs

Constructor Details

#initialize(value, symbol, from: nil) ⇒ Amount

Creates an amount for a registered symbol.

Input inference rules:

  • ‘Integer` => atomic units

  • ‘String` => UI decimal value

  • ‘Float`, `BigDecimal`, `Rational` => UI decimal value

  • ‘from:` overrides inference explicitly

Examples:

Integer inputs are atomic by default

Amount.new(1_500_000, :USDC).decimal.to_s("F")
# => "1.5"

String inputs are UI values by default

Amount.new("1.50", :USDC).atomic
# => 1500000

Parameters:

  • value (Integer, String, Float, BigDecimal, Rational)
  • symbol (Symbol, String)

    registered type identifier

  • from (Symbol, nil) (defaults to: nil)

    one of ‘:atomic`, `:ui`, or `:float`

Raises:



188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/amount.rb', line 188

def initialize(value, symbol, from: nil)
  @symbol = symbol.to_sym
  @entry = self.class.registry.lookup(@symbol)

  expected = @entry.amount_class
  if expected && expected != Amount && self.class != Amount && !instance_of?(expected)
    raise InvalidInput, "use #{expected}.new for #{@symbol}"
  end

  @atomic = infer_value(from, value)
  @display = Display.new(self)
end

Instance Attribute Details

#atomicObject (readonly)

Returns the value of attribute atomic.



166
167
168
# File 'lib/amount.rb', line 166

def atomic
  @atomic
end

#displayObject (readonly)

Returns the value of attribute display.



166
167
168
# File 'lib/amount.rb', line 166

def display
  @display
end

#symbolObject (readonly)

Returns the value of attribute symbol.



166
167
168
# File 'lib/amount.rb', line 166

def symbol
  @symbol
end

Class Method Details

.coerce_decimal(value) ⇒ BigDecimal

Coerces a numeric input to BigDecimal in a way that preserves Rational values. ‘BigDecimal(value.to_s)` raises `ArgumentError` for Rational because `Rational#to_s` produces strings like `“3/2”`. This helper is the single place every call site should use to convert a scalar / rate / display-unit scale into a BigDecimal.

Parameters:

  • value (Numeric, BigDecimal, Rational, String)

Returns:

  • (BigDecimal)


129
130
131
132
133
134
135
# File 'lib/amount.rb', line 129

def coerce_decimal(value)
  case value
  when BigDecimal then value
  when Rational then BigDecimal(value, Float::DIG + 4)
  else BigDecimal(value.to_s)
  end
end

.new(value, symbol, from: nil) ⇒ Amount

When called as ‘Amount.new` for a symbol whose registry entry binds a custom class, dispatch the construction to that class instead of raising. Direct calls to a subclass (`GoldAmount.new(…)`) still go through the default `Class.new` path. Calls that target the wrong subclass continue to raise from `#initialize`.

Parameters:

  • value (Integer, String, Float, BigDecimal, Rational)
  • symbol (Symbol, String)
  • from (Symbol, nil) (defaults to: nil)

Returns:



113
114
115
116
117
118
119
# File 'lib/amount.rb', line 113

def new(value, symbol, from: nil)
  if equal?(::Amount)
    entry_class = registry.lookup(symbol.to_sym).amount_class
    return entry_class.new(value, symbol, from:) if entry_class && entry_class != ::Amount
  end
  super
end

.parse(str) ⇒ Amount

Parses the compact client-facing string representation.

Accepts either the default form ‘SYMBOL|amount` or the explicit versioned form `v1:SYMBOL|amount`.

Examples:

Parsing the default compact format

Amount.parse("USDC|1.50")

Parsing the explicit versioned compact format

Amount.parse("v1:USDC|1.50")

Parameters:

  • str (String)

Returns:

Raises:



99
100
101
# File 'lib/amount.rb', line 99

def parse(str)
  Parser.new(str).parse
end

.register(symbol, **opts) ⇒ void

This method returns an undefined value.

Examples:

Registering a type

Amount.register :USDC,
  decimals: 6,
  display_symbol: "$",
  display_position: :prefix,
  ui_decimals: 2

Parameters:

  • symbol (Symbol, String)
  • opts (Hash)


72
73
74
# File 'lib/amount.rb', line 72

def register(symbol, **opts)
  registry.register(symbol, **opts)
end

.register_default_rate(from, to, rate) ⇒ void

This method returns an undefined value.

Examples:

Registering a directional default rate

Amount.register_default_rate :USD, :USDC, "1"

Parameters:

  • from (Symbol, String)
  • to (Symbol, String)
  • rate (String, Numeric, BigDecimal)


82
83
84
# File 'lib/amount.rb', line 82

def register_default_rate(from, to, rate)
  registry.register_default_rate(from, to, rate)
end

.registryAmount::Registry

Examples:

Accessing the shared registry

Amount.registry.locked?
# => false

Returns:



58
59
60
61
# File 'lib/amount.rb', line 58

def registry
  Amount.instance_variable_get(:@registry) ||
    replace_registry(Registry.new)
end

.with_registry(registry) { ... } ⇒ Object

Temporarily swaps the global registry. Intended for tests.

Examples:

Using a temporary registry

test_registry = Amount::Registry.new
Amount.with_registry(test_registry) do
  Amount.register :TEST, decimals: 2
end

Parameters:

Yields:

Returns:

  • (Object)


147
148
149
150
151
152
153
# File 'lib/amount.rb', line 147

def with_registry(registry)
  original = Amount.instance_variable_get(:@registry)
  replace_registry(registry)
  yield
ensure
  replace_registry(original)
end

Instance Method Details

#decimalBigDecimal

Examples:

Converting the atomic value back to a decimal quantity

Amount.usdc(1_500_000, from: :atomic).decimal.to_s("F")
# => "1.5"

Returns:

  • (BigDecimal)


221
222
223
# File 'lib/amount.rb', line 221

def decimal
  BigDecimal(@atomic) / (BigDecimal(10)**decimals)
end

#decimalsInteger

Examples:

Reading the registered storage precision

Amount.usdc("1").decimals
# => 6

Returns:

  • (Integer)


213
214
215
# File 'lib/amount.rb', line 213

def decimals
  @entry.decimals
end

#inspectString

Examples:

Console-friendly inspection

Amount.usdc("1.50").inspect
# => "#<Amount USDC $1.50>"

Returns:

  • (String)


231
232
233
# File 'lib/amount.rb', line 231

def inspect
  "#<#{self.class} #{symbol} #{ui}>"
end

#registry_entryAmount::Registry::Entry

Examples:

Accessing display configuration for this amount

Amount.usdc("1").registry_entry.ui_decimals
# => 2

Returns:



205
206
207
# File 'lib/amount.rb', line 205

def registry_entry
  @entry
end