philiprehberger-approx
Epsilon-based approximate equality for floats, arrays, and hashes
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-approx"
Or install directly:
gem install philiprehberger-approx
Usage
require "philiprehberger/approx"
Philiprehberger::Approx.equal?(1.0, 1.0 + 1e-10)
# => true
Philiprehberger::Approx.equal?(1.0, 1.1)
# => false
Custom Epsilon
Philiprehberger::Approx.equal?(1.0, 1.05, epsilon: 0.1)
# => true
Array and Hash Comparison
Philiprehberger::Approx.equal?([1.0, 2.0], [1.0 + 1e-10, 2.0])
# => true
Philiprehberger::Approx.equal?({ x: 1.0 }, { x: 1.0 + 1e-10 })
# => true
Relative Tolerance
Philiprehberger::Approx.relative_equal?(1_000_000.0, 1_000_001.0, tolerance: 1e-5)
# => true
Philiprehberger::Approx.relative_equal?(1.0, 2.0, tolerance: 1e-6)
# => false
Supports arrays and hashes recursively, just like equal?. Falls back to absolute comparison when both values are zero.
Combined Tolerance
Philiprehberger::Approx.within?(1_000_000.0, 1_000_001.0, abs: 1e-9, rel: 1e-5)
# => true (passes via relative tolerance)
Philiprehberger::Approx.within?(0.001, 0.002, abs: 0.01, rel: 1e-9)
# => true (passes via absolute tolerance)
Passes if either the absolute or relative tolerance is met. At least one of abs: or rel: must be provided.
Clamp (Snap Near-Values)
Philiprehberger::Approx.clamp(1.0 + 1e-10, 1.0)
# => 1.0 (snapped to target)
Philiprehberger::Approx.clamp(1.1, 1.0)
# => 1.1 (returned unchanged)
Philiprehberger::Approx.clamp(1.05, 1.0, epsilon: 0.1)
# => 1.0 (snapped with custom epsilon)
Returns the target if the value is approximately equal, otherwise returns the value unchanged. Useful for snapping near-values to an exact canonical value.
Reusable Comparator
comparator = Philiprehberger::Approx::Comparator.new(epsilon: 0.01, relative: 1e-3)
comparator.equal?(1_000.0, 1_000.5)
# => true
comparator.assert_near(1.0, 100.0)
# => raises Philiprehberger::Approx::Error
Zero Check
Philiprehberger::Approx.zero?(1e-12)
# => true
Philiprehberger::Approx.zero?(0.05, epsilon: 0.1)
# => true
Range Check
Philiprehberger::Approx.between?(5.0, 1.0, 10.0)
# => true
Philiprehberger::Approx.between?(10.0 + 1e-10, 1.0, 10.0)
# => true (within epsilon of upper bound)
Sign Equality
Philiprehberger::Approx.sign_equal?(5.0, 7.0)
# => true (both positive)
Philiprehberger::Approx.sign_equal?(2.0, -3.0)
# => false (opposite signs)
Philiprehberger::Approx.sign_equal?(1e-12, -1e-12)
# => true (both near zero)
Values with |x| <= epsilon are treated as zero, so two near-zero values are considered to share a sign regardless of their raw polarity.
Assert Within
Philiprehberger::Approx.assert_within(1_000_000.0, 1_000_001.0, rel: 1e-5)
# => nil (passes via relative tolerance)
Philiprehberger::Approx.assert_within(1.0, 2.0, abs: 0.01)
# => raises Philiprehberger::Approx::Error
Assert Near
Philiprehberger::Approx.assert_near(1.0, 1.0 + 1e-10)
# => nil (no error)
Philiprehberger::Approx.assert_near(1.0, 2.0)
# => raises Philiprehberger::Approx::Error
Percentage Tolerance
Philiprehberger::Approx.percent_equal?(100.0, 105.0, percent: 10)
# => true (5% difference is within 10% tolerance)
Philiprehberger::Approx.percent_equal?(100.0, 115.0, percent: 10)
# => false (15% difference exceeds 10% tolerance)
Supports arrays and hashes recursively. Returns true when both values are zero.
Diff Diagnostics
Philiprehberger::Approx.diff(1.0, 1.5, epsilon: 1.0)
# => { match: true, actual_diff: 0.5, allowed: 1.0, ratio: 0.5 }
Philiprehberger::Approx.diff(1.0, 3.0, epsilon: 1.0)
# => { match: false, actual_diff: 2.0, allowed: 1.0, ratio: 2.0 }
Returns a diagnostic hash showing the actual difference, the allowed tolerance, and the ratio between them.
RSpec Integration
require 'philiprehberger/approx'
RSpec.configure do |config|
config.include Philiprehberger::Approx::RSpecMatchers
end
RSpec.describe 'calculations' do
it 'is approximately equal' do
expect(1.0).to be_approx(1.0 + 1e-16)
expect(1.0).to be_approx(1.05, epsilon: 0.1)
end
it 'is within tolerance' do
expect(1.0).to be_approx_within(1.005, abs: 0.01)
expect(1_000_000.0).to be_approx_within(1_000_001.0, rel: 1e-5)
expect(100.0).to be_approx_within(105.0, percent: 10)
end
end
API
| Method | Description |
|---|---|
.equal?(a, b, epsilon: 1e-9) |
Check approximate equality within epsilon |
.near?(a, b, epsilon: 1e-9) |
Alias for .equal? |
.relative_equal?(a, b, tolerance: 1e-6) |
Check relative tolerance: `\ |
.within?(a, b, abs: nil, rel: nil) |
Combined mode: passes if either absolute or relative tolerance is met |
.clamp(value, target, epsilon: 1e-9) |
Return target if approximately equal, otherwise return value unchanged |
.assert_near(a, b, epsilon: 1e-9) |
Raise Error if values differ by more than epsilon |
.assert_within(a, b, abs: nil, rel: nil) |
Raise Error if values fail both tolerance checks |
.zero?(value, epsilon: 1e-9) |
Check if a numeric value is approximately zero |
.percent_equal?(a, b, percent:) |
Check approximate equality within a percentage tolerance |
.diff(a, b, epsilon: Float::EPSILON) |
Return diagnostic hash with match status, actual diff, allowed diff, and ratio |
.between?(value, min, max, epsilon: 1e-9) |
Check if value lies in [min, max] with epsilon slack |
.sign_equal?(a, b, epsilon: 1e-9) |
Check if two values share the same sign, treating near-zero values as zero |
RSpecMatchers#be_approx(expected, epsilon:) |
RSpec matcher for approximate equality |
RSpecMatchers#be_approx_within(expected, abs:, rel:, percent:) |
RSpec matcher with abs, rel, or percent tolerance |
Comparator.new(epsilon:, relative:) |
Reusable comparator with preset tolerances |
Comparator#equal?(a, b) |
Check equality using configured tolerances |
Comparator#near?(a, b) |
Alias for Comparator#equal? |
Comparator#within?(a, b) |
Check using combined absolute + relative configured tolerances |
Comparator#assert_near(a, b) |
Raise Error if values are not approximately equal |
Comparator#relative_equal?(a, b) |
Check relative tolerance using configured tolerances |
Comparator#clamp(value, target) |
Snap value to target using configured epsilon |
Comparator#zero?(value) |
Check if value is approximately zero using configured epsilon |
Comparator#between?(value, min, max) |
Check if value lies in range using configured epsilon |
Comparator#assert_within(a, b) |
Raise Error if values fail configured tolerance checks |
Error |
Error class raised by .assert_near (inherits StandardError) |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: