8

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!

Frankie
  • 11,508
  • 5
  • 53
  • 60
  • This feels like a poor use of a dictionary – why not use an array of structs instead? – Hamish Aug 18 '16 at 17:19
  • Sure, but it's actually an array of `PFObject` and that's how it's setup when I pull directly from Parse :/ – Frankie Aug 18 '16 at 17:22
  • 2
    @Frankie You should use a separate model in your application that's independent of your persistence framework's model. This will help you not have to deal with such awkward datastructures in main application logic, and will also allow you the flexibility to easy change the underlying persistence layer in the future. – Alexander Aug 18 '16 at 17:26
  • @Frankie Can you elaborate exactly how you want this sort to work? How does `{"Name" : "Bill"}` rank compared to `{"Age" : 18}`? Sorting them seems nonsensical – Alexander Aug 18 '16 at 17:28
  • @AlexanderMomchliov Thanks for your comments, I've reworded my question to try and make it clearer. – Frankie Aug 18 '16 at 17:37
  • @Frankie how can you even have a name without an age, or vice versa? What does that even mean? – Alexander Aug 18 '16 at 17:38
  • @AlexanderMomchliov It's a contrived example. Just imagine you're looking through a database schema, not every field has a value but you can still order the column. – Frankie Aug 18 '16 at 17:41
  • @Sethmr I'm not the one who down voted you. Don't be so quick to blame. – Frankie Aug 18 '16 at 18:48

4 Answers4

8

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.

Code Different
  • 90,614
  • 16
  • 144
  • 163
7

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)]
]
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
0

Make a datatype to represent your data:

struct Person 
{
    let identifier: String
    let name: String?
    let age: Int?
}

Make an extraction routine:

func unpack(objects: [[String : Any]]) -> [Person]
{
    return objects.flatMap { object in

        guard let identifier = object["ObjectID"] as? String else {
            // Invalid object
            return nil
        }
        let name = object["Name"] as? String
        let age = object["Age"] as? Int

        return Person(identifier: identifier, name: name, age: age)
    }
}

Your datatype can be sorted by its fields, because they have real types.

let objects: [[String : Any]] = 
               [["ObjectID" : "1", "Name" : "Frank", "Age" : 32],
                ["ObjectID" : "2", "Name" : "Bill"],
                ["ObjectID" : "3", "Age" : 18],
                ["ObjectID" : "4", "Name" : "Susan", "Age" : 47]]

let persons = unpack(objects)

let byName = persons.sort { $0.name < $1.name }

nils compare as "before" any other value; you can write your own comparator if you'd like to change that.

jscs
  • 63,694
  • 13
  • 151
  • 195
-1

Here is what I would do. If you are able to, I would make the struct more specific by giving it a Name and Age other than just key and value. This should give you an outline for how to achieve that though!

struct PersonInfo {
    var key: String!
    var value: AnyObject?

    init(key key: String, value: AnyObject?) {
        self.key = key
        self.value = value
    }
}

class ViewController: UIViewController {
    var possibleKeys: [String] = ["Name", "Age", "ObjectId"]
    var personInfos: [PersonInfo] = []
    override func viewDidLoad() {
        super.viewDidLoad()
        for infos in json {
            for key in possibleKeys {
                if let value = infos[key] {
                    personInfos.append(PersonInfo(key: key, value: value))
                }
            }
        }
        personInfos.sortInPlace({$0.value as? Int > $1.value as? Int})
    }
}

to make it easier, here:

struct PersonInfo {
    var key: String!
    var objectId: Int!
    var name: String?
    var age: Int? 

    init(key key: String, objectId: Int, name: String?, age: Int?) {
        self.key = key
        self.objectId = objectId
        self.name = name
        self.age = age
    }
}

class ViewController: UIViewController {
    var possibleKeys: [String] = ["Name", "Age", "ObjectId"]
    var personInfos: [PersonInfo] = []
    override func viewDidLoad() {
        super.viewDidLoad()
        for infos in json {
            var objectId: String!
            var name: String? = nil
            var age: Int? = nil
            for key in possibleKeys {
                if let value = infos[key] {
                    if key == "ObjectId" {
                        objectId = value as? String
                    }
                    if key == "Name" {
                        name = value as? String
                    }
                    if key == "Age" {
                        age = value as? Int
                    }
                }
            }
            personInfos.append(PersonInfo(key: key, objectId: objectId, name: String?, age: Int?))
        }
        //by objectId
        personInfos.sortInPlace({$0.objectId? > $1.objectId?})

        //by age
        personInfos.sortInPlace({$0.age? > $1.age?})

        //byName
        personInfos.sortInPlace({$0.name?.compare($1.name?) == NSComparisonResult.OrderedAscending})
    }
}
Sethmr
  • 3,046
  • 1
  • 24
  • 42
  • This is roughly the right idea, but your implementation doesn't make sense. Why are `key` and `objectID` IUOs? Why are you only using one or the other of the possible "Name"/"Age" fields? Both might be present. Why are you generating a new objectID value instead of using the one from the input data? (And why is this in `viewDidLoad`?) – jscs Aug 18 '16 at 18:02
  • The objectId is a fluke because I didn't notice it as I was just throwing something together quickly. You should be able to fix that yourself. I am not doing one or the other. If you look, they are in a loop, so if they exist, they will be stored. You can directly store everything without checking what's in key this way if none of the values are optional. When they can be optional, this setup is only slightly more modular but barely preferable. – Sethmr Aug 18 '16 at 18:14
  • You're right, I misread the loop, but you don't need it anyways. You have to pass each value to the initializer explicitly, so you may as well just do `let identifier = infos["ObjectID"] as? String let name = infos["Name"] as? String let age = infos["Age"] as? Int` (ideally putting the keys in something other than literal strings). – jscs Aug 18 '16 at 18:19
  • Oh lol, well, like I said... it's a setup I like to use, but it is way way more efficient when they are all guarenteed fields – Sethmr Aug 18 '16 at 18:20