From 7cbcc2d9bf1c55d627beea7e1910cb1d0ce310c9 Mon Sep 17 00:00:00 2001 From: Wade Tregaskis Date: Wed, 6 Mar 2024 17:41:34 -0800 Subject: [PATCH] Added `clamp` / `clamped` for Comparables. --- README.md | 5 + Sources/FoundationExtensions/Comparable.swift | 83 ++++++++++++ .../ComparableTests.swift | 126 ++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 Sources/FoundationExtensions/Comparable.swift create mode 100644 Tests/FoundationExtensionsTests/ComparableTests.swift diff --git a/README.md b/README.md index 9ccbe87..11d69f9 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ A miscellaneous collection of extensions to Apple's Foundation framework. * `asHexString(uppercase:delimiterEvery:delimiter:)` formats a `Data` into hex (as a `String`), e.g. `Data(bytes: "woot", count: 4).asHexString(delimiterEvery: 1)` -> `"77 6F 6F 74"`. * It also has a shorthand version `asHexString` for convenience (omitting the parentheses), if you don't need to customise its defaults. + +### Comparable (e.g. numbers, strings, anything you can put into a range, etc). + +* `clamp` and `clamped` to conform a value into a given range (supporting all finite range types), e.g. `5.clamped(..<0)` -> `-1`. + * Note: up-to-but-not-including ranges (`..<`) are only supported on types that are _also_ `Strideable`. ### Data diff --git a/Sources/FoundationExtensions/Comparable.swift b/Sources/FoundationExtensions/Comparable.swift new file mode 100644 index 0000000..02167f8 --- /dev/null +++ b/Sources/FoundationExtensions/Comparable.swift @@ -0,0 +1,83 @@ +// Created by Wade Tregaskis on 2024-03-06 + +extension Comparable { + mutating func clamp(_ range: PartialRangeFrom) { + if self < range.lowerBound { + self = range.lowerBound + } + } + + func clamped(_ range: PartialRangeFrom) -> Self { + if self < range.lowerBound { + range.lowerBound + } else { + self + } + } + + mutating func clamp(_ range: PartialRangeThrough) { + if self > range.upperBound { + self = range.upperBound + } + } + + func clamped(_ range: PartialRangeThrough) -> Self { + if self > range.upperBound { + range.upperBound + } else { + self + } + } + + mutating func clamp(_ range: ClosedRange) { + if self < range.lowerBound { + self = range.lowerBound + } else if self > range.upperBound { + self = range.upperBound + } + } + + func clamped(_ range: ClosedRange) -> Self { + if self < range.lowerBound { + range.lowerBound + } else if self > range.upperBound { + range.upperBound + } else { + self + } + } +} + +extension Comparable where Self: Strideable { + mutating func clamp(_ range: Range) { + if self < range.lowerBound { + self = range.lowerBound + } else if self >= range.upperBound { + self = range.upperBound.advanced(by: -1) + } + } + + func clamped(_ range: Range) -> Self { + if self < range.lowerBound { + range.lowerBound + } else if self >= range.upperBound { + range.upperBound.advanced(by: -1) + } else { + self + } + } + + mutating func clamp(_ range: PartialRangeUpTo) { + if self >= range.upperBound { + self = range.upperBound.advanced(by: -1) + } + } + + func clamped(_ range: PartialRangeUpTo) -> Self { + if self >= range.upperBound { + range.upperBound.advanced(by: -1) + } else { + self + } + } +} diff --git a/Tests/FoundationExtensionsTests/ComparableTests.swift b/Tests/FoundationExtensionsTests/ComparableTests.swift new file mode 100644 index 0000000..5938731 --- /dev/null +++ b/Tests/FoundationExtensionsTests/ComparableTests.swift @@ -0,0 +1,126 @@ +// Created by Wade Tregaskis on 2024-03-02. + +import XCTest +@testable import FoundationExtensions + + +final class ComparableTests: XCTestCase { + func testClamp_PartialRangeFrom() throws { + var x = 5 + + x.clamp(0...) + XCTAssertEqual(x, 5) + + x.clamp(5...) + XCTAssertEqual(x, 5) + + x.clamp(6...) + XCTAssertEqual(x, 6) + + x.clamp(50...) + XCTAssertEqual(x, 50) + } + + func testClamped_PartialRangeFrom() throws { + XCTAssertEqual(5.clamped(0...), 5) + XCTAssertEqual(5.clamped(5...), 5) + XCTAssertEqual(5.clamped(6...), 6) + XCTAssertEqual(5.clamped(50...), 50) + } + + func testClamp_PartialRangeThrough() throws { + var x = 5 + + x.clamp(...10) + XCTAssertEqual(x, 5) + + x.clamp(...5) + XCTAssertEqual(x, 5) + + x.clamp(...4) + XCTAssertEqual(x, 4) + + x.clamp(...0) + XCTAssertEqual(x, 0) + } + + func testClamped_PartialRangeThrough() throws { + XCTAssertEqual(5.clamped(...10), 5) + XCTAssertEqual(5.clamped(...5), 5) + XCTAssertEqual(5.clamped(...4), 4) + XCTAssertEqual(5.clamped(...0), 0) + } + + func testClamp_ClosedRange() throws { + var x = 5 + + x.clamp(0...10) + XCTAssertEqual(x, 5) + + x.clamp(0...5) + XCTAssertEqual(x, 5) + + x.clamp(5...10) + XCTAssertEqual(x, 5) + + x.clamp(0...4) + XCTAssertEqual(x, 4) + + x.clamp(5...10) + XCTAssertEqual(x, 5) + } + + func testClamped_ClosedRange() throws { + XCTAssertEqual(5.clamped(0...10), 5) + XCTAssertEqual(5.clamped(0...5), 5) + XCTAssertEqual(5.clamped(5...10), 5) + XCTAssertEqual(5.clamped(0...4), 4) + XCTAssertEqual(5.clamped(6...10), 6) + } + + func testClamp_Range() throws { + var x = 5 + + x.clamp(0..<10) + XCTAssertEqual(x, 5) + + x.clamp(0..<6) + XCTAssertEqual(x, 5) + + x.clamp(0..<5) + XCTAssertEqual(x, 4) + + x.clamp(4..<10) + XCTAssertEqual(x, 4) + + x.clamp(5..<10) + XCTAssertEqual(x, 5) + } + + func testClamped_Range() throws { + XCTAssertEqual(5.clamped(0..<10), 5) + XCTAssertEqual(5.clamped(0..<6), 5) + XCTAssertEqual(5.clamped(0..<5), 4) + XCTAssertEqual(5.clamped(5..<10), 5) + XCTAssertEqual(5.clamped(6..<10), 6) + } + + func testClamp_PartialRangeUpTo() throws { + var x = 5 + + x.clamp(..<10) + XCTAssertEqual(x, 5) + + x.clamp(..<6) + XCTAssertEqual(x, 5) + + x.clamp(..<5) + XCTAssertEqual(x, 4) + } + + func testClamped_PartialRangeUpTo() throws { + XCTAssertEqual(5.clamped(..<10), 5) + XCTAssertEqual(5.clamped(..<6), 5) + XCTAssertEqual(5.clamped(..<5), 4) + } +}