Class: Amount
- Inherits:
-
Object
show all
- Extended by:
- Forwardable
- Includes:
- Comparable
- Defined in:
- lib/amount.rb,
lib/amount/parser.rb,
lib/amount/display.rb,
lib/amount/version.rb,
lib/amount/registry.rb,
lib/amount/serializer.rb,
lib/amount/active_record.rb,
lib/amount/rspec_support.rb,
lib/amount/rspec_matchers.rb,
lib/amount/active_record/type.rb,
lib/amount/active_record/model.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.
Defined Under Namespace
Modules: ActiveRecord, RSpecMatchers, RSpecSupport
Classes: Display, Error, InvalidInput, Parser, Registry, Serializer, TypeMismatch, UnregisteredType
Constant Summary
collapse
- VERSION =
"0.0.1"
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
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
138
139
140
141
142
143
144
145
146
147
|
# File 'lib/amount.rb', line 138
def initialize(value, symbol, from: nil)
@symbol = symbol.to_sym
@entry = self.class.registry.lookup(@symbol)
if @entry.amount_class != self.class && @entry.amount_class != Amount
raise InvalidInput, "use #{@entry.amount_class}.new for #{@symbol}" unless instance_of?(@entry.amount_class)
end
@atomic = infer_value(from, value)
end
|
Instance Attribute Details
#atomic ⇒ Object
Returns the value of attribute atomic.
116
117
118
|
# File 'lib/amount.rb', line 116
def atomic
@atomic
end
|
#symbol ⇒ Object
Returns the value of attribute symbol.
116
117
118
|
# File 'lib/amount.rb', line 116
def symbol
@symbol
end
|
Class Method Details
.load(hash) ⇒ Amount
417
418
419
|
# File 'lib/amount.rb', line 417
def self.load(hash)
Serializer.load(hash)
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`.
83
84
85
|
# File 'lib/amount.rb', line 83
def parse(str)
Parser.new(str).parse
end
|
.register(symbol, **opts) ⇒ void
This method returns an undefined value.
56
57
58
|
# File 'lib/amount.rb', line 56
def register(symbol, **opts)
registry.register(symbol, **opts)
end
|
.register_default_rate(from, to, rate) ⇒ void
This method returns an undefined value.
66
67
68
|
# File 'lib/amount.rb', line 66
def register_default_rate(from, to, rate)
registry.register_default_rate(from, to, rate)
end
|
42
43
44
45
|
# File 'lib/amount.rb', line 42
def registry
Amount.instance_variable_get(:@registry) ||
replace_registry(Registry.new)
end
|
.with_registry(registry) { ... } ⇒ Object
Temporarily swaps the global registry. Intended for tests.
97
98
99
100
101
102
103
|
# File 'lib/amount.rb', line 97
def with_registry(registry)
original = Amount.instance_variable_get(:@registry)
replace_registry(registry)
yield
ensure
replace_registry(original)
end
|
Instance Method Details
#*(scalar) ⇒ Amount
263
264
265
266
|
# File 'lib/amount.rb', line 263
def *(scalar)
ensure_scalar!(scalar)
build((BigDecimal(@atomic) * BigDecimal(scalar.to_s)).to_i)
end
|
243
244
245
246
|
# File 'lib/amount.rb', line 243
def +(other)
rhs = coerce_other_to_self_type!(other)
build(@atomic + rhs.atomic)
end
|
253
254
255
256
|
# File 'lib/amount.rb', line 253
def -(other)
rhs = coerce_other_to_self_type!(other)
build(@atomic - rhs.atomic)
end
|
230
231
232
|
# File 'lib/amount.rb', line 230
def -@
build(-@atomic)
end
|
#/(other) ⇒ Amount, BigDecimal
276
277
278
279
280
281
282
283
284
285
286
287
288
|
# File 'lib/amount.rb', line 276
def /(other)
if other.is_a?(Amount)
ensure_same_type!(other)
raise ZeroDivisionError if other.zero?
BigDecimal(@atomic) / BigDecimal(other.atomic)
else
ensure_scalar!(other)
raise ZeroDivisionError if other.zero?
build((BigDecimal(@atomic) / BigDecimal(other.to_s)).to_i)
end
end
|
#<=>(other) ⇒ -1, ...
343
344
345
346
347
348
349
350
|
# File 'lib/amount.rb', line 343
def <=>(other)
return nil unless other.is_a?(Amount)
comparable = coerce_other_to_self_type(other)
return nil unless comparable
@atomic <=> comparable.atomic
end
|
#==(other) ⇒ Boolean
357
358
359
|
# File 'lib/amount.rb', line 357
def ==(other)
same_type?(other) && @atomic == other.atomic
end
|
222
223
224
|
# File 'lib/amount.rb', line 222
def abs
build(@atomic.abs)
end
|
#allocate(weights) ⇒ Array<(Array<Amount>, Amount)>
Allocates proportionally by integer weights and returns the leftover explicitly.
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
|
# File 'lib/amount.rb', line 322
def allocate(weights)
raise ArgumentError, "weights must be non-empty" if weights.empty?
raise ArgumentError, "weights must be non-negative integers" unless weights.all? { |weight| weight.is_a?(Integer) && weight >= 0 }
total = weights.sum
raise ArgumentError, "weights must sum to positive value" unless total.positive?
sign = atomic_sign
absolute_atomic = @atomic.abs
allocations = weights.map { |weight| absolute_atomic * weight / total }
remainder = absolute_atomic - allocations.sum
parts = allocations.map { |allocation| build(sign * allocation) }
[parts, build(sign * remainder)]
end
|
#decimal ⇒ BigDecimal
169
170
171
|
# File 'lib/amount.rb', line 169
def decimal
BigDecimal(@atomic) / (BigDecimal(10)**decimals)
end
|
#decimals ⇒ Integer
161
162
163
|
# File 'lib/amount.rb', line 161
def decimals
@entry.decimals
end
|
177
178
179
|
# File 'lib/amount.rb', line 177
def display
@display ||= Display.new(self)
end
|
#eql?(other) ⇒ Boolean
366
367
368
|
# File 'lib/amount.rb', line 366
def eql?(other)
other.class == self.class && symbol == other.symbol && @atomic == other.atomic
end
|
#hash ⇒ Integer
374
375
376
|
# File 'lib/amount.rb', line 374
def hash
[self.class, symbol, @atomic].hash
end
|
#inspect ⇒ String
187
188
189
|
# File 'lib/amount.rb', line 187
def inspect
"#<#{self.class} #{symbol} #{ui}>"
end
|
#negative? ⇒ Boolean
207
|
# File 'lib/amount.rb', line 207
def negative? = @atomic.negative?
|
#positive? ⇒ Boolean
201
|
# File 'lib/amount.rb', line 201
def positive? = @atomic.positive?
|
153
154
155
|
# File 'lib/amount.rb', line 153
def registry_entry
@entry
end
|
#same_type?(other) ⇒ Boolean
214
215
216
|
# File 'lib/amount.rb', line 214
def same_type?(other)
other.is_a?(Amount) && other.symbol == symbol
end
|
#split(n) ⇒ Array<(Array<Amount>, Amount)>
Splits into equal parts and returns the leftover explicitly.
301
302
303
304
305
306
307
308
309
|
# File 'lib/amount.rb', line 301
def split(n)
raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n.positive?
sign = atomic_sign
base, remainder = @atomic.abs.divmod(n)
parts = Array.new(n) { build(sign * base) }
[parts, build(sign * remainder)]
end
|
#to(target_symbol, rate: nil) ⇒ Amount
388
389
390
391
392
393
394
395
396
397
398
399
|
# File 'lib/amount.rb', line 388
def to(target_symbol, rate: nil)
target_symbol = target_symbol.to_sym
return self.class.new(@atomic, symbol, from: :atomic) if target_symbol == symbol
rate = resolve_rate(target_symbol, rate)
target_entry = self.class.registry.lookup(target_symbol)
decimal_result = decimal * BigDecimal(rate.to_s)
atomic_result = (decimal_result * (BigDecimal(10)**target_entry.decimals)).to_i
target_entry.amount_class.new(atomic_result, target_symbol, from: :atomic)
end
|
#to_h ⇒ Hash
405
406
407
|
# File 'lib/amount.rb', line 405
def to_h
Serializer.dump(self)
end
|
#zero? ⇒ Boolean
195
|
# File 'lib/amount.rb', line 195
def zero? = @atomic.zero?
|