Skip to content

Commit

Permalink
Add trimmingSuffix, trimmingPrefix and mutating variants (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
fedeci authored May 25, 2021
1 parent e25cf27 commit 0e2941e
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 11 deletions.
21 changes: 20 additions & 1 deletion Guides/Trim.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ func myAlgorithm2<Input>(input: Input) where Input: BidirectionalCollection {
Swift provides the `BidirectionalCollection` protocol for marking types which support reverse traversal,
and generic types and algorithms which want to make use of that should add it to their constraints.

### >= 0.3.0

In `v0.3.0` new methods are added to allow discarding all the elements matching the predicate at the beginning (prefix) or at the ending (suffix) of the collection.
- `trimmingSuffix(while:)` can only be run on collections conforming to the `BidirectionalCollection` protocol.
- `trimmingPrefix(while:)` can be run also on collections conforming to the `Collection` protocol.

```swift
let myString = " hello, world "
print(myString.trimmingPrefix(while: \.isWhitespace)) // "hello, world "

print(myString.trimmingSuffix(while: \.isWhitespace)) // " hello, world"
```
Also mutating variants for all the methods already existing and the new ones are added.
```swift
var myString = " hello, world "
myString.trim(while: \.isWhitespace)
print(myString) // "hello, world"
```

### Complexity

Calling this method is O(_n_).
Expand Down Expand Up @@ -111,4 +130,4 @@ let result = input.dropFromBothEnds(while: { ... })

// No such ambiguity here.
let result = input.trimming(while: { ... })
```
```
218 changes: 215 additions & 3 deletions Sources/Algorithms/Trim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,94 @@
//
// See https://swift.org/LICENSE.txt for license information
//

//===----------------------------------------------------------------------===//
// trimmingPrefix(while:)
//===----------------------------------------------------------------------===//

extension Collection {
/// Returns a `SubSequence` formed by discarding all elements at the start
/// of the collection which satisfy the given predicate.
///
/// This example uses `trimmingPrefix(while:)` to get a substring without the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// print(myString.trimmingPrefix(while: \.isWhitespace)) // "hello, world "
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// omitted from the resulting slice.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
public func trimmingPrefix(
while predicate: (Element) throws -> Bool
) rethrows -> SubSequence {
let start = try endOfPrefix(while: predicate)
return self[start..<endIndex]
}
}

//===----------------------------------------------------------------------===//
// trimPrefix(while:)
//===----------------------------------------------------------------------===//

extension Collection where Self: RangeReplaceableCollection {
/// Mutates a `Collection` by discarding all elements at the start
/// of it which satisfy the given predicate.
///
/// This example uses `trimPrefix(while:)` to remove the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// myString.trimPrefix(while: \.isWhitespace)
/// print(myString) // "hello, world "
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// removed from the string.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
@_disfavoredOverload
public mutating func trimPrefix(
while predicate: (Element) throws -> Bool
) rethrows {
let end = try endOfPrefix(while: predicate)
removeSubrange(startIndex..<end)
}
}

extension Collection where Self == Self.SubSequence {
/// Mutates a `Collection` by discarding all elements at the start
/// of it which satisfy the given predicate.
///
/// This example uses `trimPrefix(while:)` to remove the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// myString.trimPrefix(while: \.isWhitespace)
/// print(myString) // "hello, world "
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// removed from the string.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
public mutating func trimPrefix(
while predicate: (Element) throws -> Bool
) rethrows {
self = try trimmingPrefix(while: predicate)
}
}

//===----------------------------------------------------------------------===//
// trimming(while:) / trimmingSuffix(while:)
//===----------------------------------------------------------------------===//

extension BidirectionalCollection {
Expand All @@ -29,8 +117,132 @@ extension BidirectionalCollection {
public func trimming(
while predicate: (Element) throws -> Bool
) rethrows -> SubSequence {
let start = try endOfPrefix(while: predicate)
let end = try self[start...].startOfSuffix(while: predicate)
return self[start..<end]
return try trimmingPrefix(while: predicate).trimmingSuffix(while: predicate)
}

/// Returns a `SubSequence` formed by discarding all elements at the end
/// of the collection which satisfy the given predicate.
///
/// This example uses `trimmingSuffix(while:)` to get a substring without the white
/// space at the end of the string:
///
/// let myString = " hello, world "
/// print(myString.trimmingSuffix(while: \.isWhitespace)) // " hello, world"
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// omitted from the resulting slice.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
public func trimmingSuffix(
while predicate: (Element) throws -> Bool
) rethrows -> SubSequence {
let end = try startOfSuffix(while: predicate)
return self[startIndex..<end]
}
}

//===----------------------------------------------------------------------===//
// trim(while:) / trimSuffix(while:)
//===----------------------------------------------------------------------===//

extension BidirectionalCollection where Self: RangeReplaceableCollection {
/// Mutates a `BidirectionalCollection` by discarding all elements at the start
/// and at the end of it which satisfy the given predicate.
///
/// This example uses `trim(while:)` to remove the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// myString.trim(while: \.isWhitespace)
/// print(myString) // "hello, world"
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// removed from the string.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
@_disfavoredOverload
public mutating func trim(
while predicate: (Element) throws -> Bool
) rethrows {
replaceSubrange(startIndex..<endIndex, with: try trimming(while: predicate))
}

/// Mutates a `BidirectionalCollection` by discarding all elements at the end
/// of it which satisfy the given predicate.
///
/// This example uses `trimSuffix(while:)` to remove the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// myString.trimSuffix(while: \.isWhitespace)
/// print(myString) // " hello, world"
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// removed from the string.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
@_disfavoredOverload
public mutating func trimSuffix(
while predicate: (Element) throws -> Bool
) rethrows {
let start = try startOfSuffix(while: predicate)
removeSubrange(start..<endIndex)
}
}

extension BidirectionalCollection where Self == Self.SubSequence {
/// Mutates a `BidirectionalCollection` by discarding all elements at the start
/// and at the end of it which satisfy the given predicate.
///
/// This example uses `trim(while:)` to remove the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// myString.trim(while: \.isWhitespace)
/// print(myString) // "hello, world"
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// removed from the string.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
public mutating func trim(
while predicate: (Element) throws -> Bool
) rethrows {
self = try trimming(while: predicate)
}

/// Mutates a `BidirectionalCollection` by discarding all elements at the end
/// of it which satisfy the given predicate.
///
/// This example uses `trimSuffix(while:)` to remove the white
/// space at the beginning of the string:
///
/// let myString = " hello, world "
/// myString.trimSuffix(while: \.isWhitespace)
/// print(myString) // " hello, world"
///
/// - Parameters:
/// - predicate: A closure which determines if the element should be
/// removed from the string.
///
/// - Complexity: O(*n*), where *n* is the length of this collection.
///
@inlinable
public mutating func trimSuffix(
while predicate: (Element) throws -> Bool
) rethrows {
self = try trimmingSuffix(while: predicate)
}
}
63 changes: 56 additions & 7 deletions Tests/SwiftAlgorithmsTests/TrimTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,96 @@ import Algorithms
import XCTest

final class TrimTests: XCTestCase {

func testEmpty() {
let results_empty = ([] as [Int]).trimming { $0.isMultiple(of: 2) }
XCTAssertEqual(results_empty, [])
}

func testNoMatch() {
// No match (nothing trimmed).
let results_nomatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming {
$0.isMultiple(of: 2)
}
XCTAssertEqual(results_nomatch, [1, 3, 5, 7, 9, 11, 13, 15])
}

func testNoTailMatch() {
// No tail match (only trim head).
let results_notailmatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { $0 < 10 }
XCTAssertEqual(results_notailmatch, [11, 13, 15])
}

func testNoHeadMatch() {
// No head match (only trim tail).
let results_noheadmatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { $0 > 10 }
XCTAssertEqual(results_noheadmatch, [1, 3, 5, 7, 9])
}

func testBothEndsMatch() {
// Both ends match, some string of >1 elements do not (return that string).
let results = [2, 10, 11, 15, 20, 21, 100].trimming { $0.isMultiple(of: 2) }
XCTAssertEqual(results, [11, 15, 20, 21])
}

func testEverythingMatches() {
// Everything matches (trim everything).
let results_allmatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { _ in true }
XCTAssertEqual(results_allmatch, [])
}

func testEverythingButOneMatches() {
// Both ends match, one element does not (trim all except that element).
let results_one = [2, 10, 12, 15, 20, 100].trimming { $0.isMultiple(of: 2) }
XCTAssertEqual(results_one, [15])
}

func testTrimmingPrefix() {
let results = [2, 10, 12, 15, 20, 100].trimmingPrefix { $0.isMultiple(of: 2) }
XCTAssertEqual(results, [15, 20, 100])
}

func testTrimmingSuffix() {
let results = [2, 10, 12, 15, 20, 100].trimmingSuffix { $0.isMultiple(of: 2) }
XCTAssertEqual(results, [2, 10, 12, 15])
}

// Self == Self.Subsequence
func testTrimNoAmbiguity() {
var values = [2, 10, 12, 15, 20, 100] as ArraySlice
values.trim { $0.isMultiple(of: 2) }
XCTAssertEqual(values, [15])
}

// Self == Self.Subsequence
func testTrimPrefixNoAmbiguity() {
var values = [2, 10, 12, 15, 20, 100] as ArraySlice
values.trimPrefix { $0.isMultiple(of: 2) }
XCTAssertEqual(values, [15, 20, 100])
}

// Self == Self.Subsequence
func testTrimSuffixNoAmbiguity() {
var values = [2, 10, 12, 15, 20, 100] as ArraySlice
values.trimSuffix { $0.isMultiple(of: 2) }
XCTAssertEqual(values, [2, 10, 12, 15])
}

func testTrimRangeReplaceable() {
var values = [2, 10, 12, 15, 20, 100]
values.trim { $0.isMultiple(of: 2) }
XCTAssertEqual(values, [15])
}

func testTrimPrefixRangeReplaceable() {
var values = [2, 10, 12, 15, 20, 100]
values.trimPrefix { $0.isMultiple(of: 2) }
XCTAssertEqual(values, [15, 20, 100])
}

func testTrimSuffixRangeReplaceable() {
var values = [2, 10, 12, 15, 20, 100]
values.trimSuffix { $0.isMultiple(of: 2) }
XCTAssertEqual(values, [2, 10, 12, 15])
}
}

0 comments on commit 0e2941e

Please sign in to comment.