Sometimes, when testing whether the solution to a task (for example, here on Rosetta Code) is correct, the
difference in floating point calculations between different language implementations becomes significant.

For example, a difference between 32 bit and 64 bit floating point calculations may appear by
about the 8th significant digit in base 10 arithmetic.



Task

Create a function which returns true if two floating point numbers are approximately equal.



The function should allow for differences in the magnitude of numbers, so that, for example,

100000000000000.01   may be approximately equal to   100000000000000.011,

even though   100.01   is not approximately equal to   100.011.

If the language has such a feature in its standard library, this may be used instead of a custom function.

Show the function results with comparisons on the following pairs of values:

     100000000000000.01,   100000000000000.011     (note: should return true)
     100.01,   100.011                                                     (note: should return false)
     10000000000000.001 / 10000.0,   1000000000.0000001000
     0.001,   0.0010000001
     0.000000000000000000000101,   0.0
      sqrt(2) * sqrt(2),    2.0
     -sqrt(2) * sqrt(2),   -2.0
     3.14159265358979323846,   3.14159265358979324


Answers should be true for the first example and false in the second, so that just rounding the numbers to a fixed number of decimals should not be enough. Otherwise answers may vary and still be correct. See the Python code for one type of solution.




import Foundation
 
extension FloatingPoint {
  @inlinable
  public func isAlmostEqual(
    to other: Self,
    tolerance: Self = Self.ulpOfOne.squareRoot()
  ) -> Bool {
    // tolerances outside of [.ulpOfOne,1) yield well-defined but useless results,
    // so this is enforced by an assert rathern than a precondition.
    assert(tolerance >= .ulpOfOne && tolerance < 1, "tolerance should be in [.ulpOfOne, 1).")
 
    // The simple computation below does not necessarily give sensible
    // results if one of self or other is infinite; we need to rescale
    // the computation in that case.
    guard self.isFinite && other.isFinite else {
      return rescaledAlmostEqual(to: other, tolerance: tolerance)
    }
 
    // This should eventually be rewritten to use a scaling facility to be
    // defined on FloatingPoint suitable for hypot and scaled sums, but the
    // following is good enough to be useful for now.
    let scale = max(abs(self), abs(other), .leastNormalMagnitude)
    return abs(self - other) < scale*tolerance
  }
 
  @usableFromInline
  internal func rescaledAlmostEqual(to other: Self, tolerance: Self) -> Bool {
    // NaN is considered to be not approximately equal to anything, not even
    // itself.
    if self.isNaN || other.isNaN { return false }
    if self.isInfinite {
      if other.isInfinite { return self == other }
 
      // Self is infinite and other is finite. Replace self with the binade
      // of the greatestFiniteMagnitude, and reduce the exponent of other by
      // one to compensate.
      let scaledSelf = Self(sign: self.sign,
        exponent: Self.greatestFiniteMagnitude.exponent,
        significand: 1)
      let scaledOther = Self(sign: .plus,
        exponent: -1,
        significand: other)
 
      // Now both values are finite, so re-run the naive comparison.
      return scaledSelf.isAlmostEqual(to: scaledOther, tolerance: tolerance)
    }
 
    // If self is finite and other is infinite, flip order and use scaling
    // defined above, since this relation is symmetric.
    return other.rescaledAlmostEqual(to: self, tolerance: tolerance)
  }
}
 
let testCases = [
  (100000000000000.01, 100000000000000.011),
  (100.01, 100.011),
  (10000000000000.001 / 10000.0, 1000000000.0000001000),
  (0.001, 0.0010000001),
  (0.000000000000000000000101, 0.0),
  (sqrt(2) * sqrt(2), 2.0),
  (-sqrt(2) * sqrt(2), -2.0),
  (3.14159265358979323846, 3.14159265358979324)
]
 
for testCase in testCases {
  print("\(testCase.0), \(testCase.1) => \(testCase.0.isAlmostEqual(to: testCase.1))")
}