Skip to content

Commit

Permalink
Merge pull request #1 from RougeWare/feature/Equals-with-tolerance
Browse files Browse the repository at this point in the history
Protocol-driven equality with tolerance
  • Loading branch information
KyNorthstar authored Aug 4, 2020
2 parents 430375e + 83f1cc5 commit d43e243
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 20 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# Swift Basic Math Tools #

Some basic tools for doing math in Swift



# `TolerablyEqual` #

This is a protocol that is applied to all the language's built-in signed numbers, but which can be applied to anything, which lets you compare two values for equality, within a certain tolerance.

Let's look at this classic example:
```swift
let shouldBeOne = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1

print(shouldBeOne, shouldBeOne == 1)
// prints 0.9999999999999999 false
```


With this package, you can use the `~==` operator to easily overcome this problem
```swift
print(shouldBeOne ~== 1)
// prints true
```


You can also customize this behavior as needed:
```swift
print(1_000_000.equals(1_000_100, tolerance: 1_000))
// prints true
```

Feel free to check out the tests for more examples!
3 changes: 0 additions & 3 deletions Sources/BasicMathTools/BasicMathTools.swift

This file was deleted.

92 changes: 92 additions & 0 deletions Sources/BasicMathTools/TolerablyEqual.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// Equality with tolerance.swift
//
//
// Created by Ben Leggiero on 2020-08-02.
//

import Foundation
import CoreGraphics.CGBase



/// A type which can have its equality compared within a certain tolerance
public protocol TolerablyEqual {

/// Determines whether this number is equal to the other one, within the given tolerance.
///
/// For example:
/// ```swift
/// 123.456.equals(123.457, tolerance: 0.01) == true
/// 123.456.equals(123.457, tolerance: 0.001) == true
/// 123.456.equals(123.458, tolerance: 0.001) == false
/// 1_000_000.equals(1_000_100, tolerance: 1_000) == true
/// 1_000_000.equals(1_100_000, tolerance: 1_000) == false
/// ```
///
///
/// - Parameters:
/// - other: The other number to compare to this one
/// - tolerance: The maximum distance which `other` is allowed to be away from `self`
///
/// - Returns: `true` iff `self` and `other` are, at most `tolerance` apart from each other
func equals(_ other: Self, tolerance: Self) -> Bool


/// The tolerance in equality-comparisons used when none is specified. Setting this will determine how all future
/// tolerably-equal checks behave.
static var defaultTolerance: Self { get set }


/// Determines whether the left umber is equal to the right, within the default tolderance.
///
/// To read and change the default tolerance, use the static `defaultTolerance` variable.
///
/// - Parameters:
/// - lhs: The first value to compare
/// - rhs: The value to compare to the first value, within the default tolerance
static func ~==(lhs: Self, rhs: Self) -> Bool
}



// MARK: - Operator

/// Indicates approximate equality. For example, `1.0 ~== 1.000000001`
infix operator ~== : ComparisonPrecedence



// MARK: Synthesis

public extension TolerablyEqual {
static func ~==(lhs: Self, rhs: Self) -> Bool {
lhs.equals(rhs, tolerance: defaultTolerance)
}
}



public extension TolerablyEqual
where Self: SignedNumeric,
Self: Comparable
{
func equals(_ other: Self, tolerance: Self) -> Bool {
return abs(self - other) <= tolerance
}
}



// MARK: Default Conformances

extension CGFloat: TolerablyEqual { public static var defaultTolerance = CGFloat(CGFloat.NativeType.defaultTolerance) }
extension Float32: TolerablyEqual { public static var defaultTolerance: Float32 = 0.0001 }
extension Float64: TolerablyEqual { public static var defaultTolerance: Float64 = 0.00001 }
extension Float80: TolerablyEqual { public static var defaultTolerance: Float80 = 0.000001 }

extension Int: TolerablyEqual { public static var defaultTolerance: Int = 0 }
extension Int8: TolerablyEqual { public static var defaultTolerance: Int8 = 0 }
extension Int16: TolerablyEqual { public static var defaultTolerance: Int16 = 0 }
extension Int32: TolerablyEqual { public static var defaultTolerance: Int32 = 0 }
extension Int64: TolerablyEqual { public static var defaultTolerance: Int64 = 0 }
15 changes: 0 additions & 15 deletions Tests/BasicMathToolsTests/BasicMathToolsTests.swift

This file was deleted.

89 changes: 89 additions & 0 deletions Tests/BasicMathToolsTests/TolerablyEqual Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// TolerablyEqual Tests.swift
//
//
// Created by Ben Leggiero on 2020-08-02.
//

import XCTest
import BasicMathTools



private let originalFloat64DefaultTolerance = Float64.defaultTolerance



final class TolerablyEqual_Tests: XCTestCase {

override func setUp() {
Float64.defaultTolerance = originalFloat64DefaultTolerance
}


func testEqualsWithTolerance() {
XCTAssertTrue(123.456.equals(123.457, tolerance: 0.01))
XCTAssertTrue(123.456.equals(123.457, tolerance: 0.001))
XCTAssertFalse(123.456.equals(123.458, tolerance: 0.001))
XCTAssertTrue(1_000_000.equals(1_000_100, tolerance: 1_000))
XCTAssertFalse(1_000_000.equals(1_100_000, tolerance: 1_000))
}


func testTildeDoubleEquals_Float64() {

XCTAssertFalse(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1)
XCTAssertTrue (0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 ~== 1)

XCTAssertFalse(2 == sqrt(2) * sqrt(2))
XCTAssertTrue (2 ~== sqrt(2) * sqrt(2))

XCTAssertTrue(5 ~== 4.9999999)
XCTAssertTrue(5 ~== 5)
XCTAssertTrue(5 ~== 5.0000001)
}


func testDefaultTolerance_Float64() {

XCTAssertEqual(Float64.defaultTolerance, 0.00001)

XCTAssertFalse(5 ~== 4)
XCTAssertFalse(5 ~== 4.9)
XCTAssertFalse(5 ~== 4.99)
XCTAssertFalse(5 ~== 4.999)
XCTAssertFalse(5 ~== 4.9999)
XCTAssertTrue(5 ~== 4.99999)
XCTAssertTrue(5 ~== 4.999999)
XCTAssertTrue(5 ~== 5)
XCTAssertTrue(5 ~== 5.000001)
XCTAssertTrue(5 ~== 5.00001)
XCTAssertFalse(5 ~== 5.0001)
XCTAssertFalse(5 ~== 5.001)
XCTAssertFalse(5 ~== 5.01)
XCTAssertFalse(5 ~== 5.1)
XCTAssertFalse(5 ~== 6)

XCTAssertFalse(5 ~== 4.99998)
XCTAssertFalse(5 ~== 5.00002)

Float64.defaultTolerance = 0.1

XCTAssertEqual(Float64.defaultTolerance, 0.1)
XCTAssertTrue(5 ~== 5.0000001)
XCTAssertTrue(5 ~== 5.000001)
XCTAssertTrue(5 ~== 5.00001)
XCTAssertTrue(5 ~== 5.0001)
XCTAssertTrue(5 ~== 5.001)
XCTAssertTrue(5 ~== 5.01)
XCTAssertTrue(5 ~== 5.1)
XCTAssertFalse(5 ~== 6)
}


static var allTests = [
("testEqualsWithTolerance", testEqualsWithTolerance),
("testTildeDoubleEquals_Float64", testTildeDoubleEquals_Float64),
("testDefaultTolerance_Float64", testDefaultTolerance_Float64),
]
}
2 changes: 1 addition & 1 deletion Tests/BasicMathToolsTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(BasicMathToolsTests.allTests),
testCase(TolerablyEqual_Tests.allTests),
]
}
#endif
2 changes: 1 addition & 1 deletion Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import XCTest
import BasicMathToolsTests

var tests = [XCTestCaseEntry]()
tests += BasicMathToolsTests.allTests()
tests += TolerablyEqual_Tests.allTests
XCTMain(tests)

0 comments on commit d43e243

Please sign in to comment.