Module: Horologium::Numeric::Precision
- Defined in:
- lib/horologium/numeric/precision.rb,
sig/horologium/numeric/precision.rbs
Overview
The precision a value computes with: :standard, the fast two-part float
(TwoPartFloat), or :exact, the lossless Rational (Exact). This
module holds the rules that decide the precision of a result and coerce a
value from one precision into another.
The rule for a result is that exactness is contagious: an operation
between two :standard values stays :standard, but mixing :standard
with :exact promotes to :exact rather than dropping to :standard.
Promotion loses nothing, because a two-part float pair is already an
exact Rational. What :exact guarantees is Horologium's own arithmetic.
It cannot bring back precision an input already lost when it was built.
A scale registered with Configuration#register_scale builds and combines its values here, so this module is public API.
Constant Summary collapse
- NAMES =
The recognised precisions.
%i[standard exact].freeze
Class Method Summary collapse
-
.add(left, right) ⇒ TwoPartFloat, Exact
Adds two values.
-
.build(value, precision) ⇒ TwoPartFloat, Exact
Builds a value at a precision, from a plain number: an Exact for
:exact, a TwoPartFloat for:standard. -
.coerce(value, to:) ⇒ TwoPartFloat, Exact
Coerces a numeric value into a precision, losslessly.
-
.resolve(left, right) ⇒ Symbol
The precision a result takes from its two operands.
-
.subtract(left, right) ⇒ TwoPartFloat, Exact
Subtracts two values, promoting to exact the same way Precision.add does.
-
.validate!(precision) ⇒ Symbol
The same precision.
-
.validate_value!(value, precision) ⇒ TwoPartFloat, Exact
Checks that a value matches a precision: a TwoPartFloat for
:standard, an Exact for:exact. -
.value_type(precision) ⇒ Class
The numeric type a value takes at a precision: Exact for
:exactand TwoPartFloat for:standard.
Class Method Details
.add(left, right) ⇒ TwoPartFloat, Exact
Adds two values. Two standard values add as two-part floats; if
either is exact, both are promoted to exact Rationals first. Build a
plain number into a value with build before adding it: a bare
Float or Rational is refused, because promoting it would quietly
move the result to :exact.
102 103 104 105 106 107 108 |
# File 'lib/horologium/numeric/precision.rb', line 102 def add(left, right) if left.is_a?(TwoPartFloat) && right.is_a?(TwoPartFloat) left + right else promote(left) + promote(right) end end |
.build(value, precision) ⇒ TwoPartFloat, Exact
Builds a value at a precision, from a plain number: an Exact for
:exact, a TwoPartFloat for :standard.
83 84 85 86 87 88 89 90 |
# File 'lib/horologium/numeric/precision.rb', line 83 def build(value, precision) case validate!(precision) when :exact Exact.new(value) else TwoPartFloat.from_real(value) end end |
.coerce(value, to:) ⇒ Exact .coerce(value, to:) ⇒ TwoPartFloat .coerce(value, to:) ⇒ TwoPartFloat, Exact
Coerces a numeric value into a precision, losslessly. Promoting a
:standard value to :exact keeps its exact value; a value already
in the target precision is returned unchanged. There is no lossy
path: the contagion rule never moves an :exact value down to
:standard, and asking for that raises an error.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/horologium/numeric/precision.rb', line 61 def coerce(value, to:) case to when :exact value.is_a?(Exact) ? value : Exact.new(value) when :standard unless value.is_a?(TwoPartFloat) raise ArgumentError, "cannot coerce #{value.class} down to :standard without loss" end value else raise UnknownPrecisionError.new(to, NAMES) end end |
.resolve(left, right) ⇒ Symbol
The precision a result takes from its two operands. Same precision
passes through; a mix of :standard and :exact promotes to
:exact.
41 42 43 44 45 46 47 |
# File 'lib/horologium/numeric/precision.rb', line 41 def resolve(left, right) validate!(left) validate!(right) return :exact if left == :exact || right == :exact :standard end |
.subtract(left, right) ⇒ TwoPartFloat, Exact
Subtracts two values, promoting to exact the same way add does.
116 117 118 119 120 121 122 |
# File 'lib/horologium/numeric/precision.rb', line 116 def subtract(left, right) if left.is_a?(TwoPartFloat) && right.is_a?(TwoPartFloat) left - right else promote(left) - promote(right) end end |
.validate!(precision) ⇒ Symbol
Returns the same precision.
27 28 29 30 31 |
# File 'lib/horologium/numeric/precision.rb', line 27 def validate!(precision) return precision if NAMES.include?(precision) raise UnknownPrecisionError.new(precision, NAMES) end |
.validate_value!(value, precision) ⇒ TwoPartFloat, Exact
Checks that a value matches a precision: a TwoPartFloat for
:standard, an Exact for :exact.
132 133 134 135 136 137 138 139 140 141 |
# File 'lib/horologium/numeric/precision.rb', line 132 def validate_value!(value, precision) expected = value_type(precision) unless value.is_a?(expected) raise ArgumentError, "a #{precision} value must be a #{expected}, " \ "got a #{value.class}" end value end |
.value_type(precision) ⇒ Class
The numeric type a value takes at a precision: Exact for :exact
and TwoPartFloat for :standard.
149 150 151 |
# File 'lib/horologium/numeric/precision.rb', line 149 def value_type(precision) (validate!(precision) == :exact) ? Exact : TwoPartFloat end |