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.12.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.
-
.monotonic?(values, direction: :increasing, epsilon: nil, rel_tol: 0.0) ⇒ Boolean
Check if a sequence is approximately monotonic under the tolerance model of .equal?.
-
.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
147 148 149 150 151 |
# File 'lib/philiprehberger/approx.rb', line 147 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.
209 210 211 212 213 |
# File 'lib/philiprehberger/approx.rb', line 209 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
169 170 171 |
# File 'lib/philiprehberger/approx.rb', line 169 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.
136 137 138 |
# File 'lib/philiprehberger/approx.rb', line 136 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
275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/philiprehberger/approx.rb', line 275 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.
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/philiprehberger/approx.rb', line 237 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 |
.monotonic?(values, direction: :increasing, epsilon: nil, rel_tol: 0.0) ⇒ Boolean
Check if a sequence is approximately monotonic under the tolerance model of .equal?
Pairwise compares adjacent elements using the same absolute/relative tolerance helpers as .equal?. For strict directions (:increasing, :decreasing) each pair must satisfy the strict inequality AND must not be approximately equal within the configured tolerance. For non-strict directions (:non_decreasing, :non_increasing) each pair must satisfy the non-strict inequality OR be approximately equal within the configured tolerance. Empty and single-element sequences return true.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/philiprehberger/approx.rb', line 75 def self.monotonic?(values, direction: :increasing, epsilon: nil, rel_tol: 0.0) unless %i[increasing decreasing non_decreasing non_increasing].include?(direction) raise ArgumentError, "unknown direction: #{direction.inspect}" end arr = values.to_a return true if arr.length < 2 opts = { rel_tol: rel_tol } opts[:epsilon] = epsilon unless epsilon.nil? arr.each_cons(2).all? do |a, b| near = equal?(a, b, **opts) case direction when :increasing then a < b && !near when :decreasing then a > b && !near when :non_decreasing then a <= b || near when :non_increasing then a >= b || near end 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
221 222 223 |
# File 'lib/philiprehberger/approx.rb', line 221 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.
106 107 108 |
# File 'lib/philiprehberger/approx.rb', line 106 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.
191 192 193 194 195 196 197 198 |
# File 'lib/philiprehberger/approx.rb', line 191 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
178 179 180 |
# File 'lib/philiprehberger/approx.rb', line 178 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.
120 121 122 123 124 |
# File 'lib/philiprehberger/approx.rb', line 120 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
158 159 160 |
# File 'lib/philiprehberger/approx.rb', line 158 def self.zero?(value, epsilon: 1e-9) value.abs <= epsilon end |