Skip to content

Commit f9f3a5c

Browse files
authored
Minor revisions to minAndMax() (#143)
* Settle on 2-way approach (s/o to @NevinBR) * Add tests for predicate usage and stability * Update the return value semantics
1 parent 4038b5d commit f9f3a5c

File tree

2 files changed

+34
-50
lines changed

2 files changed

+34
-50
lines changed

Sources/Algorithms/MinMax.swift

Lines changed: 11 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,10 @@ extension Sequence {
423423
/// if its first argument should be ordered before its second
424424
/// argument; otherwise, `false`.
425425
/// - Returns: A tuple with the sequence's minimum element, followed by its
426-
/// maximum element. For either member, if the sequence provides multiple
427-
/// qualifying elements, the one chosen is unspecified. The same element may
428-
/// be used for both members if all the elements are equivalent. If the
429-
/// sequence has no elements, returns `nil`.
426+
/// maximum element. If the sequence provides multiple qualifying minimum
427+
/// elements, the first equivalent element is returned; of multiple maximum
428+
/// elements, the last is returned. If the sequence has no elements, the
429+
/// method returns `nil`.
430430
///
431431
/// - Complexity: O(*n*), where *n* is the length of the sequence.
432432
public func minAndMax(
@@ -440,55 +440,16 @@ extension Sequence {
440440
// Confirm the initial bounds.
441441
if try areInIncreasingOrder(highest, lowest) { swap(&lowest, &highest) }
442442

443-
#if true
444443
// Read the elements in pairwise. Structuring the comparisons around this
445444
// is actually faster than loops based on extracting and testing elements
446445
// one-at-a-time.
447446
while var low = iterator.next() {
448-
if var high = iterator.next() {
449-
// Update the upper bound with the larger new element.
450-
if try areInIncreasingOrder(high, low) { swap(&low, &high) }
451-
if try !areInIncreasingOrder(high, highest) { highest = high }
452-
} else {
453-
// Update the upper bound by reusing the last element. The next element
454-
// iteration will also fail, ending the loop.
455-
if try !areInIncreasingOrder(low, highest) { highest = low }
456-
}
457-
458-
// Update the lower bound with the smaller new element, which may need a
459-
// swap first to determine.
460-
if try areInIncreasingOrder(low, lowest) { lowest = low }
461-
}
462-
#else
463-
/// Ensure the second argument has a value that is ranked at least as much as
464-
/// the first argument.
465-
func sort(_ a: inout Element, _ b: inout Element) throws {
466-
if try areInIncreasingOrder(b, a) { swap(&a, &b) }
467-
}
468-
469-
/// Find the smallest and largest values out of a group of four arguments.
470-
func minAndMaxOf4(
471-
_ a: Element, _ b: Element, _ c: Element, _ d: Element
472-
) throws -> (min: Element, max: Element) {
473-
var (a, b, c, d) = (a, b, c, d)
474-
try sort(&a, &b)
475-
try sort(&c, &d)
476-
try sort(&a, &c)
477-
try sort(&b, &d)
478-
return (a, d)
479-
}
480-
481-
// Read the elements in four-at-a-time. Some say this is more effective
482-
// than a two-at-a-time loop.
483-
while let a = iterator.next() {
484-
let b = iterator.next() ?? a
485-
let c = iterator.next() ?? b
486-
let d = iterator.next() ?? c
487-
let (low, high) = try minAndMaxOf4(a, b, c, d)
447+
var high = iterator.next() ?? low
448+
if try areInIncreasingOrder(high, low) { swap(&low, &high) }
488449
if try areInIncreasingOrder(low, lowest) { lowest = low }
489450
if try !areInIncreasingOrder(high, highest) { highest = high }
490451
}
491-
#endif
452+
492453
return (lowest, highest)
493454
}
494455
}
@@ -510,10 +471,10 @@ extension Sequence where Element: Comparable {
510471
/// - Precondition: The sequence is finite.
511472
///
512473
/// - Returns: A tuple with the sequence's minimum element, followed by its
513-
/// maximum element. For either member, if there is a tie for the extreme
514-
/// value, the element chosen is unspecified. The same element may be used
515-
/// for both members if all the elements are equal. If the sequence has no
516-
/// elements, returns `nil`.
474+
/// maximum element. If the sequence provides multiple qualifying minimum
475+
/// elements, the first equivalent element is returned; of multiple maximum
476+
/// elements, the last is returned. If the sequence has no elements, the
477+
/// method returns `nil`.
517478
///
518479
/// - Complexity: O(*n*), where *n* is the length of the sequence.
519480
@inlinable

Tests/SwiftAlgorithmsTests/MinMaxTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,27 @@ final class MinAndMaxTests: XCTestCase {
248248
XCTAssertEqual(result2?.min, "a")
249249
XCTAssertEqual(result2?.max, "g")
250250
}
251+
252+
/// Confirms that the given predicate is used to find the min/max.
253+
func testPredicate() {
254+
let result = (1...5).minAndMax(by: >)
255+
XCTAssertEqual(result?.min, 5)
256+
XCTAssertEqual(result?.max, 1)
257+
258+
// Odd count
259+
let result2 = "gfabdec".minAndMax(by: >)
260+
XCTAssertEqual(result2?.min, "g")
261+
XCTAssertEqual(result2?.max, "a")
262+
}
263+
264+
/// Confirms that the min and max are "stable" as defined for minAndMax.
265+
func testStability() {
266+
for n in 1...10 {
267+
let result = repeatElement(0, count: n)
268+
.enumerated()
269+
.minAndMax(by: { $0.element < $1.element })
270+
XCTAssertEqual(result?.min.offset, 0)
271+
XCTAssertEqual(result?.max.offset, n - 1)
272+
}
273+
}
251274
}

0 commit comments

Comments
 (0)