attaswift / BTree

Fast sorted collections for Swift using in-memory B-trees

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

OrdererSet updating and removing equal objects with different order question

Fabezi opened this issue · comments

I don't fully understand the behaviour below or is this a bug? Updating and removing equal objects with different order fails or succeeds if the comparable object is below or above the other object in set.

Thanks.

import Foundation
import XCTest

class DummyObject: Comparable
{
    let name:String
    let order:Int
    
    init(name:String, order:Int) {
        self.name = name
        self.order = order
    }
    
    static func ==(a: DummyObject, b: DummyObject) -> Bool { return a.name == b.name }
    static func <(a: DummyObject, b: DummyObject)  -> Bool { return a.order > b.order }
}

class RandomTests: XCTestCase {
    
    func testSet()
    {
        var testSet = SortedSet<DummyObject>()
        
        // Creates dummy objects with name and order
        let createObject: (Int, Int) -> DummyObject = { index, order in
            let newName = "name \(index)"
            let newOrder = order
            return DummyObject(name: newName, order: newOrder)
        }
        
        // Inserts and logs update or replace
        let insertObject: (DummyObject) -> Void = { object in
            if (testSet.update(with: object) != nil) {
                print("updated", object.name, object.order);
            } else {
                print("inserted", object.name, object.order);
            }
        }
        
        // Removes and logs remove or not found
        let removeObject: (DummyObject) -> Bool = { object in
            if (testSet.remove(object) != nil) {
                print("removed", object.name, object.order);
                return true
            } else {
                print("could not find \(object.name, object.order) to remove");
                return false
            }
        }
        
        let object0 = createObject(0, 100)
        let object1 = createObject(1, 101)
        let object2 = createObject(2, 102) // Update test
        let object3 = createObject(2, 103) // Update test
        
        insertObject(object0)
        insertObject(object1)
        insertObject(object2) // Update test
        insertObject(object3) // update test
        
        // Update object object2 with comparable object with different order
        // -> (FAILS WITH 101 BUT NOT WITH 103, ACTUAL OBJECT IN SET IS 102)
        XCTAssertEqual(testSet.count, 3, "object2 should've been replaced by object3 and not inserted")

        // Remove object 3 with comparable object with different order
        // -> (FAILS WITH 102 BUT NOT WITH 104, ACTUAL OBJECT IN SET IS 103)
        let likeObject3 = createObject(2, 104)
        XCTAssert(removeObject(likeObject3), "object3 should've been removed")
        
        // Remove object 1 with comparable object with different order (FAILS)
        // -> (FAILS)
        let likeObject1 = createObject(1, 10)
        XCTAssert(removeObject(likeObject1), "object1 should've been removed")
    }
}

The bug is in DummyObject's implementation of Comparable; in particular, it does not implement a strict total ordering. For example, object2 < object3 and object2 == object3 are both true, which is a clear violation of Comparable requirements.

Such a broken ordering relation will confuse any algorithm that uses Comparable. For example, you'll see that Array.sort will have trouble sorting your values, too.

In a correct implementation of Comparable, both == and < need to look at the same properties.

For example, this is a valid implementation:

  static func ==(a: DummyObject, b: DummyObject) -> Bool { return a.name == b.name }
  static func <(a: DummyObject, b: DummyObject)  -> Bool { return a.name < b.name }

This is another option:

  static func ==(a: DummyObject, b: DummyObject) -> Bool { return a.order == b.order }
  static func <(a: DummyObject, b: DummyObject)  -> Bool { return a.order > b.order }

And here is a third:

  static func ==(a: DummyObject, b: DummyObject) -> Bool { return (a.name, a.order) == (b.name, b.order) }
  static func <(a: DummyObject, b: DummyObject)  -> Bool { return (a.name, -a.order) < (b.name, -b.order) }

(Of course, there are many more options to define these correctly.) The one to choose depends on the semantics that you're trying to achieve.

You can't mix and match definitions of == and < from these solutions; they are inherently interrelated and can't be separated from each other.

@lorentey Thank you! Of course.