3

So at first this seemed like a pretty straight-forward problem to solve, but there are some little catches i found, and my current end-solution is really ugly, so I'm curious to see what you guys can come up with

I have a class, OptionalObject. It has four properties,

let prop1: String?
let prop2: String?
let prop3: String?
let prop4: String?

I then have an array of a thousand of these OptionalObject objects, and I'm guaranteed that there are no two objects with the exact same properties.

EX: (OptionalObject: prop1, prop2, prop3, prop4

Obj1: ABC, 123, Hello, Goodbye
Obj2: DEF, 456, nil, nil
Obj3: nil, nil, Hello, Goodbye
Obj4: nil, nil, Hello, nil
Obj5: ABC, nil, nil, nil
Obj6: ABC, nil, Hello, Goodbye
Obj7: DEF, 123, nil, Goodbye
Obj8: DEF, nil, nil, nil
...

and then I have a singular object of another class that has all 4 Strings (non-optional)

(ConcreteObject: arg1: String, arg2: String, arg3: String, arg4: String)

I want to find the best matching OptionalObject to my ConcreteObject, based on these four properties.

Which I can think of two ways to do it, one is to use filter on all OptionalObjects, the other is to manually enumerate over all OptionalObjects, have nested if statements to check properties (which is effectively the same as filter anyways)

If I were to filter, it could be something like this:

let matchingOptionalObject = optionalObjectsArray.filter {return $0.prop1 == arg1 && $0.prop2 == arg2 && $0.prop3 == arg3 && $0.prop4 == arg4}

and boom I now have one object that matches... but only if it's an exact match.

If my ConcreteObject has (DEF, 123, Hello, Goodbye) I wouldn't get any matches, because no OptionalObjects match exactly.

I would expect a return of Obj3, Obj4, Obj7, Obj8


So then naturally I think OK, let's first look at which arguments we have as non-nil, and then construct a query accordingly.

For this example, I'm going to pretend there's only two properties so it's easier to understand:

let matchingOptionalObject = optionalObjectsArray.filter { 
    if $0.prop1 != nil {
        if $0.prop2 != nil {
            return $0.prop1 == arg1 && $0.prop2 == arg2
        } else {
            return $0.prop1 == arg1
        }
    } else {
        if $0.prop2 != nil {
            return $0.prop2 == arg2
        } else {
            return false
        }
    }
}

But then the problem is that because there are 4 properties, there are like at least 10 different possibilities of unique nil arguments that we need to cover, and this becomes a really really ugly

Which is what I currently have, I have a bunch of ugly if statements checking the combination of nil arguments, and then constructing a filter query accordingly...


Which OK, we need to move on, so I guess we can deal with it and open an issue for figuring out a better solution later, but then there is one more requirement that makes this solution not work...

Which is that each property has a different weight.

Two Rules for picking the best match when there are multiple matches:
1) Pick the match that has the most properties that match
2) If the matches have the same number of properties that match, pick the match that has the highest weight.

Prop1 has the highest weight
Prop2 has the lowest

So with the example of ConcreteObject: (DEF, 123, Hello, Goodbye)

Matched OptionalObjects:

Obj3: nil, nil, Hello, Goodbye
Obj4: nil, nil, Hello, nil
Obj7: DEF, 123, nil, Goodbye
Obj8: DEF, nil, nil, nil

We would pick Obj7 as the best match because it has 3 matching properties


But let's say for some other example with a new ConcreteObject and a new set of OptionalObjects, we have this match:

Our new Matched OptionalObjects:

New1: nil, 999, nil, NiHao
New2: XYZ, 999, nil, nil

We would pick New2, because even though New1 and New2 both have 2 matching properties, New2 has the matching properties of higher weight.


So, there's the dilemma.
I'm hoping that I'm just not remembering some key concepts years ago in my undergrad algorithms class, and that there's some clean solution (maybe even something that Swift provides), but I'm near my wit's end-- so really any suggestions or insight anybody has is more than welcome

A O
  • 5,516
  • 3
  • 33
  • 68
  • If your properties are really called `prop1`, `prop2` etc. you should maybe represent them as an array `props: [String?]`. – Sulthan May 31 '18 at 07:16
  • oh no i just renamed them that way to make it easier to focus on the problem at hand, for my actual object it makes sense for the properties to be separate like that – A O May 31 '18 at 20:13

2 Answers2

1

Mathematics, and specifically basic algebra, gives you the answer.

You need to define a binary relation on your OptionalObject instances, that is antisymmetric, transitive but not connex. Like < for integers.

Here is the signature:

func myRelation(_ o1: OptionalObject, _ o2: OptionalObject) -> Bool {
  // compare the 4 member properties of o1 and o2, using weights if not nil
  // return true if o1 matches best, otherwise return false
}

In your question, you have specified the rules you need to implement in this function. BUT NOTE THAT if the rules do not lead to an antisymmetric, transitive and not connex binary relation, your problem is not well defined: you need to have rules that define a total order, for a solution being available, mathematically speaking.

Now, your set of OptionalObject instances must be ordered using the Swift standard func sort(by: (Element, Element) -> Bool):

optionalObjectsArray.sort(by: myRelation)

Finally, the first object in the returned collection is the one you were looking for:

let myBestMatch = OptionalObjectsArray.sort(by: myRelation).first()
Alexandre Fenyo
  • 4,526
  • 1
  • 17
  • 24
  • 1
    I believe because the array of OptionalObjects is guaranteed to be unique, that there is going to be a total order. Makes sense, and I think @rmaddy wrote a `myRelation` implementation for me with his `score` method – A O May 30 '18 at 23:33
  • 1
    i think my problem was that I was trying to do a matching solution, whereas a sort and pick first makes way more sense – A O May 30 '18 at 23:34
  • 1
    Minor nitpick: The `sort()` method in Swift takes a `<` relation (a strict weak ordering) as argument, not a `<=` relation. – Martin R May 31 '18 at 07:04
1

Here's one reasonable solution. See code comments for details.

struct OptionalObject {
    let prop1: String?
    let prop2: String?
    let prop3: String?
    let prop4: String?
}

struct ConcreteObject {
    let prop1: String
    let prop2: String
    let prop3: String
    let prop4: String

    // Determine the score.
    // "matches" counts the number of matching properties.
    // "weight" gives 8 for the 1st property, 4 for the 2nd, 2 for the 3rd, 1 for the 4th. Adjust to suit your needs
    func score(for opt: OptionalObject) -> (matches: Int, weight: Int) {
        var matches = 0
        var weight = 0
        if opt.prop1 == self.prop1 { matches += 1; weight += 8 }
        if opt.prop2 == self.prop2 { matches += 1; weight += 4 }
        if opt.prop3 == self.prop3 { matches += 1; weight += 2 }
        if opt.prop4 == self.prop4 { matches += 1; weight += 1 }

        return (matches, weight)
    }

    // Compares two OptionalObject by getting the score of each
    // against "self".
    func compare(lhs: OptionalObject, rhs: OptionalObject) -> Bool {
        let scoreL = score(for: lhs)
        let scoreR = score(for: rhs)

        // If the number of matches are the same, compare the weight
        return scoreL > scoreR
    }
}

// Test ConcreteObject    
let concrete = ConcreteObject(prop1: "DEF", prop2: "123", prop3: "Hello", prop4: "Goodbye")

// List of OptionalObject
var optionals: [OptionalObject] = [
    OptionalObject(prop1: nil, prop2: nil, prop3: "Hello", prop4: nil),
    OptionalObject(prop1: "DEF", prop2: "456", prop3: nil, prop4: nil),
    OptionalObject(prop1: "ABC", prop2: "123", prop3: "Hello", prop4: "Goodbye"),
    OptionalObject(prop1: nil, prop2: nil, prop3: "Hello", prop4: "Goodbye"),
    OptionalObject(prop1: "DEF", prop2: "456", prop3: "Hello", prop4: "Goodbye"),
    //OptionalObject(prop1: nil, prop2: nil, prop3: nil, prop4: nil),
]

// Sort the list based on the ConcreteObject
let sorted = optionals.sorted { concrete.compare(lhs: $0, rhs: $1) }
print(sorted)

The results are sorted in the desired order. The first object in sorted has the highest score.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • 1
    wow that was crazy fast, glad to see you're still around-- when i was really active a few years ago I saw you everywhere. thanks for the answer, i'm going to try it now :) – A O May 30 '18 at 23:30
  • 1
    beautiful, was really easy to implement, and an efficient solution. thanks again – A O May 30 '18 at 23:59
  • I’d recommend using reduce instead of sort. Reduce would give you an order n solution, sort is order n log n. – David Berry May 31 '18 at 04:18
  • 1
    You can simply `return scoreL > scoreR` in the comparison function, tuples (of comparable elements) are themselves comparable. – Martin R May 31 '18 at 06:58
  • 1
    You can also compare optional values with non-optionals directly: `if opt.prop1 == self.prop1 { ... }` etc. – Martin R May 31 '18 at 07:01
  • @MartinR I didn't know that about tuples. Very nice. And thanks for the reminder about using `==` with optionals. Silly oversight. – rmaddy May 31 '18 at 15:30
  • @rmaddy: I learned that from Hamish: https://stackoverflow.com/a/37612765/1187415. – Martin R May 31 '18 at 18:34