0

I am trying to sort an array of objects that has a string date field in the from of "MM-DD-YYYY" and a boolean field. I want to create a function that sorts the array by both the date field and by the boolean field. I am having a hard time figuring out how to do this. The done field is default set to false and is not needed in my init func.

var items = [BucketItem(title: "blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-14-2017"),BucketItem(title: "blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-11-2017"), BucketItem(title: "blah blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-9-2017")]

I looked at the .sort function but I don't how to compare the dates since they are in string format and I also don't know how to sort by two fields rather than just one. I want to sort so that if the boolean is true, then it is automatically less then an item with a boolean field of false. If both booleans are false, then it is sorted by date.

user4151797
  • 65
  • 2
  • 8
  • 3
    There is no Boolean field in your example? – Nirav D Feb 15 '17 at 17:03
  • Create a DateFormatter, with the right dateFormat, then `let date0 = dateformatter.date(from: $0.dat!)` and `let date1 = dateformatter.date(from: $1.dat!)`, and what's your logic with the boolean? How is done the sort? – Larme Feb 15 '17 at 17:15
  • 1
    Possible duplicate of [Swift - Sort array of objects with multiple criteria](http://stackoverflow.com/questions/37603960/swift-sort-array-of-objects-with-multiple-criteria) – Alexander Feb 15 '17 at 17:17
  • 1
    @Grimxn Chaining sorts like that doesn't work – Alexander Feb 15 '17 at 17:29
  • @Grimxn That's a coincidence, it doesn't work in the general case. `sorted` vs `sort` doesn't matter, either – Alexander Feb 15 '17 at 17:43
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/135796/discussion-between-alexander-and-grimxn). – Alexander Feb 15 '17 at 17:52

2 Answers2

2

I would create an internal extension of Date that defines an initializer that takes a string, and parses it into a Date. You can use this in your entire module to parse dates according to your project's choice of date string format.

import Foundation

internal extension Date {
    init(_ s: String) {
        let df = DateFormatter()
        df.dateFormat = "MM-d-yyyy"
        guard let date = df.date(from: s) else {
            fatalError("Invalid date string.")
        }
        self.init(timeIntervalSince1970: date.timeIntervalSince1970)
    }
}

Then I would modify your structure to store the Date, rather than the String. Of course, modify the initializer to match.

let items = [
    BucketItem(
        title: "blah",
        des: "description",
        lat: 134.6, lon: 27.0,
        date: Date("02-14-2017")
    ),
    BucketItem(
        title: "blah",
        des: "description",
        lat: 134.6, lon: 27.0,
        date: Date("02-11-2017")
    ),
    BucketItem(
        title: "blah blah",
        des: "description",
        lat: 134.6, lon: 27.0,
        date: Date("02-9-2017")
]

From here, you have Comparable Date instances that you can use in your sorting. To see how to sort based off multiple criteria, see my answer here. Here's the rough code:

extension BucketItem: Equatable {
    static func ==(lhs: BucketItem, rhs: BucketItem) -> Bool {
        return lhs.isDone == rhs.isDone
            && lhs.date == rhs.date
            // && lhs.foo == rhs.foo
            // ...and so on, for all criteria that define equality of two BucketItems
    }
}

func <(lhs: Bool, rhs: Bool) -> Bool {
    return !lhs && rhs // false is less than true
}

extension BucketItem: Comparable {
    static func <(lhs: BucketItem, rhs: BucketItem) -> Bool {
        // First sort by isDone
        if (lhs.isDone != rhs.isDone) { return lhs.isDone < rhs.isDone }
        // else if (lhs.foo != rhs.foo) { return lhs.foo < rhs.foo }
        // ...and so on, for all sort criteria
        else (lhs.date != rhs.date) { return lhs.date < rhs.date }
    } 
}

letsortedItems = items.sorted()
Community
  • 1
  • 1
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • does the internal extension go in the class that defines BucketItem? – user4151797 Feb 15 '17 at 19:52
  • @user4151797 nope, and it can't be – Alexander Feb 15 '17 at 19:56
  • Oh so then where do you define the internal extension ? – user4151797 Feb 15 '17 at 20:09
  • @user41517979 anywhere at the top level scope. For something like this (that you'll likely use in many places across your project), it may be worth putting it in its own file, but it's not necessary – Alexander Feb 15 '17 at 20:30
  • I did as you said but my sorting function is still giving me issues: `func tableViewSorter(item: BucketItem){ items.sort{ if $0.done && !$1.done{ //item $0 is less return $0 < $1 } else if !$0.done && $1.done{ //item $0 is greater return $0 > $1 } else if $0.done && $1.done { //want to compare by date return $0.date < $1.date } else{ //compare by date as well return $0.date < $1.date } } }` – user4151797 Feb 16 '17 at 05:40
  • i get "Binary operator cannot be applied to two bucketItem operands" error at each return statement. – user4151797 Feb 16 '17 at 05:41
  • @user4151797 There's a lot going on there. For one, you're trying to return `$0 < $1` in a function that doesn't have a return value. Secondly, `$0 < $1` is trying to call `<` with two `BucketItem` parameters. You haven't defined such a function. Have you seen the answer I've linked? – Alexander Feb 16 '17 at 05:47
0

You can define your BucketItem as

struct BucketItem {
let title: String
let des: String
let lat: Double
let lon: Double
let dat: String
let someBoolValue : Bool
var date: NSDate {
    get {
        return BucketItem.dateFormatter.dateFromString(dat)!
    }
}
static let dateFormatter: NSDateFormatter = {
    let formatter = NSDateFormatter()
    formatter.dateFormat = "MM-dd-yyyy"
    return formatter
    }()

}

Then call sort method on the array

 var items = [BucketItem(title: "blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-14-2017", someBoolValue: false),BucketItem(title: "blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-11-2017", someBoolValue :true), BucketItem(title: "blah blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-9-2017",someBoolValue: false),  BucketItem(title: "blah blah", des: "description", lat: 134.6, lon: 27.0, dat: "02-9-2017",someBoolValue: true)]
    items.sortInPlace{
        if $0.date != $1.date {
            return $0.date.compare($1.date) == NSComparisonResult.OrderedAscending
        }
        else {
            return !$0.someBoolValue && $1.someBoolValue
        }
    }
Sahana Kini
  • 562
  • 2
  • 8
  • `($0.someBoolValue != $1.someBoolValue) && $0.someBoolValue == false` can just be `!$0.someBoolValue && $1.someBoolValue` – Alexander Feb 15 '17 at 18:02
  • Also, I think it's better to take a date in the initializer. Adding the date conversion within `BucketItem` makes it less flexible; it forces you to use that one and only conversion. It's much better to just take a `Date` parameter, and let the consumer do the formatting as they wish – Alexander Feb 15 '17 at 18:03
  • @Alexander Thank you for the feedback :) – Sahana Kini Feb 15 '17 at 18:06
  • 1
    why not simply `$0.date < $1.date`? You can do that in Swift 3. – Sulthan Feb 15 '17 at 18:09