Home > Enterprise >  Swift - Sorting list by the existence of an optional key AND non empty string value of the key
Swift - Sorting list by the existence of an optional key AND non empty string value of the key

Time:05-17

I have a model called DataModel, 2 properties with string values. There is a list of this Model as seen below.

struct DataModel: Hashable, Codable {
    var value: String
    var optionalKey: String?
}

[
 {
   value: "100",
   optionalKey: "value"
 },
 {
   value: "200"
 },
 {
   value: "300",
   optionalKey: ""
 },
 {
   value: "400",
   optionalKey: "value"
 }
]

How can I safely sort the list so that any object where the 'optionalKey' exists, is at the beginning of the list. The desired result is as follows:


[
 {
   value: "100",
   optionalKey: "value"
 },
 {
   value: "400",
   optionalKey: "value"
 }
 {
   value: "200"
 },
 {
   value: "300",
   optionalKey: ""  // <- empty string to be treated as if key does not exist

 }
]

CodePudding user response:

To always get the objects where optionalKey is nil or empty sorted last you can do

let sorted = values.sorted(by: {
    let first = ($0.optionalKey ?? "").isEmpty ? 0 : 1
    let second = ($1.optionalKey ?? "").isEmpty ? 0 : 1
    return first > second
})

CodePudding user response:

If you're deserializing JSON and want an empty string to be nil you can implement your custom init function:

extension DataModel {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        value = try values.decode(String.self, forKey: .value)
        let optionalKey = try? values.decode(String.self, forKey: .optionalKey)
        // If `optionalKey` string has no characters assign nil
        self.optionalKey = optionalKey?.count == 0 ? nil : optionalKey
    }
}

Then sort the list based on the presence of the optionalKey value:

let list: [DataModel] = decodedData.sorted(by: { lhs, rhs in
    // `true` if left side is not `nil` and right side is `nil`
    // in other words, non-nil gets priority in the list 
    return lhs.optionalKey != nil && rhs.optionalKey == nil
})

CodePudding user response:

Te sort closure is quite simple.
You compare two objects: object1 & object2, where object1 is the first parameter closure and object2 the second one. Here they are DataModel instances.
You decide between these two that can be ANY object from the array which one should go first by returning true or false comparing them.
Under the hood, it can be ANY sorting method (bubble, etc.), it doesn't matter. You just have to compare two elements, and decide which one should go before the other one.

That's the kind of code you should be able to write:

let explicit = models.sorted { object1, object2 in
    // They both have optionalKey value
    if let optional1 = object1.optionalKey, let optional2 = object2.optionalKey {
        if optional1.isEmpty && !optional2.isEmpty { //One if empty -> The other one should go first
            return false
        } else if !optional1.isEmpty && optional2.isEmpty { //One if empty -> The other one should go first (the other version)
            return true
        } else { //They are both empties OR both non-empty, we compare then just with value
            return object1.value < object2.value
        }
    } else if let optional1 = object1.optionalKey { //Here, only object2 had nil optionalKey value
        if optional1.isEmpty { //optionKey of object1 exists, but is empty
            return false
        } else { //optionKey of object1 exists, and is not empty
            return true
        }
    } else if let optional2 = object2.optionalKey { //Here, only object2 had nil optionalKey value
        if optional2.isEmpty { //optionKey of object1 exists, but is empty
            return true
        } else { //optionKey of object2 exists, and is not empty
            return false
        }
    } else { //Both are nil
        return object1.value < object2.value
    }
}
print(explicit)

It's verbose, but it goes case by case, now we can simplify it:

let sorted = models.sorted { object1, object2 in
    //Since in your case being nil or empty is the same, let's simplify with giving empty value directly
    let optionalValue1 = object1.optionalKey ?? ""
    let optionalValue2 = object2.optionalKey ?? ""
    if optionalValue1.isEmpty && !optionalValue2.isEmpty {
        return false
    } else if !optionalValue1.isEmpty && optionalValue2.isEmpty {
        return true
    } else {
        return object1.value < object2.value
    }
}
print(sorted)

There could be even more less verbose code, BUT since you aren't able to even do sort it verbosely, I'd keep it as much "simplified". You still need to understand it to debug it/change it in future.

Now, I let that for later but, you might not compare the String values like that, since they are "numbers". For instance, if you have "2" and "10", in a String comparison, "10" is lower than "2", so you'd need to replace object1.value < object2.value with object1.value.compare(object2.value, options: .numeric) != .orderedDescending

CodePudding user response:

If you want to sort the DataModel object array according to optionalKey status and also value, You should look my code below.

Array element has optionalKey is firstly included in the sorting according to the value then other array elements is included in the sorting.

private func sortDataObjectByValue() -> [DataModel]{
    let models = [
        DataModel(value: "100", optionalKey: "value"),
        DataModel(value: "800", optionalKey: ""),
        DataModel(value: "1800", optionalKey: "value"),
        DataModel(value: "1200"),
        DataModel(value: "900", optionalKey: ""),
        DataModel(value: "500", optionalKey: "value")
    ]
    
    var sortedModels:[DataModel] = []
    var modelsWithKey:[DataModel] = []
    var modelsWithoutKey:[DataModel] = []
    
    for model in models {
        if let key = model.optionalKey, key != "" {
            modelsWithKey.append(model)
        }
        else {
            modelsWithoutKey.append(model)
        }
    }
    
    modelsWithKey = modelsWithKey.sorted(by: { (Int($0.value) ?? 0) < (Int($1.value) ?? 0) } )
    sortedModels  = modelsWithKey
    
    modelsWithoutKey = modelsWithoutKey.sorted(by: { (Int($0.value) ?? 0) < (Int($1.value) ?? 0) } )
    sortedModels  = modelsWithoutKey
    
    debugPrint(sortedModels)
    return sortedModels
}

0 : DataModel - value : "100" optionalKey : Optional - some : "value"

1 : DataModel - value : "500" optionalKey : Optional - some : "value"

2 : DataModel - value : "1800" optionalKey : Optional - some : "value"

3 : DataModel - value : "800" optionalKey : Optional - some : ""

4 : DataModel - value : "900" optionalKey : Optional - some : ""

5 : DataModel - value : "1200" - optionalKey : nil

Now you have sorted DataModel array according to optionalKey status and value.

  • Related