Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift sort array of dictionaries by key where value is optional AnyObject

I'm pulling an array of dictionaries straight from Parse and displaying them in a table. So I'd really like to work with the data structure I'm handed (the oddly structured dictionaries below).

A PFObject is [String : AnyObject?] and I want to be able to sort by any key so I don't know the object type AND the key might be missing from some of the dictionaries. Because in Parse, if you don't give a property a value, it is simply nonexistent. For example:

[
    {
        "ObjectId" : "1",
        "Name" : "Frank",
        "Age" : 32
    },
    {
        "ObjectId" : "2",
        "Name" : "Bill"
    },
    {
        "ObjectId" : "3",
        "Age" : 18
    }
    {
        "ObjectId" : "4",
        "Name" : "Susan",
        "Age" : 47
    }

]

I want the dictionaries with missing keys to always be ordered after the sorted dictionaries. An example:

Original Table:

ObjectId   Name       Age
1          Frank      32
2          Bill     
3                     18
4          Susan      47

Ordered By Name:

ObjectId   Name       Age
2          Bill       
1          Frank      32
4          Susan      47
3                     18

As I don't have a lot of control over the data model, and it's usage is limited throughout the application, I'd prefer to focus on an algorithmic solution rather than structural.

I came up with a way to do this but it just seems inefficient and slow, I'm certain there's someone who can do this better.

//dataModel is an array of dictionary objects used as my table source
//sort mode is NSComparisonResult ascending or descending
//propertyName is the dictionary key

        //first filter out any objects that dont have this key
        let filteredFirstHalf = dataModel.filter({ $0[propertyName] != nil })
        let filteredSecondHalf = dataModel.filter({ $0[propertyName] == nil })

        //sort the dictionaries that have the key
        let sortedAndFiltered = filteredFirstHalf { some1, some2 in

            if let one = some1[propertyName] as? NSDate, two = some2[propertyName] as? NSDate {
                return one.compare(two) == sortMode
            } else if let one = some1[propertyName] as? String, two = some2[propertyName] as? String {
                return one.compare(two) == sortMode
            } else if let one = some1[propertyName] as? NSNumber, two = some2[propertyName] as? NSNumber {
                return one.compare(two) == sortMode
            } else {
                fatalError("filteredFirstHalf shouldn't be here")
            }
        }

        //this will always put the blanks behind the sorted
        dataModel = sortedAndFiltered + filteredSecondHalf

Thanks!

like image 673
Frankie Avatar asked Aug 18 '16 17:08

Frankie


2 Answers

Swift can't compare any two objects. You have to cast them to a specific type first:

let arr: [[String: Any]] = [
    ["Name" : "Frank", "Age" : 32],
    ["Name" : "Bill"],
    ["Age" : 18],
    ["Name" : "Susan", "Age" : 47]
]

let key = "Name" // The key you want to sort by

let result = arr.sort {
    switch ($0[key], $1[key]) {
        case (nil, nil), (_, nil):
            return true
        case (nil, _):
            return false
        case let (lhs as String, rhs as String):
            return lhs < rhs
        case let (lhs as Int, rhs as Int):
            return  lhs < rhs
        // Add more for Double, Date, etc.
        default:
            return true
    }
}

print(result)

If there are multiple dictionaries that have no value for the specified key, they will be placed at the end of the result array but their relative orders are uncertain.

like image 81
Code Different Avatar answered Oct 09 '22 13:10

Code Different


Requirements

So you have an array of dictionaries.

let dictionaries: [[String:AnyObject?]] = [
    ["Name" : "Frank", "Age" : 32],
    ["Name" : "Bill"],
    ["Age" : 18],
    ["Name" : "Susan", "Age" : 47]
]

You want to sort the array:

  • with the Name value ascending
  • dictionaries without a Name String should be at the end

Solution

Here's the code (in functional programming style)

let sorted = dictionaries.sort { left, right -> Bool in
    guard let rightKey = right["Name"] as? String else { return true }
    guard let leftKey = left["Name"] as? String else { return false }
    return leftKey < rightKey
}

Output

print(sorted)

[
    ["Name": Optional(Bill)],
    ["Name": Optional(Frank), "Age": Optional(32)],
    ["Name": Optional(Susan), "Age": Optional(47)],
    ["Age": Optional(18)]
]
like image 31
Luca Angeletti Avatar answered Oct 09 '22 11:10

Luca Angeletti