iosswiftsortingdictionarycustom-properties

Sort/Filter dictionary by property of object in Key


I have a dictionary like eg:

let dict = Dictionary<Month, Array<Int32>>()

Obj1.price = "10"
Obj1.value = "abc"
Obj1.title = "January"

Obj2.price = "10"
Obj2.value = "def"
Obj2.title = "April"

Obj3.price = "10"
Obj3.value = "pqr"
Obj3.title = "February"

Obj4.price = "4"
Obj4.value = "mnq"
Obj4.title = "April"

dict = [ Obj1: [3,4], Obj2 : [1,2], Obj3: [8,9], Obj4: [3,3] ]

I have a custom array of month

let sortTemplate = ["April", "May", "June", "July", "August", "September", "October", "November", "December", "January", "February", "March"]

I want to get the dictionary sorted as [ Obj2 : [1,2], Obj4: [3,3], Obj1: [3,4], Obj3: [8,9] ]

In short I am expecting to sort the dictionary according to custom reference array on property of key. I know we cannot have sorted dictionary but want to sort according to custom sortTemplate and insert into Array of dictionaries

Any hint in this regard will be useful. I know we can sort with values and also keys


Solution

  • This is one possible solution which uses Dictionarys built-in sort function, however presenting the title property in your example as custom enum rather than a String. The "sort template" is then given implicitly by the ordering of the months in the enum.

    I.e., enum MonthSortTemplate and your class MyClass (the latter which you have not shower us, so I made a MWE myself) as:

    enum MonthSortTemplate: Int {
        case April = 1
        case January
        case February
        // ... rest of months follows, in the order you prefer
    }
    
    class MyClass {
        var price = ""
        var value = ""
        var title: MonthSortTemplate = .April    
    }
    
    // Hashable (just some dummy, don't know how you've set this up)
    extension MyClass: Hashable {
        var hashValue: Int {
            return price.hashValue ^ value.hashValue
        }
    }
    
    // Equatable (just some dummy, don't know how you've set this up)
    func ==(lhs: MyClass, rhs: MyClass) -> Bool {
        return lhs.price == rhs.price && lhs.value == rhs.value
    }
    

    Create your MyClass instances, add into your dictionary, and use .sort(...) function of the latter for a custom closure, specified for this specific type of comparison.

    var Obj1 = MyClass()
    var Obj2 = MyClass()
    var Obj3 = MyClass()
    
    Obj1.price = "10"
    Obj1.value = "abc"
    Obj1.title = .January
    
    Obj2.price = "10"
    Obj2.value = "def"
    Obj2.title = .April
    
    Obj3.price = "10"
    Obj3.value = "pqr"
    Obj3.title = .February
    
    var dict = Dictionary<MyClass, Array<Int32>>()
    dict = [ Obj1: [3,4], Obj2 : [1,2], Obj3: [8,9]]
    
    // your custom sort closure, for Dictionary.sort(...) method
    let byMonthTemplate = {
        (elem1:(key: MyClass, val: [Int32]), elem2:(key: MyClass, val: [Int32]))->Bool in
        if elem1.key.title.rawValue < elem2.key.title.rawValue {
            return true
        } else {
            return false
        }
    }
    
    let sortedDict = dict.sort(byMonthTemplate)
    print("\(dict)")
    

    An alternative---if your really like your class property title to be of type String---is to define the < operator for MyClass objects:

    func <(lhs: MyClass, rhs: MyClass) -> Bool {
        // do comparison stuff with strings lhs.title and rhs.title
        // with regard to your ordering of choice (array sortTemplate)
        return ... 
    }
    

    In this case, the "messy" stuff ends up in this function, whereas the actual sorting can be performed quite elegantly as

    let sortedDict = dict.sort { $0.0 < $1.0 }
    

    Personally, I would prefer the enum solution (that, however, is off topic).


    EDIT:

    At your request, I'll include one example of the < operator for your class MyClass. This is by no means an optimal one, but perhaps you can refine it from my example.

    // add sortTemplate as a static property of MyClass
    class MyClass {
        var price = ""
        var value = ""
        var title = "" 
        static let sortTemplate = ["April", "May", "June", "July", "August", "September", "October", "November", "December", "January", "February", "March"]
    }
    
    // define < operator for MyClass objects
    func <(lhs: MyClass, rhs: MyClass) -> Bool {
        let indexOfLhs = MyClass.sortTemplate.indexOf({$0 == lhs.title})
        let indexOfRhs = MyClass.sortTemplate.indexOf({$0 == rhs.title})
        return indexOfLhs < indexOfRhs
    }
    
    // you can now sort your dictionary according to
    let sortedDict = dict.sort { $0.0 < $1.0 }