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.

Returns:

  • (Array[Symbol])
%i[standard exact].freeze

Class Method Summary collapse

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.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when either side is not a value



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.

Parameters:

  • value (Integer, Float, Rational)

    the number to hold

  • precision (Symbol)

    the precision to hold it at

Returns:

Raises:



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.

Overloads:

Parameters:

  • value (TwoPartFloat, Exact)

    the value to coerce

  • to (Symbol)

    the target precision

Returns:

Raises:

  • (UnknownPrecisionError)

    when to is not recognised

  • (ArgumentError)

    when asked to coerce :exact down to :standard, which would lose precision



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.

Parameters:

  • left (Symbol)

    one operand's precision

  • right (Symbol)

    the other operand's precision

Returns:

  • (Symbol)

    the result's precision

Raises:



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.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when either side is not a value



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.

Parameters:

  • precision (Symbol)

Returns:

  • (Symbol)

    the same precision

Raises:



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.

Parameters:

  • value (TwoPartFloat, Exact)

    the value to check

  • precision (Symbol)

    the precision it claims

Returns:

Raises:

  • (UnknownPrecisionError)

    when the precision is not recognised

  • (ArgumentError)

    when the value does not match the precision



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.

Parameters:

  • precision (Symbol)

    the precision

Returns:

  • (Class)

    the type its values take

Raises:



149
150
151
# File 'lib/horologium/numeric/precision.rb', line 149

def value_type(precision)
  (validate!(precision) == :exact) ? Exact : TwoPartFloat
end