繁体   English   中英

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

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

我创建了一个名为 Contact 的结构,它代表一个人类联系人,其中当前有几个在一个数组中。 它们已经按字母顺序排序,但是我想按 name 属性按字母顺序对它们进行排序,该属性是一个字符串,但我不只是想将它们按顺序排列在单个数组中,我想将对象拆分为不同的集合对应于他们名字的第一个字母。 例如。 “A”包含 2 个对象,其中联系人名称以 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 中的字母和 Objects 显示到 indexPath.rows 中,就像 iPhone 原生的 Contacts 应用程序那样。 我实际上不确定这是否是实现这一结果的最理想方式,所以我欢迎对这个问题提出任何挑战!

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

根据谓词对集合进行分块

我们可以让自己受到 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[...] )上成功调用index(where:)

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

恕我直言,没有单地图方法可以做到这一点,所以算法是:

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

你可能想这样做:

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 mapped = stride(from: 0, to: contactData.count, by: 2).map {
    [contactData[$0], contactData[$0+1]]
}

print(mapped)

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

mapped将表示Contact数组的数组,每个数组应包含一对对象。

将集合过滤和拆分为与给定谓词一样多的较小集合的解决方案。

例如给定的整数数组 [1, 2, 3, 4] 应用谓词: oddeven>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
        }
    }
}

像这样使用字典的 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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM