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.6.0'

Class Method Summary collapse

Class Method Details

.assert_near(a, b, epsilon: 1e-9) ⇒ 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 difference

Raises:

  • (Error)

    if values differ by more than epsilon



79
80
81
82
83
# File 'lib/philiprehberger/approx.rb', line 79

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

  raise Error, "expected #{a.inspect} to be near #{b.inspect} (epsilon: #{epsilon})"
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



114
115
116
117
118
# File 'lib/philiprehberger/approx.rb', line 114

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



101
102
103
# File 'lib/philiprehberger/approx.rb', line 101

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

.clamp(value, target, epsilon: 1e-9) ⇒ 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 difference

Returns:

  • (Numeric)

    target if approximately equal, otherwise value



69
70
71
# File 'lib/philiprehberger/approx.rb', line 69

def self.clamp(value, target, epsilon: 1e-9)
  equal?(value, target, epsilon: epsilon) ? 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



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/philiprehberger/approx.rb', line 136

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) ⇒ Boolean

Check if two values are approximately equal within epsilon

Parameters:

  • a (Numeric, Array, Hash)

    first value

  • b (Numeric, Array, Hash)

    second value

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

    maximum allowed difference

Returns:

  • (Boolean)

    true if values are approximately equal



17
18
19
# File 'lib/philiprehberger/approx.rb', line 17

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

.near?(a, b, epsilon: 1e-9) ⇒ 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 difference

Returns:

  • (Boolean)

    true if values are near each other



27
28
29
# File 'lib/philiprehberger/approx.rb', line 27

def self.near?(a, b, epsilon: 1e-9)
  equal?(a, b, epsilon: epsilon)
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



126
127
128
# File 'lib/philiprehberger/approx.rb', line 126

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



40
41
42
# File 'lib/philiprehberger/approx.rb', line 40

def self.relative_equal?(a, b, tolerance: 1e-6)
  compare_relative(a, b, tolerance)
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)


54
55
56
57
58
# File 'lib/philiprehberger/approx.rb', line 54

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



90
91
92
# File 'lib/philiprehberger/approx.rb', line 90

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