0

Using Swift 2.1 I'm trying to make a function that sorts an array of dictionaries by the date value for the key dateKey.

I want to add this as an extension to the Array type, so I can call it using someArray.sortDictionariesByDate(dateKey: String, dateFormatter: NSDateFormatter)

extension Array where Element: CollectionType {
  mutating func sortDictionariesByDate(dateKey: String, dateFormatter: NSDateFormatter) {
    sortInPlace {
      dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
      if let dateStringA: String = ($0 as? [String: AnyObject])?[dateKey] as? String, let dateStringB = ($1 as? [String: AnyObject])?[dateKey] as? String {
        if let dateA: NSDate = dateFormatter.dateFromString(dateStringA), dateB: NSDate = dateFormatter.dateFromString(dateStringB) {
          return dateA.compare(dateB) == .OrderedAscending
        }
      }
      return false
    }
  }
}

This works fine as long as the dictionaries are typed as [String: AnyObject], but if I use it on a dictionary of type [String: String] it doesn't work since it's unable to cast String to AnyObject. I assume this is because String is a Struct not a Class. I've also tried to typecast the elements to [String: Any] instead, but then it won't work regardless of using dictionaries of type [String: AnyObject] or [String: String].

Is there any cast I can use to support dictionaries with key type String and any value type (String, AnyObject etc.), or perhaps a where clause or protocol conformance that can be added to the extension to avoid casting completely?

EDIT: Here are two example arrays as per request

var sampleArray1: [[String: AnyObject]] = [["date": "2015-10-24T13:00:00.000Z", "foo": "bar"], ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"]]
sampleArray1.sortDictionariesByDate("date", dateFormatter: NSDateFormatter())
var sampleArray2: [[String: String]] = [["date": "2015-10-24T13:00:00.000Z", "foo": "bar"], ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"]]
sampleArray2.sortDictionariesByDate("date", dateFormatter: NSDateFormatter())
thomey
  • 97
  • 9
  • Help us help you, add some sample input data please – Code Different Oct 22 '15 at 17:05
  • Edited to add sample data – thomey Oct 22 '15 at 18:34
  • The problem is that what's in your dictionaries is not dates. It's just strings. Why don't you store _actual dates_ in your dictionaries? Then it's _trivial_ to sort them and you won't need your extension. – matt Oct 22 '15 at 18:36
  • Also there is no need for your `.OrderedAscending` foo. A date is just a number of seconds; it is directly sortable on that number. – matt Oct 22 '15 at 18:38
  • @matt Thanks for your input. I thought having the function as an extension would make it more reusable. I prefer leaving the dates as strings, as that's the way they're returned from the API. I could parse the response creating dates, but I'm still a bit puzzled about how to easily sort the array of dictionaries by the value of the date key without explicitly declaring the dictionary type to be able to subcript it. `Array.sortInPlace` still requires typecasting even if the value is NSDate. Regarding your last comment, did you mean to use `timeIntervalSinceNow` instead of `NSDate.compare()`? – thomey Oct 22 '15 at 20:17
  • 1
    Actually I'd use `timeIntervalSinceReferenceDate`, but yes, exactly what I mean. – matt Oct 22 '15 at 20:22
  • Gotcha. And as for the sortInPlace part...? To my knowledge that would still require knowing the type of the dictionaries in order to be able to get the date values by keys? I could of course do `if let dict1 = $0 as? [String: AnyObject], let dict2 = $0 as? [String: AnyObject] {...} else if let dict1 = $0 as? [String: String], let dict2 = $0 as? [String: String] {...}`, but that means specifically adding all the desired types, whereas I was hoping to have a generic function to sort any Array of dictionaries as long as they contain the key "date"... – thomey Oct 22 '15 at 20:37

3 Answers3

2

First issue: you are comparing strings, not dates. Fortunately, your string's format make it directly comparable as both a string and the date value it represents. Hence, you don't need to convert it to NSDate at all.

The second issue is that typecasting Dictionary<Key,Value1> to Dictionary<Key,Value2> wholesale doesn't work, even when Value1 is covariant on Value2. It may work in trivial example like this...

let a : [String: String] = ["name": "David", "location": "Chicago"]
let b = a as [String: AnyObject]

...because the compiler can see the value type (String) in a and optimize it by compile time. For dynamic situations like yours, there's no information available ahead of time.


When you need dynamism, you can always go back to the old friend, Objective-C:

extension Array where Element: CollectionType {
    mutating func sortDictionariesByDate(dateKey: String) {
        self.sortInPlace {
            if let a = $0 as? NSDictionary, b = $1 as? NSDictionary {
                return (a[dateKey] as? String) < (b[dateKey] as? String)
            } else {
                return false
            }
        }
    }
}

// Example:
var sampleArray1: [[String: AnyObject]] = [
    ["date": "2015-10-24T13:00:00.000Z", "foo": "bar"],
    ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"],
    ["date": "2015-10-24T10:00:00.000Z", "foo": "bar"]
]
var sampleArray2: [[String: String]] = [
    ["date": "2015-10-24T13:00:00.000Z", "foo": "bar"], 
    ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"],
    ["date": "2015-10-24T10:00:00.000Z", "foo": "bar"]
]

sampleArray1.sortDictionariesByDate("date")
sampleArray2.sortDictionariesByDate("date")

Note that since you are now comparing strings rather than date, no NSDateFormatter is needed.

Code Different
  • 90,614
  • 16
  • 144
  • 163
  • I would have preferred a pure Swift solution, but I haven't been able to find any that provide the same dynamism and simplicity, so I'm marking this as the correct answer. – thomey Oct 23 '15 at 08:41
1

What you're asking to do is silly. You're not going to write a single function that works on both [[String:AnyObject]] and [[String:String]] without casting. Swift has strict typing. That's just how it is. Cast and be happy!

The trick to casting an array of a complex type like a dictionary is to use map to cast each element individually (this is actually what array casting does behind the scenes anyway):

let sampleArray1: [[String: AnyObject]] = [["date": "2015-10-24T13:00:00.000Z", "foo": "bar"], ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"]]
let sampleArray2: [[String: String]] = [["date": "2015-10-24T13:00:00.000Z", "foo": "bar"], ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"]]

func sortByDate(arr:[[String:AnyObject]]) -> [[String:AnyObject]] {
    let result = arr.sort {
        d1, d2 in
        let dat1 = NSDateFormatter().dateFromString(d1["date"] as! String)!
        let dat2 = NSDateFormatter().dateFromString(d2["date"] as! String)!
        return dat1.timeIntervalSinceReferenceDate < dat2.timeIntervalSinceReferenceDate
    }
    return result
}

let arr1 = sortByDate(sampleArray1)
let arr2 = sortByDate(sampleArray2.map{$0 as [String:AnyObject]})
// and if you have to, `map` arr2 back down to [String:String]

I admit I didn't completely fill in the spec, but it's easy to add more parameters so that you supply the key name and the formatter. Note that the example will actually crash on your data because we have no matching date format. You need to supply a valid format.

I also think (as part of that same point) it's silly that you've put date strings into your dictionaries rather than actual dates. But I've gone along with the premise just for the sake of argument.

thomey
  • 97
  • 9
matt
  • 515,959
  • 87
  • 875
  • 1,141
0

Here is a different solution that keeps the function as a extension of Array while handling the casting within the function thus avoiding having to use .map() back and forth when dealing with different types of dictionaries. The downside is that you have to known the types supported by the function in advance, whereas @matt 's solution lets you handle the casting when calling the method, at the expense of not being an extension, not sorting in place and mapping back and forth.

extension Array where Element: CollectionType {
  mutating func sortDictionariesByDate(dateKey: String, dateFormatter: NSDateFormatter) {
    sortInPlace {
      dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
      var dateString1: String?
      var dateString2: String?

      if let a = $0 as? [String: AnyObject], let b = $1 as? [String: AnyObject] {
        if let dateStringA: String = a[dateKey] as? String, dateStringB: String = b[dateKey] as? String {
          dateString1 = dateStringA
          dateString2 = dateStringB
        }
      } else if let a = $0 as? [String: String], let b = $1 as? [String: String] {
        dateString1 = a[dateKey]
        dateString2 = b[dateKey]
      }

      if let dateString1: String = dateString1, let dateString2: String = dateString2 {
        if let date1: NSDate = dateFormatter.dateFromString(dateString1), date2: NSDate = dateFormatter.dateFromString(dateString2) {
          return date1.timeIntervalSinceReferenceDate < date2.timeIntervalSinceReferenceDate
        }
      }
      return false
    }
  }
}

// Example:
var sampleArray1: [[String: AnyObject]] = [
    ["date": "2015-10-24T13:00:00.000Z", "foo": "bar"],
    ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"],
    ["date": "2015-10-24T10:00:00.000Z", "foo": "bar"]
]
var sampleArray2: [[String: String]] = [
    ["date": "2015-10-24T13:00:00.000Z", "foo": "bar"], 
    ["date": "2015-10-24T14:00:00.000Z", "foo": "bar"],
    ["date": "2015-10-24T10:00:00.000Z", "foo": "bar"]
]

sampleArray1.sortDictionariesByDate("date")
sampleArray2.sortDictionariesByDate("date")
thomey
  • 97
  • 9