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
-
.assert_near(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Object
Assert that two values are approximately equal, raising on mismatch.
-
.assert_within(a, b, abs: nil, rel: nil) ⇒ Object
Assert that two values pass within?, raising on mismatch.
-
.between?(value, min, max, epsilon: 1e-9) ⇒ Boolean
Check if a numeric value lies within [min, max] with epsilon slack on both ends.
-
.clamp(value, target, epsilon: 1e-9, rel_tol: 0) ⇒ Numeric
Snap a value to target if approximately equal, otherwise return unchanged.
-
.diff(a, b, epsilon: Float::EPSILON) ⇒ Hash
Return a diagnostic hash showing why values do or do not match.
-
.equal?(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Boolean
Check if two values are approximately equal within epsilon.
-
.max_diff(a, b, epsilon: 1e-10) ⇒ Hash?
Find the element pair with the largest absolute difference across two arrays or hashes.
-
.near?(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Boolean
Alias for equal? with explicit epsilon.
-
.percent_equal?(a, b, percent:) ⇒ Boolean
Check if two values are approximately equal within a percentage tolerance.
-
.relative_equal?(a, b, tolerance: 1e-6) ⇒ Boolean
Check if two values are approximately equal using relative tolerance.
-
.sign_equal?(a, b, epsilon: 1e-9) ⇒ Boolean
Check if two numeric values share the same sign.
-
.tolerance_range(value, epsilon: 1e-9) ⇒ Array<Numeric>
Return the tolerance bounds [min, max] around a value for a given epsilon.
-
.within?(a, b, abs: nil, rel: nil) ⇒ Boolean
Check if two values are approximately equal using combined tolerance.
-
.zero?(value, epsilon: 1e-9) ⇒ Boolean
Check if a numeric value is approximately zero.
Class Method Details
.assert_near(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Object
Assert that two values are approximately equal, raising on mismatch
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.
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
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.
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
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)
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.
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
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
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.
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.
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
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.
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
98 99 100 |
# File 'lib/philiprehberger/approx.rb', line 98 def self.zero?(value, epsilon: 1e-9) value.abs <= epsilon end |