Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XCTAssertUnorderedEqualSequences: Improve algorithmic complexity when elements are Hashable (Resolves #176) #177

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
79 changes: 69 additions & 10 deletions Tests/SwiftAlgorithmsTests/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,31 +84,90 @@ func XCTAssertEqualSequences<S1: Sequence, S2: Sequence>(
message(), file: file, line: line)
}

// Two sequences contains exactly the same element but not necessarily in the same order.
/// Asserts two sequences contain exactly the same elements but not necessarily
/// in the same order.
/// - Complexity: O(*n* * *m*) where *n* is the number of elements in the first
/// sequence and *m* is the number of elements in the second sequence
func XCTAssertUnorderedEqualSequences<S1: Sequence, S2: Sequence>(
_ expression1: @autoclosure () throws -> S1,
_ expression2: @autoclosure () throws -> S2,
_ message: @autoclosure () -> String = "",
file: StaticString = #file, line: UInt = #line
) rethrows where S1.Element: Equatable, S1.Element == S2.Element {
func fail(_ reason: String) {
let message = message()
XCTFail(message.isEmpty ? reason : "\(message) - \(reason)",
file: file, line: line)
}

var s1 = Array(try expression1())
var missing: [S1.Element] = []
for elt in try expression2() {
guard let idx = s1.firstIndex(of: elt) else {
missing.append(elt)
continue
}
s1.remove(at: idx)
// Since ordering does not matter, avoid the performance costs of removing
// an element at an index in the middle of the array, which requires
// shifting all subsequent elements down.
let lastIdx = s1.index(before: s1.endIndex)
s1.swapAt(idx, lastIdx)
s1.remove(at: lastIdx)
}

if !missing.isEmpty {
fail("first sequence missing '\(missing)' elements from second sequence")
}

XCTAssertTrue(
missing.isEmpty, "first sequence missing '\(missing)' elements from second sequence",
file: file, line: line
)
if !s1.isEmpty {
fail("first sequence contains \(s1) missing from second sequence")
}
}

XCTAssertTrue(
s1.isEmpty, "first sequence contains \(s1) missing from second sequence",
file: file, line: line
)
/// Asserts two sequences contain exactly the same elements but not necessarily
/// in the same order.
/// - Complexity: O(*n* + *m*) where *n* is the number of elements in the first
/// sequence and *m* is the number of elements in the second sequence
func XCTAssertUnorderedEqualSequences<S1: Sequence, S2: Sequence>(
_ expression1: @autoclosure () throws -> S1,
_ expression2: @autoclosure () throws -> S2,
_ message: @autoclosure () -> String = "",
file: StaticString = #file, line: UInt = #line
) rethrows where S1.Element: Hashable, S1.Element == S2.Element {
func fail(_ reason: String) {
let message = message()
XCTFail(message.isEmpty ? reason : "\(message) - \(reason)",
file: file, line: line)
}

// Map the elements of the first sequence to the number of times it appeared
// in the sequence
var s1 = [S1.Element: Int](try expression1().lazy.map({
($0, 1)
}), uniquingKeysWith: {
$0 + $1
})

var missing: [S1.Element] = []
for elt in try expression2() {
if let count = s1[elt] {
if count > 1 {
s1[elt] = (count - 1)
} else {
s1[elt] = nil
}
} else {
missing.append(elt)
}
}

if !missing.isEmpty {
fail("first sequence missing '\(missing)' elements from second sequence")
}

if !s1.isEmpty {
fail("second sequence missing one or more occurrences of '\(s1.keys)' elements from first sequence")
}
}

func XCTAssertEqualSequences<S1: Sequence, S2: Sequence>(
Expand Down