1

Using Swift 4.2, I'm trying to figure out sorting my objects that contain two dates, such that the most recent object is first in the array.

list of objects in DB

I'm experimenting with the following, but it fails due to:

Binary operator '>=' cannot be applied to two '(Date?, Date?)' operands.

let sortedSignComments = signComments.sorted(by: {
    guard let serverDate0 = $0.serverLastUpdated else { return false }
    guard let serverDate1 = $1.serverLastUpdated else { return false }

    guard let clientDate0 = $0.clientLastUpdated else { return false }
    guard let clientDate1 = $1.clientLastUpdated else { return false }

    let lhs = (formatter.date(from: serverDate0), formatter.date(from: clientDate0))
    let rhs = (formatter.date(from: serverDate1), formatter.date(from: clientDate1))

    return lhs >= rhs
})
Sebastian Dwornik
  • 2,526
  • 2
  • 34
  • 57
  • There is no single natural way to sort pairs of dates. What precisely does it mean, *for your purposes*, for LHS to be sorted "before" RHS? – Mike Mertsock Nov 27 '18 at 20:48
  • 3
    You have to decide which is the first sort criteria (say serverdate). If the server dates are equal then compare the client dates. If the server dates are different then just return the result of that comparison – Paulw11 Nov 27 '18 at 20:49
  • You probably also want to reconsider your `guard` statements. Your data shows that you have null values and your current code just "gives up" if there is a null in any of the four values – Paulw11 Nov 27 '18 at 20:50
  • since you return false in all guards that makes the code safe but may result in unexpected comparison results – Shehata Gamal Nov 27 '18 at 20:54
  • It's a work in progress. The guard's will be changed. @Paulw11 The server dates will _never_ be the exact same. But client dates can't be ignored because they might be more recent then the server date. – Sebastian Dwornik Nov 27 '18 at 20:57
  • Related: [Sort array of objects with multiple criteria](https://stackoverflow.com/questions/37603960/swift-sort-array-of-objects-with-multiple-criteria) – Martin R Nov 27 '18 at 21:04
  • @SebastianDwornik: You have to tell us how exactly the entries should be compared. What if both elements have a server date and a client date? What if one has only a server date and the other only a client date, which one comes first? Etc, etc ... – Otherwise we can only *guess* the correct solution. – Martin R Nov 27 '18 at 21:13
  • What about treating dates as timestamps, taking `maximum(_ x: Double, _ y: Double)` from tuple and then sort by comparing those values? – gorzki Nov 27 '18 at 21:14
  • @MartinR These are timeStamps and should be sorted as such with the most recent one being first. Either the server or the client will have the most recent timestamp for that object. – Sebastian Dwornik Nov 27 '18 at 21:18
  • Also related https://stackoverflow.com/a/53427282/2907715 (A proper way of dealing with Optional comparison by Mr Martin) – ielyamani Nov 27 '18 at 21:25
  • This isn't an answer to your question, but using a `DateFormatter` to convert a string to a `Date` is a slow operation. Your code is doing 4 string-to-date conversions for every comparison. That is going to slow your sorting down dramatically. You should do a pass on your data converting your date strings to `Date` values first, and then sort based on dates. If you're sorting more than a few hundred records your sort will be REALLY Slow otherwise. – Duncan C Nov 27 '18 at 21:26
  • @DuncanC I thought creating the `DateFormatter()` was mainly the slow operation. Once it's created, all other operations are negligible. But we digress... – Sebastian Dwornik Nov 27 '18 at 21:34
  • @SebastianDwornik sorry but the whole process is wrong ( tuple isn't encouraged here at all , return false for nil values , using formatter inside the sort ) – Shehata Gamal Nov 27 '18 at 21:41
  • @SebastianDwornik: If you have a solution for your problem then you should post it as an *answer,* not as an update to your question. – Martin R Nov 28 '18 at 07:57

3 Answers3

0

I recommend

1-to use class / struct as a model instead of tuple

2- Make the server && client vars as either Date/timeStamp that you fill when you get the data with the formatter

3- Sort according to the properties

For your current code you need

return lhs.$0! >= rhs.$0! && lhs.$1! >= rhs.$1!
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • 1
    The sort predicate must be a “strict weak ordering” – and this isn't one. – Martin R Nov 27 '18 at 20:59
  • @MartinR comm#1 this depends on how the op wants the result simple `>=' can be replaced with `<` comm#2 why assume it'll fail if it's a date created / stored in server with same dateformat as getting it ?? – Shehata Gamal Nov 27 '18 at 21:17
0

This seems to work well now. Thank you all for helping.

// fyi ref: @Martin R, https://stackoverflow.com/a/53427282/7599
func filterOptionalsWithLargeNil<T: Comparable>(lhs: T?, rhs: T?) -> T? {
    var result: T?

    switch (lhs, rhs) {
    case let(l?, r?): result = l > r ? l : r    // Both lhs and rhs are not nil
    case let(nil, r?): result = r               // Lhs is nil
    case let(l?, nil): result = l               // Lhs is not nil, rhs is nil
    case (.none, .none):
        result = nil
    }

    return result
}

...

    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.timeZone = TimeZone.autoupdatingCurrent
    formatter.dateFormat = DALconfig.ISO8601dateFormat

    let sortedSignComments = signComments.sorted(by: {
        // Convert ISO8601 date string format to Date() object.
        let serverDate0: Date? = $0.serverLastUpdated != nil ? formatter.date(from: $0.serverLastUpdated!) : nil
        let serverDate1: Date? = $1.serverLastUpdated != nil ? formatter.date(from: $1.serverLastUpdated!) : nil
        let clientDate0: Date? = $0.clientLastUpdated != nil ? formatter.date(from: $0.clientLastUpdated!) : nil
        let clientDate1: Date? = $1.clientLastUpdated != nil ? formatter.date(from: $1.clientLastUpdated!) : nil

        // Filter and sort each object separately.
        let objDate0: Date? = self.filterOptionalsWithLargeNil(lhs: serverDate0, rhs: clientDate0)
        let objDate1: Date? = self.filterOptionalsWithLargeNil(lhs: serverDate1, rhs: clientDate1)

        // Final comparison.
        guard let finalDate0 = objDate0 else { return false }
        guard let finalDate1 = objDate1 else { return false }

        return finalDate0.compare(finalDate1) == .orderedDescending

    })

Results in:

enter image description here

Sebastian Dwornik
  • 2,526
  • 2
  • 34
  • 57
0

If your server and clients are always going to be in the same time zone (and you don't mind the fall daylight saving time gap) you could sort the objects without converting the strings to Dates.

If you have a large number of objects it will be more efficient to map to a sortable value that you apply a regular sort on, rather than performing the date conversions N*Log(N) times.

For example:

func getSortKey(obj:MyObject) -> Date { return .... }
sortedObjects = objectList.map{($0,getSortKey($0))}.sorted{$0.1>$1.1}.map{$0.0}

For your specific requirements, the getSortKey() function would probably need to pick one of the two dates out of the object so that there is only one value to compare. I'm guessing you will want the objects to be sorted by the latest of the two dates so the function could return something like this (pseudo code) :

 var sortKey = Date.distantPast 
 if let serverDateString = obj.serverLastUpdated,
    let serverDate       = formatter.date(from: serverDateString)
 { sortKey = serverDate }
 if let clientDateString = obj.clientLastUpdated,
    let clientDate       = formatter.date(from: clientDateString),
    sortKey < clientDate
 { sortKey = clientDate }
 return  sortKey

If you don't have a large number of objects, you could use the function directly in the sort:

sortedObjects = objectList.sorted{getSortKey($0)>getSortKey($1)}
Alain T.
  • 40,517
  • 4
  • 31
  • 51