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.10.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



215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/philiprehberger/approx.rb', line 215

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

.max_diff(a, b, epsilon: 1e-10) ⇒ Hash?

Find the element pair with the largest absolute difference across two arrays or hashes

For arrays, iterates element-by-element and returns the index and values of the pair with the greatest absolute difference. For hashes, iterates over shared keys and uses :key instead of :index. Returns nil if both collections are empty. Raises Approx::Error for mismatched types or non-collection inputs.

Parameters:

  • a (Array, Hash)

    first collection

  • b (Array, Hash)

    second collection

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

    tolerance used to set :match on the returned hash

Returns:

  • (Hash, nil)

    hash with :index/:key, :a, :b, :diff, :match, :epsilon or nil

Raises:

  • (Error)

    if inputs are not both arrays or both hashes



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/philiprehberger/approx.rb', line 177

def self.max_diff(a, b, epsilon: 1e-10)
  unless (a.is_a?(Array) && b.is_a?(Array)) || (a.is_a?(Hash) && b.is_a?(Hash))
    raise Error, 'both arguments must be arrays or both must be hashes'
  end

  if a.is_a?(Array)
    flat_a = a.flatten
    flat_b = b.flatten
    return nil if flat_a.empty? && flat_b.empty?

    best = nil
    flat_a.each_with_index do |val_a, i|
      val_b = flat_b[i]
      d = (val_a - val_b).abs
      best = { index: i, a: val_a, b: val_b, diff: d } if best.nil? || d > best[:diff]
    end
    best.merge(match: best[:diff] <= epsilon, epsilon: epsilon)
  else
    shared_keys = a.keys & b.keys
    return nil if shared_keys.empty?

    best = nil
    shared_keys.each do |k|
      val_a = a[k]
      val_b = b[k]
      d = (val_a - val_b).abs
      best = { key: k, a: val_a, b: val_b, diff: d } if best.nil? || d > best[:diff]
    end
    best.merge(match: best[:diff] <= epsilon, epsilon: epsilon)
  end
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