Skip to content

Commit d43e243

Browse files
authored
Merge pull request #1 from RougeWare/feature/Equals-with-tolerance
Protocol-driven equality with tolerance
2 parents 430375e + 83f1cc5 commit d43e243

File tree

7 files changed

+213
-20
lines changed

7 files changed

+213
-20
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
11
# Swift Basic Math Tools #
22

33
Some basic tools for doing math in Swift
4+
5+
6+
7+
# `TolerablyEqual` #
8+
9+
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.
10+
11+
Let's look at this classic example:
12+
```swift
13+
let shouldBeOne = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1
14+
15+
print(shouldBeOne, shouldBeOne == 1)
16+
// prints 0.9999999999999999 false
17+
```
18+
19+
20+
With this package, you can use the `~==` operator to easily overcome this problem
21+
```swift
22+
print(shouldBeOne ~== 1)
23+
// prints true
24+
```
25+
26+
27+
You can also customize this behavior as needed:
28+
```swift
29+
print(1_000_000.equals(1_000_100, tolerance: 1_000))
30+
// prints true
31+
```
32+
33+
Feel free to check out the tests for more examples!

Sources/BasicMathTools/BasicMathTools.swift

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//
2+
// Equality with tolerance.swift
3+
//
4+
//
5+
// Created by Ben Leggiero on 2020-08-02.
6+
//
7+
8+
import Foundation
9+
import CoreGraphics.CGBase
10+
11+
12+
13+
/// A type which can have its equality compared within a certain tolerance
14+
public protocol TolerablyEqual {
15+
16+
/// Determines whether this number is equal to the other one, within the given tolerance.
17+
///
18+
/// For example:
19+
/// ```swift
20+
/// 123.456.equals(123.457, tolerance: 0.01) == true
21+
/// 123.456.equals(123.457, tolerance: 0.001) == true
22+
/// 123.456.equals(123.458, tolerance: 0.001) == false
23+
/// 1_000_000.equals(1_000_100, tolerance: 1_000) == true
24+
/// 1_000_000.equals(1_100_000, tolerance: 1_000) == false
25+
/// ```
26+
///
27+
///
28+
/// - Parameters:
29+
/// - other: The other number to compare to this one
30+
/// - tolerance: The maximum distance which `other` is allowed to be away from `self`
31+
///
32+
/// - Returns: `true` iff `self` and `other` are, at most `tolerance` apart from each other
33+
func equals(_ other: Self, tolerance: Self) -> Bool
34+
35+
36+
/// The tolerance in equality-comparisons used when none is specified. Setting this will determine how all future
37+
/// tolerably-equal checks behave.
38+
static var defaultTolerance: Self { get set }
39+
40+
41+
/// Determines whether the left umber is equal to the right, within the default tolderance.
42+
///
43+
/// To read and change the default tolerance, use the static `defaultTolerance` variable.
44+
///
45+
/// - Parameters:
46+
/// - lhs: The first value to compare
47+
/// - rhs: The value to compare to the first value, within the default tolerance
48+
static func ~==(lhs: Self, rhs: Self) -> Bool
49+
}
50+
51+
52+
53+
// MARK: - Operator
54+
55+
/// Indicates approximate equality. For example, `1.0 ~== 1.000000001`
56+
infix operator ~== : ComparisonPrecedence
57+
58+
59+
60+
// MARK: Synthesis
61+
62+
public extension TolerablyEqual {
63+
static func ~==(lhs: Self, rhs: Self) -> Bool {
64+
lhs.equals(rhs, tolerance: defaultTolerance)
65+
}
66+
}
67+
68+
69+
70+
public extension TolerablyEqual
71+
where Self: SignedNumeric,
72+
Self: Comparable
73+
{
74+
func equals(_ other: Self, tolerance: Self) -> Bool {
75+
return abs(self - other) <= tolerance
76+
}
77+
}
78+
79+
80+
81+
// MARK: Default Conformances
82+
83+
extension CGFloat: TolerablyEqual { public static var defaultTolerance = CGFloat(CGFloat.NativeType.defaultTolerance) }
84+
extension Float32: TolerablyEqual { public static var defaultTolerance: Float32 = 0.0001 }
85+
extension Float64: TolerablyEqual { public static var defaultTolerance: Float64 = 0.00001 }
86+
extension Float80: TolerablyEqual { public static var defaultTolerance: Float80 = 0.000001 }
87+
88+
extension Int: TolerablyEqual { public static var defaultTolerance: Int = 0 }
89+
extension Int8: TolerablyEqual { public static var defaultTolerance: Int8 = 0 }
90+
extension Int16: TolerablyEqual { public static var defaultTolerance: Int16 = 0 }
91+
extension Int32: TolerablyEqual { public static var defaultTolerance: Int32 = 0 }
92+
extension Int64: TolerablyEqual { public static var defaultTolerance: Int64 = 0 }

Tests/BasicMathToolsTests/BasicMathToolsTests.swift

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// TolerablyEqual Tests.swift
3+
//
4+
//
5+
// Created by Ben Leggiero on 2020-08-02.
6+
//
7+
8+
import XCTest
9+
import BasicMathTools
10+
11+
12+
13+
private let originalFloat64DefaultTolerance = Float64.defaultTolerance
14+
15+
16+
17+
final class TolerablyEqual_Tests: XCTestCase {
18+
19+
override func setUp() {
20+
Float64.defaultTolerance = originalFloat64DefaultTolerance
21+
}
22+
23+
24+
func testEqualsWithTolerance() {
25+
XCTAssertTrue(123.456.equals(123.457, tolerance: 0.01))
26+
XCTAssertTrue(123.456.equals(123.457, tolerance: 0.001))
27+
XCTAssertFalse(123.456.equals(123.458, tolerance: 0.001))
28+
XCTAssertTrue(1_000_000.equals(1_000_100, tolerance: 1_000))
29+
XCTAssertFalse(1_000_000.equals(1_100_000, tolerance: 1_000))
30+
}
31+
32+
33+
func testTildeDoubleEquals_Float64() {
34+
35+
XCTAssertFalse(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1)
36+
XCTAssertTrue (0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 ~== 1)
37+
38+
XCTAssertFalse(2 == sqrt(2) * sqrt(2))
39+
XCTAssertTrue (2 ~== sqrt(2) * sqrt(2))
40+
41+
XCTAssertTrue(5 ~== 4.9999999)
42+
XCTAssertTrue(5 ~== 5)
43+
XCTAssertTrue(5 ~== 5.0000001)
44+
}
45+
46+
47+
func testDefaultTolerance_Float64() {
48+
49+
XCTAssertEqual(Float64.defaultTolerance, 0.00001)
50+
51+
XCTAssertFalse(5 ~== 4)
52+
XCTAssertFalse(5 ~== 4.9)
53+
XCTAssertFalse(5 ~== 4.99)
54+
XCTAssertFalse(5 ~== 4.999)
55+
XCTAssertFalse(5 ~== 4.9999)
56+
XCTAssertTrue(5 ~== 4.99999)
57+
XCTAssertTrue(5 ~== 4.999999)
58+
XCTAssertTrue(5 ~== 5)
59+
XCTAssertTrue(5 ~== 5.000001)
60+
XCTAssertTrue(5 ~== 5.00001)
61+
XCTAssertFalse(5 ~== 5.0001)
62+
XCTAssertFalse(5 ~== 5.001)
63+
XCTAssertFalse(5 ~== 5.01)
64+
XCTAssertFalse(5 ~== 5.1)
65+
XCTAssertFalse(5 ~== 6)
66+
67+
XCTAssertFalse(5 ~== 4.99998)
68+
XCTAssertFalse(5 ~== 5.00002)
69+
70+
Float64.defaultTolerance = 0.1
71+
72+
XCTAssertEqual(Float64.defaultTolerance, 0.1)
73+
XCTAssertTrue(5 ~== 5.0000001)
74+
XCTAssertTrue(5 ~== 5.000001)
75+
XCTAssertTrue(5 ~== 5.00001)
76+
XCTAssertTrue(5 ~== 5.0001)
77+
XCTAssertTrue(5 ~== 5.001)
78+
XCTAssertTrue(5 ~== 5.01)
79+
XCTAssertTrue(5 ~== 5.1)
80+
XCTAssertFalse(5 ~== 6)
81+
}
82+
83+
84+
static var allTests = [
85+
("testEqualsWithTolerance", testEqualsWithTolerance),
86+
("testTildeDoubleEquals_Float64", testTildeDoubleEquals_Float64),
87+
("testDefaultTolerance_Float64", testDefaultTolerance_Float64),
88+
]
89+
}

Tests/BasicMathToolsTests/XCTestManifests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import XCTest
33
#if !canImport(ObjectiveC)
44
public func allTests() -> [XCTestCaseEntry] {
55
return [
6-
testCase(BasicMathToolsTests.allTests),
6+
testCase(TolerablyEqual_Tests.allTests),
77
]
88
}
99
#endif

Tests/LinuxMain.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import XCTest
33
import BasicMathToolsTests
44

55
var tests = [XCTestCaseEntry]()
6-
tests += BasicMathToolsTests.allTests()
6+
tests += TolerablyEqual_Tests.allTests
77
XCTMain(tests)

0 commit comments

Comments
 (0)