Skip to content

Commit f11d398

Browse files
authored
Add BidirectionalCollection.trimming (#4)
1 parent 34c0a0b commit f11d398

File tree

4 files changed

+224
-0
lines changed

4 files changed

+224
-0
lines changed

Guides/Trim.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Trim
2+
3+
[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Trim.swift) |
4+
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/TrimTests.swift)]
5+
6+
Returns a `SubSequence` formed by discarding all elements at the start and end of the collection
7+
which satisfy the given predicate.
8+
9+
This example uses `trimming(where:)` to get a substring without the white space at the beginning and end of the string.
10+
11+
```swift
12+
let myString = " hello, world "
13+
print(myString.trimming(where: \.isWhitespace)) // "hello, world"
14+
15+
let results = [2, 10, 11, 15, 20, 21, 100].trimming(where: { $0.isMultiple(of: 2) })
16+
print(results) // [11, 15, 20, 21]
17+
```
18+
19+
## Detailed Design
20+
21+
A new method is added to `BidirectionalCollection`:
22+
23+
```swift
24+
extension BidirectionalCollection {
25+
26+
public func trimming(where predicate: (Element) throws -> Bool) rethrows -> SubSequence
27+
}
28+
```
29+
30+
This method requires `BidirectionalCollection` for an efficient implementation which visits as few elements as possible.
31+
32+
A less-efficient implementation is _possible_ for any `Collection`, which would involve always traversing the
33+
entire collection. This implementation is not provided, as it would mean developers of generic algorithms who forget
34+
to add the `BidirectionalCollection` constraint will receive that inefficient implementation:
35+
36+
```swift
37+
func myAlgorithm<Input>(input: Input) where Input: Collection {
38+
39+
let trimmedInput = input.trimming(where: { ... }) // Uses least-efficient implementation.
40+
}
41+
42+
func myAlgorithm2<Input>(input: Input) where Input: BidirectionalCollection {
43+
44+
let trimmedInput = input.trimming(where: { ... }) // Uses most-efficient implementation.
45+
}
46+
```
47+
48+
Swift provides the `BidirectionalCollection` protocol for marking types which support reverse traversal,
49+
and generic types and algorithms which want to make use of that should add it to their constraints.
50+
51+
### Complexity
52+
53+
Calling this method is O(_n_).
54+
55+
### Naming
56+
57+
The name `trim` has precedent in other programming languages. Another popular alternative might be `strip`.
58+
59+
| Example usage | Languages |
60+
|-|-|
61+
| ''String''.Trim([''chars'']) | C#, VB.NET, Windows PowerShell |
62+
| ''string''.strip(); | D |
63+
| (.trim ''string'') | Clojure |
64+
| ''sequence'' [ predicate? ] trim | Factor |
65+
| (string-trim '(#\Space #\Tab #\Newline) ''string'') | Common Lisp |
66+
| (string-trim ''string'') | Scheme |
67+
| ''string''.trim() | Java, JavaScript (1.8.1+), Rust |
68+
| Trim(''String'') | Pascal, QBasic, Visual Basic, Delphi |
69+
| ''string''.strip() | Python |
70+
| strings.Trim(''string'', ''chars'') | Go |
71+
| LTRIM(RTRIM(''String'')) | Oracle SQL, T-SQL |
72+
| string:strip(''string'' [,''option'', ''char'']) | Erlang |
73+
| ''string''.strip or ''string''.lstrip or ''string''.rstrip | Ruby |
74+
| trim(''string'') | PHP, Raku |
75+
| [''string'' stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] | Objective-C/Cocoa |
76+
| ''string'' withBlanksTrimmed ''string'' withoutSpaces ''string'' withoutSeparators | Smalltalk |
77+
| string trim ''$string'' | Tcl |
78+
| TRIM(''string'') or TRIM(ADJUSTL(''string'')) | Fortran |
79+
| TRIM(''string'') | SQL |
80+
| String.trim ''string'' | OCaml 4+ |
81+
82+
Note: This is an abbreviated list from Uncyclopedia. [Full table](https://en.wikipedia.org/wiki/Comparison_of_programming_languages_(string_functions)#trim)
83+
84+
The standard library includes a variety of methods which perform similar operations:
85+
86+
- Firstly, there are `dropFirst(Int)` and `dropLast(Int)`. These return slices but do not support user-defined predicates.
87+
If the collection's `count` is less than the number of elements to drop, they return an empty slice.
88+
- Secondly, there is `drop(while:)`, which also returns a slice and is equivalent to a 'left-trim' (trimming from the head but not the tail).
89+
If the entire collection is dropped, this method returns an empty slice.
90+
- Thirdly, there are `removeFirst(Int)` and `removeLast(Int)` which do not return slices and actually mutate the collection.
91+
If the collection's `count` is less than the number of elements to remove, this method triggers a runtime error.
92+
- Lastly, there are the `popFirst()` and `popLast()` methods, which work like `removeFirst()` and `removeLast()`,
93+
except they do not trigger a runtime error for empty collections.
94+
95+
The closest neighbours to this function would be the `drop` family of methods. Unfortunately, unlike `dropFirst(Int)`,
96+
the name `drop(while:)` does not specify which end(s) of the collection it operates on. Moreover, one could easily
97+
mistake code such as:
98+
99+
```swift
100+
let result = myString.drop(while: \.isWhitespace)
101+
```
102+
103+
With a lazy filter that drops _all_ whitespace characters regardless of where they are in the string.
104+
Besides that, the root `trim` leads to clearer, more conscise code, which is more aligned with other programming
105+
languages:
106+
107+
```swift
108+
// Does `result` contain the input, trimmed of certain elements?
109+
// Or does this code mutate `input` in-place and return the elements which were dropped?
110+
let result = input.dropFromBothEnds(where: { ... })
111+
112+
// No such ambiguity here.
113+
let result = input.trimming(where: { ... })
114+
```

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Read more about the package, and the intent behind it, in the [announcement on s
3232

3333
- [`chunked(by:)`, `chunked(on:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chunked.md): Eager and lazy operations that break a collection into chunks based on either a binary predicate or when the result of a projection changes.
3434
- [`indexed()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md): Iterate over tuples of a collection's indices and elements.
35+
- [`trimming(where:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Trim.md): Returns a slice by trimming elements from a collection's start and end.
3536

3637

3738
## Adding Swift Algorithms as a Dependency

Sources/Algorithms/Trim.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension BidirectionalCollection {
13+
14+
/// Returns a `SubSequence` formed by discarding all elements at the start and end of the collection
15+
/// which satisfy the given predicate.
16+
///
17+
/// This example uses `trimming(where:)` to get a substring without the white space at the
18+
/// beginning and end of the string:
19+
///
20+
/// ```
21+
/// let myString = " hello, world "
22+
/// print(myString.trimming(where: \.isWhitespace)) // "hello, world"
23+
/// ```
24+
///
25+
/// - parameters:
26+
/// - predicate: A closure which determines if the element should be omitted from the
27+
/// resulting slice.
28+
///
29+
/// - complexity: `O(n)`, where `n` is the length of this collection.
30+
///
31+
@inlinable
32+
public func trimming(
33+
where predicate: (Element) throws -> Bool
34+
) rethrows -> SubSequence {
35+
36+
// Consume elements from the front.
37+
let sliceStart = try firstIndex { try predicate($0) == false } ?? endIndex
38+
// sliceEnd is the index _after_ the last index to match the predicate.
39+
var sliceEnd = endIndex
40+
while sliceStart != sliceEnd {
41+
let idxBeforeSliceEnd = index(before: sliceEnd)
42+
guard try predicate(self[idxBeforeSliceEnd]) else {
43+
return self[sliceStart..<sliceEnd]
44+
}
45+
sliceEnd = idxBeforeSliceEnd
46+
}
47+
// Trimmed everything.
48+
return self[Range(uncheckedBounds: (sliceStart, sliceStart))]
49+
}
50+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import Algorithms
13+
import XCTest
14+
15+
final class TrimTests: XCTestCase {
16+
17+
func testEmpty() {
18+
let results_empty = ([] as [Int]).trimming { $0.isMultiple(of: 2) }
19+
XCTAssertEqual(results_empty, [])
20+
}
21+
22+
func testNoMatch() {
23+
// No match (nothing trimmed).
24+
let results_nomatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming {
25+
$0.isMultiple(of: 2)
26+
}
27+
XCTAssertEqual(results_nomatch, [1, 3, 5, 7, 9, 11, 13, 15])
28+
}
29+
30+
func testNoTailMatch() {
31+
// No tail match (only trim head).
32+
let results_notailmatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { $0 < 10 }
33+
XCTAssertEqual(results_notailmatch, [11, 13, 15])
34+
}
35+
36+
func testNoHeadMatch() {
37+
// No head match (only trim tail).
38+
let results_noheadmatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { $0 > 10 }
39+
XCTAssertEqual(results_noheadmatch, [1, 3, 5, 7, 9])
40+
}
41+
42+
func testBothEndsMatch() {
43+
// Both ends match, some string of >1 elements do not (return that string).
44+
let results = [2, 10, 11, 15, 20, 21, 100].trimming { $0.isMultiple(of: 2) }
45+
XCTAssertEqual(results, [11, 15, 20, 21])
46+
}
47+
48+
func testEverythingMatches() {
49+
// Everything matches (trim everything).
50+
let results_allmatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { _ in true }
51+
XCTAssertEqual(results_allmatch, [])
52+
}
53+
54+
func testEverythingButOneMatches() {
55+
// Both ends match, one element does not (trim all except that element).
56+
let results_one = [2, 10, 12, 15, 20, 100].trimming { $0.isMultiple(of: 2) }
57+
XCTAssertEqual(results_one, [15])
58+
}
59+
}

0 commit comments

Comments
 (0)