Module: Philiprehberger::Approx

Defined in:
lib/philiprehberger/approx.rb,
lib/philiprehberger/approx/version.rb,
lib/philiprehberger/approx/comparator.rb,
lib/philiprehberger/approx/rspec_matchers.rb

Defined Under Namespace

Modules: RSpecMatchers Classes: Comparator, Error

Constant Summary collapse

VERSION =
'0.9.0'

Class Method Summary collapse

Class Method Details

.assert_near(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Object

Assert that two values are approximately equal, raising on mismatch

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • epsilon (Float) (defaults to: 1e-9)

    maximum allowed absolute difference

  • rel_tol (Float) (defaults to: 0)

    relative tolerance (default 0 — disabled)

Raises:

  • (Error)

    if values differ by more than the allowed tolerance



87
88
89
90
91
# File 'lib/philiprehberger/approx.rb', line 87

def self.assert_near(a, b, epsilon: 1e-9, rel_tol: 0)
  return if equal?(a, b, epsilon: epsilon, rel_tol: rel_tol)

  raise Error, "expected #{a.inspect} to be near #{b.inspect} (epsilon: #{epsilon}, rel_tol: #{rel_tol})"
end

.assert_within(a, b, abs: nil, rel: nil) ⇒ Object

Assert that two values pass within?, raising on mismatch

At least one of abs: or rel: must be provided.

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • abs (Float, nil) (defaults to: nil)

    absolute tolerance

  • rel (Float, nil) (defaults to: nil)

    relative tolerance

Raises:

  • (Error)

    if values fail both tolerance checks



149
150
151
152
153
# File 'lib/philiprehberger/approx.rb', line 149

def self.assert_within(a, b, abs: nil, rel: nil)
  return if within?(a, b, abs: abs, rel: rel)

  raise Error, "expected #{a.inspect} to be within #{b.inspect} (abs: #{abs}, rel: #{rel})"
end

.between?(value, min, max, epsilon: 1e-9) ⇒ Boolean

Check if a numeric value lies within [min, max] with epsilon slack on both ends

Parameters:

  • value (Numeric)

    value to test

  • min (Numeric)

    inclusive lower bound

  • max (Numeric)

    inclusive upper bound

  • epsilon (Float) (defaults to: 1e-9)

    tolerance applied to each bound

Returns:

  • (Boolean)

    true if min - epsilon <= value <= max + epsilon



109
110
111
# File 'lib/philiprehberger/approx.rb', line 109

def self.between?(value, min, max, epsilon: 1e-9)
  value.between?(min - epsilon, max + epsilon)
end

.clamp(value, target, epsilon: 1e-9, rel_tol: 0) ⇒ Numeric

Snap a value to target if approximately equal, otherwise return unchanged

Returns target when value is within epsilon of target. Useful for snapping near-values to an exact canonical value.

Parameters:

  • value (Numeric)

    the value to potentially snap

  • target (Numeric)

    the target to snap to

  • epsilon (Float) (defaults to: 1e-9)

    maximum allowed absolute difference

  • rel_tol (Float) (defaults to: 0)

    relative tolerance (default 0 — disabled)

Returns:

  • (Numeric)

    target if approximately equal, otherwise value



76
77
78
# File 'lib/philiprehberger/approx.rb', line 76

def self.clamp(value, target, epsilon: 1e-9, rel_tol: 0)
  equal?(value, target, epsilon: epsilon, rel_tol: rel_tol) ? target : value
end

.diff(a, b, epsilon: Float::EPSILON) ⇒ Hash

Return a diagnostic hash showing why values do or do not match

Parameters:

  • a (Numeric)

    first value

  • b (Numeric)

    second value

  • epsilon (Float) (defaults to: Float::EPSILON)

    maximum allowed difference

Returns:

  • (Hash)

    diagnostic hash with :match, :actual_diff, :allowed, :ratio



171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/philiprehberger/approx.rb', line 171

def self.diff(a, b, epsilon: Float::EPSILON)
  actual_diff = (a - b).abs.to_f
  allowed = epsilon.to_f
  ratio = allowed.zero? ? Float::INFINITY : actual_diff / allowed

  {
    match: actual_diff <= allowed,
    actual_diff: actual_diff,
    allowed: allowed,
    ratio: ratio
  }
end

.equal?(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Boolean

Check if two values are approximately equal within epsilon

When rel_tol is non-zero, values match if either tolerance passes, matching Python’s math.isclose semantics: |a - b| <= max(rel_tol * max(|a|, |b|), epsilon)

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • epsilon (Float) (defaults to: 1e-9)

    maximum allowed absolute difference

  • rel_tol (Float) (defaults to: 0)

    relative tolerance (default 0 — disabled)

Returns:

  • (Boolean)

    true if values are approximately equal



22
23
24
# File 'lib/philiprehberger/approx.rb', line 22

def self.equal?(a, b, epsilon: 1e-9, rel_tol: 0)
  compare(a, b, epsilon, rel_tol)
end

.near?(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Boolean

Alias for equal? with explicit epsilon

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • epsilon (Float) (defaults to: 1e-9)

    maximum allowed absolute difference

  • rel_tol (Float) (defaults to: 0)

    relative tolerance (default 0 — disabled)

Returns:

  • (Boolean)

    true if values are near each other



33
34
35
# File 'lib/philiprehberger/approx.rb', line 33

def self.near?(a, b, epsilon: 1e-9, rel_tol: 0)
  equal?(a, b, epsilon: epsilon, rel_tol: rel_tol)
end

.percent_equal?(a, b, percent:) ⇒ Boolean

Check if two values are approximately equal within a percentage tolerance

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • percent (Float)

    maximum allowed percentage difference

Returns:

  • (Boolean)

    true if values are within the percentage tolerance



161
162
163
# File 'lib/philiprehberger/approx.rb', line 161

def self.percent_equal?(a, b, percent:)
  compare_percent(a, b, percent)
end

.relative_equal?(a, b, tolerance: 1e-6) ⇒ Boolean

Check if two values are approximately equal using relative tolerance

Relative tolerance: |a - b| / max(|a|, |b|) <= tolerance Falls back to absolute comparison when both values are zero.

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • tolerance (Float) (defaults to: 1e-6)

    maximum allowed relative difference

Returns:

  • (Boolean)

    true if values are relatively near each other



46
47
48
# File 'lib/philiprehberger/approx.rb', line 46

def self.relative_equal?(a, b, tolerance: 1e-6)
  compare_relative(a, b, tolerance)
end

.sign_equal?(a, b, epsilon: 1e-9) ⇒ Boolean

Check if two numeric values share the same sign

Values with |x| <= epsilon are treated as zero, so two near-zero values are considered to share a sign regardless of their raw polarity.

Parameters:

  • a (Numeric)

    first value

  • b (Numeric)

    second value

  • epsilon (Float) (defaults to: 1e-9)

    threshold below which a value is treated as zero

Returns:

  • (Boolean)

    true if both values share the same sign (or both are near zero)



131
132
133
134
135
136
137
138
# File 'lib/philiprehberger/approx.rb', line 131

def self.sign_equal?(a, b, epsilon: 1e-9)
  a_zero = a.abs <= epsilon
  b_zero = b.abs <= epsilon
  return true if a_zero && b_zero
  return false if a_zero || b_zero

  (a.positive? && b.positive?) || (a.negative? && b.negative?)
end

.tolerance_range(value, epsilon: 1e-9) ⇒ Array<Numeric>

Return the tolerance bounds [min, max] around a value for a given epsilon

Parameters:

  • value (Numeric)

    center value

  • epsilon (Float) (defaults to: 1e-9)

    tolerance radius

Returns:

  • (Array<Numeric>)

    two-element array [value - epsilon, value + epsilon]



118
119
120
# File 'lib/philiprehberger/approx.rb', line 118

def self.tolerance_range(value, epsilon: 1e-9)
  [value - epsilon, value + epsilon]
end

.within?(a, b, abs: nil, rel: nil) ⇒ Boolean

Check if two values are approximately equal using combined tolerance

Passes if either absolute or relative tolerance is met. At least one of abs: or rel: must be provided.

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

  • abs (Float, nil) (defaults to: nil)

    absolute tolerance

  • rel (Float, nil) (defaults to: nil)

    relative tolerance

Returns:

  • (Boolean)

    true if values pass either tolerance check

Raises:

  • (ArgumentError)


60
61
62
63
64
# File 'lib/philiprehberger/approx.rb', line 60

def self.within?(a, b, abs: nil, rel: nil)
  raise ArgumentError, 'at least one of abs: or rel: must be provided' if abs.nil? && rel.nil?

  compare_within(a, b, abs, rel)
end

.zero?(value, epsilon: 1e-9) ⇒ Boolean

Check if a numeric value is approximately zero

Parameters:

  • value (Numeric)

    value to test

  • epsilon (Float) (defaults to: 1e-9)

    maximum allowed difference from zero

Returns:

  • (Boolean)

    true if |value| <= epsilon



98
99
100
# File 'lib/philiprehberger/approx.rb', line 98

def self.zero?(value, epsilon: 1e-9)
  value.abs <= epsilon
end