Skip to content

Commit

Permalink
SWIFT-344 Add equalsIgnoreKeyOrder (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncarbon authored Dec 4, 2020
1 parent b8e5602 commit 6ababc1
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 15 deletions.
35 changes: 35 additions & 0 deletions Sources/SwiftBSON/BSONDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,38 @@ extension BSONDocument: BSONValue {
extension BSONDocument: CustomStringConvertible {
public var description: String { self.toExtendedJSONString() }
}

extension BSONDocument {
/**
* Returns whether this `BSONDocument` contains exactly the same key/value pairs as the provided `BSONDocument`,
* regardless of the order of the keys.
*
* Warning: This method is much less efficient than checking for regular equality since the document is internally
* ordered.
*
* - Parameters:
* - other: a `BSONDocument` to compare this document with.
*
* - Returns: a `Bool` indicating whether the two documents are equal.
*/
public func equalsIgnoreKeyOrder(_ other: BSONDocument) -> Bool {
guard self.count == other.count else {
return false
}

for (k, v) in self {
let otherValue = other[k]
if case let (.document(docA), .document(docB)?) = (v, otherValue) {
guard docA.equalsIgnoreKeyOrder(docB) else {
return false
}
continue
}
guard v == otherValue else {
return false
}
}

return true
}
}
16 changes: 1 addition & 15 deletions Tests/SwiftBSONTests/CommonTestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,11 @@ public func sortedEqual(_ expectedValue: BSONDocument?) -> Predicate<BSONDocumen
return PredicateResult(status: .fail, message: msg)
}

let matches = expected.sortedEquals(actual)
let matches = expected.equalsIgnoreKeyOrder(actual)
return PredicateResult(status: PredicateStatus(bool: matches), message: msg)
}
}

extension BSONDocument {
/// Compares two `BSONDocument`s and returns true if they have the same key/value pairs in them.
public func sortedEquals(_ other: BSONDocument) -> Bool {
let keys = self.keys.sorted()
let otherKeys = other.keys.sorted()

// first compare keys, because rearrangeDoc will discard any that don't exist in `expected`
expect(keys).to(equal(otherKeys))

let rearranged = rearrangeDoc(other, toLookLike: self)
return self == rearranged
}
}

/// Given two documents, returns a copy of the input document with all keys that *don't*
/// exist in `standard` removed, and with all matching keys put in the same order they
/// appear in `standard`.
Expand Down
90 changes: 90 additions & 0 deletions Tests/SwiftBSONTests/DocumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,96 @@ final class DocumentTests: BSONTestCase {
.to(equal(["hi": true, "hello": "hi", "cat": 2] as BSONDocument))
}

func testEqualsIgnoreKeyOrder() throws {
// basic comparisons
let doc1: BSONDocument = ["foo": "bar", "bread": 1]
let doc2: BSONDocument = ["foo": "bar", "bread": 1]
expect(doc1.equalsIgnoreKeyOrder(doc2)).to(equal(true))

let doc3: BSONDocument = ["foo": "bar", "bread": 1]
let doc4: BSONDocument = ["foo": "foo", "bread": 2]
expect(doc3.equalsIgnoreKeyOrder(doc4)).to(equal(false))

// more complex comparisons
let a: BSONDocument = [
"string": "test string",
"true": true,
"false": false,
"int": 25,
"int32": .int32(5),
"int64": .int64(10),
"double": .double(15),
"regex": .regex(BSONRegularExpression(pattern: "^abc", options: "imx")),
"decimal128": .decimal128(try! BSONDecimal128("1.2E+10")),
"minkey": .minKey,
"maxkey": .maxKey,
"date": .datetime(Date(timeIntervalSince1970: 500.004)),
"timestamp": .timestamp(BSONTimestamp(timestamp: 5, inc: 10)),
"nesteddoc": ["a": 1, "b": 2, "c": false, "d": [3, 4]],
"oid": .objectID(try! BSONObjectID("507f1f77bcf86cd799439011")),
"array1": [1, 2],
"array2": ["string1", "string2"],
"null": .null,
"code": .code(BSONCode(code: "console.log('hi');")),
"nestedarray": [[1, 2], [.int32(3), .int32(4)]],
"codewscope": .codeWithScope(BSONCodeWithScope(code: "console.log(x);", scope: ["x": 2]))
]

let b: BSONDocument = [
"true": true,
"int": 25,
"int32": .int32(5),
"int64": .int64(10),
"string": "test string",
"double": .double(15),
"decimal128": .decimal128(try! BSONDecimal128("1.2E+10")),
"minkey": .minKey,
"date": .datetime(Date(timeIntervalSince1970: 500.004)),
"timestamp": .timestamp(BSONTimestamp(timestamp: 5, inc: 10)),
"nestedarray": [[1, 2], [.int32(3), .int32(4)]],
"codewscope": .codeWithScope(BSONCodeWithScope(code: "console.log(x);", scope: ["x": 2])),
"nesteddoc": ["b": 2, "a": 1, "d": [3, 4], "c": false],
"oid": .objectID(try! BSONObjectID("507f1f77bcf86cd799439011")),
"false": false,
"regex": .regex(BSONRegularExpression(pattern: "^abc", options: "imx")),
"array1": [1, 2],
"array2": ["string1", "string2"],
"null": .null,
"code": .code(BSONCode(code: "console.log('hi');")),
"maxkey": .maxKey
]

// comparing two documents with the same key-value pairs in different order should return true
expect(a.equalsIgnoreKeyOrder(b)).to(equal(true))

let c: BSONDocument = [
"true": true,
"int": 52,
"int32": .int32(15),
"int64": .int64(100),
"string": "this is different string",
"double": .double(15),
"decimal128": .decimal128(try! BSONDecimal128("1.2E+10")),
"minkey": .minKey,
"date": .datetime(Date(timeIntervalSince1970: 500.004)),
"array1": [1, 2],
"timestamp": .timestamp(BSONTimestamp(timestamp: 5, inc: 10)),
"nestedarray": [[1, 2], [.int32(3), .int32(4)]],
"codewscope": .codeWithScope(BSONCodeWithScope(code: "console.log(x);", scope: ["x": 2])),
"nesteddoc": ["1": 1, "2": 2, "3": true, "4": [5, 6]],
"oid": .objectID(try! BSONObjectID("507f1f77bcf86cd799439011")),
"false": false,
"regex": .regex(BSONRegularExpression(pattern: "^abc", options: "imx")),
"array2": ["string3", "string2", "string1"],
"null": .null,
"code": .code(BSONCode(code: "console.log('hi');")),
"maxkey": .maxKey
]

// comparing two documents with same keys but different values should return false
expect(a.equalsIgnoreKeyOrder(c)).to(equal(false))
}

func testRawBSON() throws {
let doc = try BSONDocument(fromJSON: "{\"a\":[{\"$numberInt\":\"10\"}]}")
let fromRawBSON = try BSONDocument(fromBSON: doc.buffer)
Expand Down

0 comments on commit 6ababc1

Please sign in to comment.