4

I have multiple sets of two arrays like that. I am getting them from a 3rd party.

var array1 : [Any?]
var array2 : [Any?]

I know about types of objects in these array (in compile time). As example that first element will be a String and second is an Int.

I currently compare each set of arrays like that (please notice that arrays aren't homogeneous).

array1[0] as? String == array2[0] as? String
array1[1] as? Int == array2[1] as? Int
...

The biggest problem that each set have different types in it. As result, I have let say 10 sets of arrays with 5 elements in each. I had to do 10*5 explicit conversion to specific type and comparation.

I want to be able to write a common method which can compare two arrays like that (without a need to specify all the types)

compareFunc(array1, array2)

I started to go down the road. However, I can't figure out what should I cast the objects too. I tried Equatable. However, swift doesn't like direct usage of it. So, something like that doesn't work

func compare(array1: [Any?], array2: [Any?]) {
    for index in 0..<array1.count {
       if (array1[index] as? Equatable != array2[index] as? Equatable) {
         // Do something
       }
    }
}

Each object in this array will be Equatable. However, I am not sure how to use it in this case.

Victor Ronin
  • 22,758
  • 18
  • 92
  • 184
  • I think you should maybe take a step back and consider how you ended up with an array of `Any` in the first place. You say you're getting it from a 3rd party library, but what does the array actually represent and why do you need to compare it? You cannot do what you're asking for in pure Swift (at least not without type-casting, which I imagine defeats the purpose of what you want in the first place), as the `==` operator requires a concrete type in order to do any comparison. And therefore due to the static typing in Swift, trying to abstract away the types simply won't work. – Hamish May 19 '16 at 18:48

3 Answers3

2

Construct a simple element-by-element comparison function based on (attempted) type conversion to known element types

Since you're aiming to compare arrays of (optional) Any elements, you could just construct a function that performs element-by-element comparison by using a switch block to attempt to downcast the elements of the array to different known types in your "3rd party arrays". Note that you needn't specify which element position that corresponds to a specific type (as this might differ between different set of arrays), just the exhaustive set of the different types that any given element might be of.

An example of such a function follows below:

func compareAnyArrays(arr1: [Any?], _ arr2: [Any?]) -> Bool {
    /* element-by-element downcasting (to known types) followed by comparison */
    return arr1.count == arr2.count && !zip(arr1, arr2).contains {
        
        /* note that a 'true' below indicates the arrays differ (i.e., 'false' w.r.t. array equality) */
        if let v1 = $1 {
            
            /* type check for known types */
            switch $0 {
            case .None: return true
            case let v0 as String:
                if let v1 = v1 as? String { return !(v0 == v1) }; return true
            case let v0 as Int:
                if let v1 = v1 as? Int { return !(v0 == v1) }; return true
            /* ...
               expand with the known possible types of your array elements
               ... */
            case _ : return true
                /*  */
            }
        }
        else if let _ = $0 { return true }
        return false
    }
}

or, alternative, making the switch block a little less bloated by making use of (a slightly modified of) the compare(...) helper function from @Roman Sausarnes:s answer

func compareAnyArrays(arr1: [Any?], _ arr2: [Any?]) -> Bool {
    
    /* modified helper function from @Roman Sausarnes:s answer */
    func compare<T: Equatable>(obj1: T, _ obj2: Any) -> Bool {
        return obj1 == obj2 as? T
    }
    
    /* element-by-element downcasting (to known types) followed by comparison */
    return arr1.count == arr2.count && !zip(arr1, arr2).contains {
        
        /* note also that a 'true' below indicates the arrays differ
         (=> false w.r.t. equality) */
        if let v1 = $1 {
            
            /* type check for known types */
            switch $0 {
            case .None: return true
            case let v0 as String: return !compare(v0, v1)
            case let v0 as Int: return !compare(v0, v1)
                /* ...
                 expand with the known possible types of your array elements
                 ... */
            case _ : return true
                /*  */
            }
        }
        else if let _ = $0 { return true }
        return false
    }
}

Example usage:

/* Example usage #1 */
let ex1_arr1 : [Any?] = ["foo", nil, 3, "bar"]
let ex1_arr2 : [Any?] = ["foo", nil, 3, "bar"]
compareAnyArrays(ex1_arr1, ex1_arr2) // true

/* Example usage #2 */
let ex2_arr1 : [Any?] = ["foo", nil, 2, "bar"]
let ex2_arr2 : [Any?] = ["foo", 3, 2, "bar"]
compareAnyArrays(ex2_arr1, ex2_arr2) // false
Community
  • 1
  • 1
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • This is most promising approach. It still will require to specify all types in a switch, but there are less types then sets of arrays * index in array. – Victor Ronin May 19 '16 at 21:52
  • @VictorRonin I believe it'll be difficult to circumvent that requirement, so you might have to live with the `switch` statement with the various types explicitly listed. Could possibly be circumvented using some runtime introspection hacks, but that's not something I would recommend for production code. Note that you can probably combine mine and Roman Sausarne:s answer if you'd like short-circuiting the array element comparison (in case of a `false` element comp.); in my answer above, the array will be fully element-by-element compared prior to summarizing the equality condition. – dfrib May 19 '16 at 21:56
  • Actually runtime introspection isn't that bad idea. There is a library EVReflection, which does a lot of it to serialize/deserialize JSON and it works very smooth (and reliably) – Victor Ronin May 19 '16 at 22:43
1

This is a marginal solution, but it should reduce some of the duplicative code that you are trying to avoid:

func compareAnyArray(a1: [Any?], _ a2: [Any?]) -> Bool {

    // A helper function for casting and comparing.
    func compare<T: Equatable>(obj1: Any, _ obj2: Any, t: T.Type) -> Bool {
        return obj1 as? T == obj2 as? T
    }

    guard a1.count == a2.count else { return false }

    return a1.indices.reduce(true) {

        guard let _a1 = a1[$1], let _a2 = a2[$1] else { return $0 && a1[$1] == nil && a2[$1] == nil }

        switch $1 {
        // Add a case statement for each index in the array:
        case 0:
            return $0 && compare(_a1, _a2, t: Int.self)
        case 1:
            return $0 && compare(_a1, _a2, t: String.self)
        default: 
            return false
        }
    }
}

That isn't the most beautiful solution, but it will reduce the amount of code you have to write, and it should work for any two [Any?], as long as you know that the type at index 0 is an Int, and the type at index 1 is a String, etc...

Aaron Rasmussen
  • 13,082
  • 3
  • 42
  • 43
  • I think it's interesting direction which was improved upon by dfri. However, your approach won't work for me out of the box, because different arrays have different types for different indexes. I mean set 1 will have Int, String, Int and set 2 will have String, String, Int. – Victor Ronin May 19 '16 at 21:54
1

A compact and Swift 5 version of great solution of Aaron Rasmussen:

func compare(a1: [Any?], a2: [Any?]) -> Bool {
    guard a1.count == a2.count else { return false }

    func compare<T: Equatable>(obj1: Any, _ obj2: Any, t: T.Type) -> Bool {
        return obj1 as? T == obj2 as? T
    }

    return a1.indices.reduce(true) {

        guard let _a1 = a1[$1], let _a2 = a2[$1] else { return $0 && a1[$1] == nil && a2[$1] == nil }

        switch $1 {
        case 0:  return $0 && compare(obj1: _a1, _a2, t: Int.self)
        case 1:  return $0 && compare(obj1: _a1, _a2, t: String.self)
        case 2:  return $0 && compare(obj1: _a1, _a2, t: <#TypeOfObjectAtIndex2#>.self)
        default: return false
        }
    }
}
eli7ah
  • 355
  • 2
  • 10