Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Map Array of Objects Alphabetically by Name(String) into Separate Letter Collections within a new Array

Tags:

arrays

ios

swift

I have created a struct called Contact which represents a human contact where there are currently a few in an Array. They are already sorted alphabetically however I would like to sort them alphabetically by the name property which is a String BUT I don't just want to have them in order in a single array, I would like to split the objects out into different collections which is corresponded by the first letter of their name. eg. "A" contains 2 objects where a Contacts name begins with A, "B" for names like Bobby, Brad etc.. and so on and so forth.

let contactData:[Contact] = [
  Contact(id: 1, available: true, name: "Adam"),
  Contact(id: 2, available: true, name: "Adrian"),
  Contact(id: 3, available: true, name: "Balthazar"),
  Contact(id: 4, available: true, name: "Bobby")
]

I would like to create something like

let sectionTitles = ["A", "B"]
let sortedContactData = [
  [
    Contact(name: "Adam"),
    Contact(name: "Adrian")
  ],
  [
     Contact(name:"Balthazar")
     Contact(name:"Bobby")
  ]         
]

Or something similar...

The end result is that I would like to display them into a UITableView with the letters in Sections and the Objects into indexPath.rows much like how the Contacts app native to the iPhone does it. I am actually not sure whether this is the most ideal way to achieve this result so I welcome any challenges to this question!

like image 784
fadfad Avatar asked Mar 23 '17 15:03

fadfad


People also ask

How do I sort a string array alphabetically in Swift?

You can use either sort() or sorted() to sort strings in ascending order.

How do you sort an array in descending order in Swift?

Strings in Swift conform to the Comparable protocol, so the names are sorted in ascending order according to the less-than operator ( < ). To sort the elements of your collection in descending order, pass the greater-than operator ( > ) to the sort(by:) method.

What is array map in Swift?

Example 1: Swift Array map() This is a short-hand closure that multiplies each element of numbers by 3. $0 is the shortcut to mean the first parameter passed into the closure. Finally, we have stored the transformed elements in the result variable.


2 Answers

let sortedContacts = contactData.sorted(by: { $0.name < $1.name }) // sort the Array first.
print(sortedContacts)

let groupedContacts = sortedContacts.reduce([[Contact]]()) {
    guard var last = $0.last else { return [[$1]] }
    var collection = $0
    if last.first!.name.characters.first == $1.name.characters.first {
        last += [$1]
        collection[collection.count - 1] = last
    } else {
        collection += [[$1]]
    }
    return collection
}
print(groupedContacts)
  1. sort the list. O(nlogn) , where n is the number of items in the Array(contactData).
  2. use reduce to iterate each contact in the list, then either add it to new group, or the last one. O(n), where n is the number of items in the Array(sortedContacts).

If you need to have a better printed information, you better make Contact conforms to protocol CustomStringConvertible

like image 102
antonio081014 Avatar answered Sep 28 '22 10:09

antonio081014


Chunk up a collection based on a predicate

We could let ourselves be inspired by Github user oisdk:s chunk(n:) method of collection, and modify this to chunk up a Collection instance based on a supplied (Element, Element) -> Bool predicate, used to decide whether a given element should be included in the same chunk as the preceeding one.

extension Collection {
    func chunk(by predicate: @escaping (Iterator.Element, Iterator.Element) -> Bool) -> [SubSequence] {
        var res: [SubSequence] = []
        var i = startIndex
        var k: Index
        while i != endIndex {
            k = endIndex
            var j = index(after: i)
            while j != endIndex {
                if !predicate(self[i], self[j]) {
                    k = j
                    break
                }
                formIndex(after: &j)
            }           
            res.append(self[i..<k])
            i = k
        }
        return res
    }
}

Applying this to your example

Example setup (where we, as you've stated, assume that the contactData array is already sorted).

struct Contact {
    let id: Int
    var available: Bool
    let name: String
}

let contactData: [Contact] = [
  Contact(id: 1, available: true, name: "Adam"),
  Contact(id: 2, available: true, name: "Adrian"),
  Contact(id: 3, available: true, name: "Balthazar"),
  Contact(id: 4, available: true, name: "Bobby")
]

Using the chunk(by:) method above to split the contactData array into chunks of Contact instances, based on the initial letter of their names:

let groupedContactData = contactData.chunk { 
    $0.name.characters.first.map { String($0) } ?? "" ==
        $1.name.characters.first.map { String($0) } ?? ""
}

for group in groupedContactData {
    print(group.map { $0.name })
} /* ["Adam", "Adrian"]
     ["Balthazar", "Bobby"] */

Improving the chunk(by:) method above

In my initial (non-compiling) version of chunk(by:) above, I wanted to make use of the index(where:) method available to Slice instances:

// does not compile!
extension Collection {
    func chunk(by predicate: @escaping (Iterator.Element, Iterator.Element) -> Bool) -> [SubSequence] {
        var res: [SubSequence] = []
        var i = startIndex
        var j = index(after: i)
        while i != endIndex {
            j = self[j..<endIndex]
                .index(where: { !predicate(self[i], $0) } ) ?? endIndex
            /*         ^^^^^ error: incorrect argument label in call
                                    (have 'where:', expected 'after:') */
            res.append(self[i..<j])
            i = j
        }
        return res
    }
}

But it seems as if it can not resolve this method correctly, probably due to a lacking constraint (Collection where ...) in the extension. Maybe someone can shed light on how to allow the stdlib-simplified extension above?

We may, however, implement this somewhat briefer extension if we apply it to Array, in which case index(where:) can be successfully called on the ArraySlice instance (self[...]):

// ok
extension Array {
    func chunk(by predicate: @escaping (Iterator.Element, Iterator.Element) -> Bool) -> [SubSequence] {
        var res: [SubSequence] = []
        var i = startIndex
        var j = index(after: i)
        while i != endIndex {
            j = self[j..<endIndex]
                .index(where: { !predicate(self[i], $0) } ) ?? endIndex
            res.append(self[i..<j])
            i = j
        }
        return res
    }
}
like image 25
dfrib Avatar answered Sep 28 '22 09:09

dfrib