diff --git a/CHANGELOG.md b/CHANGELOG.md index 6194fd5..4631854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Addedge -- None. +### Added +- New `Comparable.clamped(to:)` and `Comparable.clamp(to:)` interfaces for any `Comparable`, e. g. `Int`. ### Changed - None. ### Deprecated @@ -18,7 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - None. ## [3.0.0] - 2019-04-30 -### Addedge +### Added - New `Withable` protocol to init/copy objects and set properties in a convenient way on a single line. ### Changed - Upgraded to Swift 5 & Xcode 10.2. diff --git a/Frameworks/HandySwift/Extensions/ComparableExtension.swift b/Frameworks/HandySwift/Extensions/ComparableExtension.swift new file mode 100644 index 0000000..395918b --- /dev/null +++ b/Frameworks/HandySwift/Extensions/ComparableExtension.swift @@ -0,0 +1,86 @@ +// +// Created by Frederick Pietschmann on 07.05.19. +// Copyright © 2019 Flinesoft. All rights reserved. +// + +import Foundation + +extension Comparable { + // MARK: - Clamp: Returning Variants + /// Returns `self` clamped to the given limits. + /// + /// - Parameter limits: The closed range determining minimum & maxmimum value. + /// - Returns: + /// - `self`, if it is inside the given limits. + /// - `lowerBound` of the given limits, if `self` is smaller than it. + /// - `upperBound` of the given limits, if `self` is greater than it. + public func clamped(to limits: ClosedRange) -> Self { + if limits.lowerBound > self { + return limits.lowerBound + } else if limits.upperBound < self { + return limits.upperBound + } else { + return self + } + } + + /// Returns `self` clamped to the given limits. + /// + /// - Parameter limits: The partial range (from) determining the minimum value. + /// - Returns: + /// - `self`, if it is inside the given limits. + /// - `lowerBound` of the given limits, if `self` is smaller than it. + public func clamped(to limits: PartialRangeFrom) -> Self { + if limits.lowerBound > self { + return limits.lowerBound + } else { + return self + } + } + + /// Returns `self` clamped to the given limits. + /// + /// - Parameter limits: The partial range (through) determining the maximum value. + /// - Returns: + /// - `self`, if it is inside the given limits. + /// - `upperBound` of the given limits, if `self` is greater than it. + public func clamped(to limits: PartialRangeThrough) -> Self { + if limits.upperBound < self { + return limits.upperBound + } else { + return self + } + } + + // MARK: Mutating Variants + /// Clamps `self` to the given limits. + /// + /// - `self`, if it is inside the given limits. + /// - `lowerBound` of the given limits, if `self` is smaller than it. + /// - `upperBound` of the given limits, if `self` is greater than it. + /// + /// - Parameter limits: The closed range determining minimum & maxmimum value. + public mutating func clamp(to limits: ClosedRange) { + self = clamped(to: limits) + } + + /// Clamps `self` to the given limits. + /// + /// - `self`, if it is inside the given limits. + /// - `lowerBound` of the given limits, if `self` is smaller than it. + /// + /// - Parameter limits: The partial range (from) determining the minimum value. + public mutating func clamp(to limits: PartialRangeFrom) { + self = clamped(to: limits) + } + + /// Clamps `self` to the given limits. + /// + /// - `self`, if it is inside the given limits. + /// - `upperBound` of the given limits, if `self` is greater than it. + /// + /// - Parameter limits: The partial range (through) determining the maximum value. + public mutating func clamp(to limits: PartialRangeThrough) { + self = clamped(to: limits) + } +} diff --git a/HandySwift.xcodeproj/project.pbxproj b/HandySwift.xcodeproj/project.pbxproj index 8b00342..1a454cd 100644 --- a/HandySwift.xcodeproj/project.pbxproj +++ b/HandySwift.xcodeproj/project.pbxproj @@ -97,6 +97,12 @@ CC4AE0CF2087D8A7009931F6 /* RegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4AE0CD2087D895009931F6 /* RegexTests.swift */; }; CC4AE0D02087D8A8009931F6 /* RegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4AE0CD2087D895009931F6 /* RegexTests.swift */; }; CC4AE0D12087D8A9009931F6 /* RegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4AE0CD2087D895009931F6 /* RegexTests.swift */; }; + CC66E047228199A0007ABF61 /* ComparableExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66E046228199A0007ABF61 /* ComparableExtension.swift */; }; + CC66E048228199DE007ABF61 /* ComparableExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66E046228199A0007ABF61 /* ComparableExtension.swift */; }; + CC66E049228199DF007ABF61 /* ComparableExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66E046228199A0007ABF61 /* ComparableExtension.swift */; }; + CC66E12522819FFF007ABF61 /* ComparableExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66E12322819FCF007ABF61 /* ComparableExtensionTests.swift */; }; + CC66E1262281A000007ABF61 /* ComparableExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66E12322819FCF007ABF61 /* ComparableExtensionTests.swift */; }; + CC66E1272281A000007ABF61 /* ComparableExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC66E12322819FCF007ABF61 /* ComparableExtensionTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -170,6 +176,8 @@ C5CFB6AB20B0A70300830511 /* Unowned.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unowned.swift; sourceTree = ""; }; CC120CC1205FDB9300C37D7C /* Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Regex.swift; sourceTree = ""; }; CC4AE0CD2087D895009931F6 /* RegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegexTests.swift; sourceTree = ""; }; + CC66E046228199A0007ABF61 /* ComparableExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparableExtension.swift; sourceTree = ""; }; + CC66E12322819FCF007ABF61 /* ComparableExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparableExtensionTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -271,6 +279,7 @@ children = ( 82CAE2951C2EE64900F934A7 /* ArrayExtension.swift */, 3F95C8D120F22A3C0045AFD0 /* CollectionExtension.swift */, + CC66E046228199A0007ABF61 /* ComparableExtension.swift */, 8280D7DB1C4A6EC9001172EF /* DictionaryExtension.swift */, A1F221631E3CC05100419B06 /* DispatchTimeIntervalExtension.swift */, 823B2B4C1C24ABA4007B3CDD /* IntExtension.swift */, @@ -286,6 +295,7 @@ children = ( 82CAE2971C2EE95200F934A7 /* ArrayExtensionTests.swift */, 3F95C8D320F22DC60045AFD0 /* CollectionExtensionTests.swift */, + CC66E12322819FCF007ABF61 /* ComparableExtensionTests.swift */, 8280D7DF1C4A6FF3001172EF /* DictionaryExtensionTests.swift */, 827599631E520FB800787F99 /* DispatchTimeIntervalExtensionTests.swift */, 823B2B4F1C24AC00007B3CDD /* IntExtensionTests.swift */, @@ -687,6 +697,7 @@ 8218E4D62211D193007AAAF3 /* NSRangeExtension.swift in Sources */, 3F95C8D220F22A3C0045AFD0 /* CollectionExtension.swift in Sources */, 823B2B4D1C24ABA4007B3CDD /* IntExtension.swift in Sources */, + CC66E047228199A0007ABF61 /* ComparableExtension.swift in Sources */, C5C89B9420B0A0C10048B07C /* Weak.swift in Sources */, 8251AA2022786D460022B277 /* Withable.swift in Sources */, 82CAE2921C2ED1A200F934A7 /* StringExtension.swift in Sources */, @@ -713,6 +724,7 @@ 82812A9F1D06926800CD5B6C /* GlobalsTests.swift in Sources */, 8218E4DA2211D270007AAAF3 /* NSRangeExtensionTests.swift in Sources */, 8280D7E01C4A6FF3001172EF /* DictionaryExtensionTests.swift in Sources */, + CC66E12522819FFF007ABF61 /* ComparableExtensionTests.swift in Sources */, 3F95C8D520F22DEE0045AFD0 /* CollectionExtensionTests.swift in Sources */, CC4AE0CF2087D8A7009931F6 /* RegexTests.swift in Sources */, ); @@ -729,6 +741,7 @@ 8218E4D72211D193007AAAF3 /* NSRangeExtension.swift in Sources */, 825EFE051C33358400558497 /* StringExtension.swift in Sources */, 82E21E8E211AF9960061EB1B /* CollectionExtension.swift in Sources */, + CC66E048228199DE007ABF61 /* ComparableExtension.swift in Sources */, C5CFB6AF20B0A78F00830511 /* Weak.swift in Sources */, 8251AA2122786D460022B277 /* Withable.swift in Sources */, A1F221651E3CC05100419B06 /* DispatchTimeIntervalExtension.swift in Sources */, @@ -755,6 +768,7 @@ 82812AA01D06926800CD5B6C /* GlobalsTests.swift in Sources */, 8218E4DB2211D270007AAAF3 /* NSRangeExtensionTests.swift in Sources */, 8280D7E11C4A6FF3001172EF /* DictionaryExtensionTests.swift in Sources */, + CC66E1262281A000007ABF61 /* ComparableExtensionTests.swift in Sources */, 3F95C8D620F22DEF0045AFD0 /* CollectionExtensionTests.swift in Sources */, CC4AE0D02087D8A8009931F6 /* RegexTests.swift in Sources */, ); @@ -771,6 +785,7 @@ 8218E4D82211D193007AAAF3 /* NSRangeExtension.swift in Sources */, 825EFE0B1C33358500558497 /* StringExtension.swift in Sources */, 82E21E8F211AF9970061EB1B /* CollectionExtension.swift in Sources */, + CC66E049228199DF007ABF61 /* ComparableExtension.swift in Sources */, C5CFB6B020B0A79000830511 /* Weak.swift in Sources */, 8251AA2222786D460022B277 /* Withable.swift in Sources */, A1F221661E3CC05100419B06 /* DispatchTimeIntervalExtension.swift in Sources */, @@ -797,6 +812,7 @@ 82812AA11D06926800CD5B6C /* GlobalsTests.swift in Sources */, 8218E4DC2211D270007AAAF3 /* NSRangeExtensionTests.swift in Sources */, 8280D7E21C4A6FF3001172EF /* DictionaryExtensionTests.swift in Sources */, + CC66E1272281A000007ABF61 /* ComparableExtensionTests.swift in Sources */, 3F95C8D720F22DEF0045AFD0 /* CollectionExtensionTests.swift in Sources */, CC4AE0D12087D8A9009931F6 /* RegexTests.swift in Sources */, ); diff --git a/README.md b/README.md index f77b55d..5425872 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,39 @@ let intArray = 5.timesMake { Int(randomBelow: 1_000)! } // => [481, 16, 680, 87, 912] ``` +### ComparableExtension + +#### clamped(to:) + +Apply a limiting range as the bounds of a `Comparable`. +Supports `ClosedRange` (`a ... b`), `PartialRangeFrom` (`a...`) and `PartialRangeThrough` (`...b`) as the `limits`. + +``` Swift +let myNum = 3 +myNum.clamped(to: 0 ... 6) // => 3 +myNum.clamped(to: 0 ... 2) // => 2 +myNum.clamped(to: 4 ... 6) // => 4 +myNum.clamped(to: 5...) // => 4 +myNum.clamped(to: ...2) // => 2 + +let myString = "d" +myString.clamped(to: "a" ... "g") // => "d" +myString.clamped(to: "a" ... "c") // => "c" +myString.clamped(to: "e" ... "g") // => "e" +myString.clamped(to: "f"...) // => "f" +myString.clamped(to: ..."c") // => "c" +``` + +#### clamp(to:) + +In-place `mutating` variant of `clamped(to:)` + +``` Swift +var myNum = 3 +myNum.clamp(to: 0...2) +myNum // => 2 +``` + ### StringExtension #### .stripped() diff --git a/Tests/HandySwiftTests/Extensions/ComparableExtensionTests.swift b/Tests/HandySwiftTests/Extensions/ComparableExtensionTests.swift new file mode 100644 index 0000000..b1ee0c4 --- /dev/null +++ b/Tests/HandySwiftTests/Extensions/ComparableExtensionTests.swift @@ -0,0 +1,119 @@ +// +// Created by Frederick Pietschmann on 07.05.19. +// Copyright © 2019 Flinesoft. All rights reserved. +// + +import Foundation + +@testable import HandySwift +import XCTest + +class ComparableExtensionTests: XCTestCase { + // MARK: Returning Variants + func testClampedClosedRange() { + let myNum = 3 + XCTAssertEqual(myNum.clamped(to: 0 ... 4), 3) + XCTAssertEqual(myNum.clamped(to: 0 ... 2), 2) + XCTAssertEqual(myNum.clamped(to: 4 ... 6), 4) + + let myString = "d" + XCTAssertEqual(myString.clamped(to: "a" ... "e"), "d") + XCTAssertEqual(myString.clamped(to: "a" ... "c"), "c") + XCTAssertEqual(myString.clamped(to: "e" ... "g"), "e") + } + + func testClampedPartialRangeFrom() { + let myNum = 3 + XCTAssertEqual(myNum.clamped(to: 2...), 3) + XCTAssertEqual(myNum.clamped(to: 4...), 4) + + let myString = "d" + XCTAssertEqual(myString.clamped(to: "c"...), "d") + XCTAssertEqual(myString.clamped(to: "e"...), "e") + } + + func testClampedPartialRangeThrough() { + let myNum = 3 + XCTAssertEqual(myNum.clamped(to: ...4), 3) // swiftlint:disable:this tuple_index + XCTAssertEqual(myNum.clamped(to: ...2), 2) // swiftlint:disable:this tuple_index + + let myString = "d" + XCTAssertEqual(myString.clamped(to: ..."e"), "d") + XCTAssertEqual(myString.clamped(to: ..."c"), "c") + } + + // MARK: Mutating Variants + func testClampClosedRange() { + let myNum = 3 + + var myNumCopy = myNum + myNumCopy.clamp(to: 0 ... 4) + XCTAssertEqual(myNumCopy, 3) + + myNumCopy = myNum + myNumCopy.clamp(to: 0 ... 2) + XCTAssertEqual(myNumCopy, 2) + + myNumCopy = myNum + myNumCopy.clamp(to: 4 ... 6) + XCTAssertEqual(myNumCopy, 4) + + let myString = "d" + + var myStringCopy = myString + myStringCopy.clamp(to: "a" ... "e") + XCTAssertEqual(myStringCopy, "d") + + myStringCopy = myString + myStringCopy.clamp(to: "a" ... "c") + XCTAssertEqual(myStringCopy, "c") + + myStringCopy = myString + myStringCopy.clamp(to: "e" ... "g") + XCTAssertEqual(myStringCopy, "e") + } + + func testClampPartialRangeFrom() { + let myNum = 3 + + var myNumCopy = myNum + myNumCopy.clamp(to: 2...) + XCTAssertEqual(myNumCopy, 3) + + myNumCopy = myNum + myNumCopy.clamp(to: 4...) + XCTAssertEqual(myNumCopy, 4) + + let myString = "d" + + var myStringCopy = myString + myStringCopy.clamp(to: "c"...) + XCTAssertEqual(myStringCopy, "d") + + myStringCopy = myString + myStringCopy.clamp(to: "e"...) + XCTAssertEqual(myStringCopy, "e") + } + + func testClampPartialRangeThrough() { + let myNum = 3 + + var myNumCopy = myNum + myNumCopy.clamp(to: ...4) // swiftlint:disable:this tuple_index + XCTAssertEqual(myNumCopy, 3) + + myNumCopy = myNum + myNumCopy.clamp(to: ...2) // swiftlint:disable:this tuple_index + XCTAssertEqual(myNumCopy, 2) + + let myString = "d" + + var myStringCopy = myString + myStringCopy.clamp(to: ..."e") + XCTAssertEqual(myStringCopy, "d") + + myStringCopy = myString + myStringCopy.clamp(to: ..."c") + XCTAssertEqual(myStringCopy, "c") + } +} diff --git a/UsageExamples.playground/Contents.swift b/UsageExamples.playground/Contents.swift index 605a5ce..a1a2e5b 100644 --- a/UsageExamples.playground/Contents.swift +++ b/UsageExamples.playground/Contents.swift @@ -51,6 +51,32 @@ stringArray let intArray = 5.timesMake { Int(randomBelow: 1_000)! } +//: ## ComparableExtension +//: ### clamped(to:) +//: Apply a limiting range as the bounds of a `Comparable`. +//: Supports `ClosedRange` (`a ... b`), `PartialRangeFrom` (`a...`) and `PartialRangeThrough` (`...b`) as the `limits`. + +let myNum = 3 +myNum.clamped(to: 0 ... 6) +myNum.clamped(to: 0 ... 2) +myNum.clamped(to: 4 ... 6) +myNum.clamped(to: 5...) +myNum.clamped(to: ...2) + +let myString = "d" +myString.clamped(to: "a" ... "g") +myString.clamped(to: "a" ... "c") +myString.clamped(to: "e" ... "g") +myString.clamped(to: "f"...) +myString.clamped(to: ..."c") + +//: ### clamp(to:) +//: In-place `mutating` variant of `clamped(to:)` + +var myNum = 3 +myNum.clamp(to: 0 ... 2) +myNum + //: ## StringExtension //: ### string.strip //: Returns string with whitespace characters stripped from start and end.