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.11.0'
Class Method Summary collapse
-
.all_equal?(values, epsilon: nil, rel_tol: nil) ⇒ Boolean
Check if every element of an enumerable is approximately equal to the first.
-
.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
.all_equal?(values, epsilon: nil, rel_tol: nil) ⇒ Boolean
Check if every element of an enumerable is approximately equal to the first
Accepts any Enumerable and iterates via .to_a. Empty and single-element collections return true. Reuses the same tolerance semantics as .equal?, so either absolute epsilon or relative tolerance (or both) may be supplied.
47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/philiprehberger/approx.rb', line 47 def self.all_equal?(values, epsilon: nil, rel_tol: nil) arr = values.to_a return true if arr.length < 2 opts = {} opts[:epsilon] = epsilon unless epsilon.nil? opts[:rel_tol] = rel_tol unless rel_tol.nil? first = arr.first arr.drop(1).all? { |v| equal?(first, v, **opts) } end |
.assert_near(a, b, epsilon: 1e-9, rel_tol: 0) ⇒ Object
Assert that two values are approximately equal, raising on mismatch
109 110 111 112 113 |
# File 'lib/philiprehberger/approx.rb', line 109 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.
171 172 173 174 175 |
# File 'lib/philiprehberger/approx.rb', line 171 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
131 132 133 |
# File 'lib/philiprehberger/approx.rb', line 131 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.
98 99 100 |
# File 'lib/philiprehberger/approx.rb', line 98 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
237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/philiprehberger/approx.rb', line 237 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.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/philiprehberger/approx.rb', line 199 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
183 184 185 |
# File 'lib/philiprehberger/approx.rb', line 183 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.
68 69 70 |
# File 'lib/philiprehberger/approx.rb', line 68 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.
153 154 155 156 157 158 159 160 |
# File 'lib/philiprehberger/approx.rb', line 153 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
140 141 142 |
# File 'lib/philiprehberger/approx.rb', line 140 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.
82 83 84 85 86 |
# File 'lib/philiprehberger/approx.rb', line 82 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
120 121 122 |
# File 'lib/philiprehberger/approx.rb', line 120 def self.zero?(value, epsilon: 1e-9) value.abs <= epsilon end |