Swift:将对象数组按名称(字符串)按字母顺序映射到新数组中的单独字母集合中

6
我创建了一个名为Contact的结构体,它代表一个人类联系人,目前有一些在数组中。他们已经按字母顺序排序,但我想按照名字属性(一个字符串)按字母顺序对它们进行排序,但我不仅想将它们按顺序放在单个数组中,我还想将对象拆分成不同的集合,这些集合与它们的名称的第一个字母相对应。例如,“A”包含两个对象,其中一个联系人的姓名以A开头,“B”用于像Bobby、Brad等名字……依此类推。
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")
]

我想创建类似的东西

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

或者类似的东西...

最终结果是我想将它们显示到一个UITableView中,字母在Sections中,对象在indexPath.rows中,就像iPhone本地的联系人应用程序一样。实际上,我不确定是否这是实现此结果的最理想方式,所以欢迎对此问题提出任何挑战!

5个回答

10
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. 对列表进行排序。时间复杂度为O(nlogn),其中n是Array(contactData)中项的数量。
  2. 使用reduce遍历列表中的每个联系人,然后将其添加到新组或最后一个组。时间复杂度为O(n),其中n是Array(sortedContacts)中项的数量。

如果需要更好的打印信息,则最好使Contact符合CustomStringConvertible协议。


1

根据谓词将集合分块

我们可以受到Github用户oisdk:s chunk(n:)的启发,修改这个方法以根据提供的(Element, Element)->Bool谓词将Collection实例分块,用于决定是否应该与前面的元素一起包含在相同的块中。

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
    }
}

将此应用于您的示例

示例设置(根据您的说明,我们假设contactData数组已经排序)。

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")
]

使用上面的chunk(by:)方法,根据姓名首字母将contactData数组拆分为Contact实例的块:
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"] */

改进上述的 chunk(by:) 方法

在我最初(不能编译的)版本的 chunk(by:) 中,我想利用 Slice 实例 可用的 index(where:) 方法

// 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
    }
}

但似乎它无法正确解析此方法,可能是由于扩展中缺少约束条件(Collection where ...)。也许有人可以说明如何允许上述stdlib简化扩展?
然而,如果我们将其应用于Array,则可以实现这个更为简短的扩展,此时index(where:)可以成功调用ArraySlice实例(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
    }
}

该扩展的问题在于编译器对给定“Collection”的“SubSequence”所知道的内容非常有限 - 它只知道它是一个“Sequence”,但不知道它应该具有与主集合相同的元素类型(在这种情况下还应该是一个“Collection”并具有相同的索引类型)。这是因为我们没有关联类型的“where”子句(但很快就会有了,这将极大地改善情况 :))。 - Hamish
解决方案是添加约束where SubSequence : Collection, SubSequence.Iterator.Element == Iterator.Element, SubSequence.Index == Index,以让编译器知道:1)您可以在子序列上使用index(where:)方法;2)子序列的元素与主集合相同(因此可以使用谓词);3)返回的索引是相同的索引类型。尽管值得注意的是,子序列具有与集合相同的索引类型并不一定保证子序列索引值对应于集合的索引值。 - Hamish
这在切片中是有保证的,但据我所知,没有办法限制集合扩展,使子序列成为切片。虽然这样说,但我不知道是否存在这样的情况,即集合的子序列具有相同的索引类型,但不具有与主集合相同的索引值 - 但这仍然是一个边角案例,我觉得值得一提。 - Hamish
即使有了上述的限制,仍然存在一个问题,那就是在方法主体中调用了index(after: i),这可能会导致空集合崩溃 - 相反,您可以推迟j的初始化,例如 var j: Index,然后在赋值时使用j = self[index(after: i)..<endIndex].index(where: ...)(以防止第一个满足谓词的元素产生无限循环)。现在使用 index(after:) 是安全的,因为我们知道 i 不是 endIndex - Hamish
1
啊,关于子序列与主集合具有相同的索引类型但不具有相同值的边缘情况,忽略我之前的评论 - 有关范围下标的文档说:“访问的切片使用与原始集合相同的元素索引”,因此它听起来是Collection的语义要求,因此您的实现应该没问题。 - Hamish
显示剩余2条评论

1

使用 Dictionary's init(grouping:by:) 如下:

    lazy var sectionDictionary: Dictionary<String, [Contact]> = {


        return Dictionary(grouping: contactData, by: {

            // assumes title is a non-empty string
            let name = $0.name
            let normalizedName = name.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
            let firstCharAsString = String(normalizedName.first!).uppercased()
            return firstCharAsString
        })
    }()

我列出了不同的转换步骤,但如果您喜欢,可以将它们合并成一行。

这将生成一个字典,其中部分名称作为键,对象数组作为值。
从那里,您可以轻松提取部分数组的数组,并免费获取部分名称数组:

    lazy var sectionTitles: [String] = {

        return self.sectionDictionary.keys.sorted()
    }()

    lazy var sections: Array<[String]> = {

        return self.sectionTitles.map { sectionDictionary[$0]! }
    }()

请注意,我使用了一些强制解包,您应该在生产代码中进行guard

0

一种将集合过滤并拆分为多个小集合的解决方案,根据给定的谓词数量。

例如,给定整数数组 [1, 2, 3, 4] 和谓词:odd, even>3

结果将是[ [1, 3], [2, 4], [4] ]

注意:子集可能会因给定的谓词而重复。(我很好奇这是否可以改进。 reduce 的复杂度为:O(n))

extension Collection {
    func filterParts(_ predicates: ((Element) -> Bool)...) -> [ [Element] ] {
        let empty = predicates.map { _ -> [Element] in return [] }
        let enumeratedPredicates = predicates.enumerated()
        return reduce(empty) { (result, element) in
            var result = result
            enumeratedPredicates.forEach { offset, predicate in
                if predicate(element) {
                    result[offset].append(element)
                }
            }
            return result
        }
    }
}

0

在我看来,没有一种单一的映射方式可以做到这一点,因此算法是:

var sectionedData: [String: [Contact]] = [:]
contactData.forEach {
    guard let firstLetter = $0.name.characters.first else {
        sectionedData["#"] = (sectionedData["#"] ?? []) + [$0]
        return
    }
    let firstLetterStr = String(firstLetter)
    sectionedData[firstLetterStr] = (sectionedData[firstLetterStr] ?? []) + [$0]
}

let sortedContactData = sectionedData.sorted(by: { $0.0.key < $0.1.key })

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接