如何在Swift中按数组元素进行分组

137

假设我有以下这段代码:

class Stat {
   var statEvents : [StatEvents] = []
}

struct StatEvents {
   var name: String
   var date: String
   var hours: Int
}


var currentStat = Stat()

currentStat.statEvents = [
   StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
   StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
]

var filteredArray1 : [StatEvents] = []
var filteredArray2 : [StatEvents] = []
我可以手动多次调用下一个函数,以便将2个数组按“相同名称”分组。

我可以手动多次调用下一个函数,以便将2个数组按“相同名称”分组。

filteredArray1 = currentStat.statEvents.filter({$0.name == "dinner"})
filteredArray2 = currentStat.statEvents.filter({$0.name == "lunch"})

问题在于我不知道变量值,比如这里的 "dinner" 和 "lunch",因此我希望可以按名称自动将 statEvents 数组分组,以便每个不同的名称都对应一个数组。

我该如何做到这一点?


1
请查看我的Swift 4答案,它使用了新的Dictionary init(grouping:by:)初始化器。 - Imanou Petit
16个回答

264

Swift 4:

自 Swift 4 开始,这个功能已经被添加到标准库中。您可以像这样使用它:

Dictionary(grouping: statEvents, by: { $0.name })

[
  "dinner": [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ],
  "lunch": [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
]

Swift 3:

public extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        var categories: [U: [Iterator.Element]] = [:]
        for element in self {
            let key = key(element)
            if case nil = categories[key]?.append(element) {
                categories[key] = [element]
            }
        }
        return categories
    }
}

很不幸,上面的append函数复制了底层数组,而不是原地进行变异,这并不是最好的选择。这会导致相当大的减速。你可以通过使用引用类型包装器来解决这个问题:

class Box<A> {
  var value: A
  init(_ val: A) {
    self.value = val
  }
}

public extension Sequence {
  func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
    var categories: [U: Box<[Iterator.Element]>] = [:]
    for element in self {
      let key = key(element)
      if case nil = categories[key]?.value.append(element) {
        categories[key] = Box([element])
      }
    }
    var result: [U: [Iterator.Element]] = Dictionary(minimumCapacity: categories.count)
    for (key,val) in categories {
      result[key] = val.value
    }
    return result
  }
}

即使你对最终的字典进行两次遍历,这个版本在大多数情况下仍然比原始版本更快。

Swift 2:

public extension SequenceType {
  
  /// Categorises elements of self into a dictionary, with the keys given by keyFunc
  
  func categorise<U : Hashable>(@noescape keyFunc: Generator.Element -> U) -> [U:[Generator.Element]] {
    var dict: [U:[Generator.Element]] = [:]
    for el in self {
      let key = keyFunc(el)
      if case nil = dict[key]?.append(el) { dict[key] = [el] }
    }
    return dict
  }
}

在您的情况下,您可以让keyFunc返回的“键”是名称:
currentStat.statEvents.categorise { $0.name }
[  
  dinner: [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ], lunch: [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
  ]
]

你将会得到一个字典,其中每个键都是一个名称,每个值都是该名称对应的StatEvents数组。
func categorise<S : SequenceType, U : Hashable>(seq: S, @noescape keyFunc: S.Generator.Element -> U) -> [U:[S.Generator.Element]] {
  var dict: [U:[S.Generator.Element]] = [:]
  for el in seq {
    let key = keyFunc(el)
    dict[key] = (dict[key] ?? []) + [el]
  }
  return dict
}

categorise(currentStat.statEvents) { $0.name }

这将产生输出:

extension StatEvents : Printable {
  var description: String {
    return "\(self.name): \(self.date)"
  }
}
print(categorise(currentStat.statEvents) { $0.name })
[
  dinner: [
    dinner: 01-01-2015,
    dinner: 01-01-2015,
    dinner: 01-01-2015
  ], lunch: [
    lunch: 01-01-2015,
    lunch: 01-01-2015
  ]
]

(The swiftstub在这里


非常感谢@oisdk!您知道是否有一种方法可以访问创建的字典值的索引吗?我的意思是,我知道如何获取键和值,但我想获得这些字典的索引“0”,“1”,“2”... - Ruben
哎呀,它总是返回空值(nil)。 - Ruben
Swift 1.2 中与 .indices 等价的是 .keys。但是请注意,Swift 的 Dictionary 类型没有定义的排序方式。因此,如果您在字典上调用 .keys.indices,不能保证每次都会按相同的顺序返回键。为了支持类似于分组的 UITableView,您需要将键提取到一个数组中,并使用该数组作为各个部分标题的后备存储。 - memmons
1
这是解决方案的一个良好开端,但它有一些缺点。在这里使用模式匹配(if case)是不必要的,更重要的是,使用dict[key]?.append)向字典中存储的内容添加元素会导致每次都进行复制。请参阅http://rosslebeau.com/2016/swift-copy-write-psa-mutating-dictionary-entries。 - Alexander
2
字典(通过\.name将currentStat.statEvents分组) - Leo Dabus
显示剩余8条评论

87

在Swift 5中,Dictionary有一个名为init(grouping:by:)的初始化方法。 init(grouping:by:)的声明如下:

init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence

创建一个新的字典,其中键是由给定闭包返回的分组,值是返回每个特定键的元素数组。


以下 Playground 代码展示了如何使用 init(grouping:by:) 来解决您的问题:


struct StatEvents: CustomStringConvertible {
    
    let name: String
    let date: String
    let hours: Int
    
    var description: String {
        return "Event: \(name) - \(date) - \(hours)"
    }
    
}

let statEvents = [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
]

let dictionary = Dictionary(grouping: statEvents, by: { (element: StatEvents) in
    return element.name
})
//let dictionary = Dictionary(grouping: statEvents) { $0.name } // also works  
//let dictionary = Dictionary(grouping: statEvents, by: \.name) // also works

print(dictionary)
/*
prints:
[
    "dinner": [Event: dinner - 01-01-2015 - 1, Event: dinner - 01-01-2015 - 1],
    "lunch": [Event: lunch - 01-01-2015 - 1, Event: lunch - 01-01-2015 - 1]
]
*/

4
可以的,这段代码还可以写成 let dictionary = Dictionary(grouping: statEvents) { $0.name }。这是一种语法糖写法。 - user1046037
1
这应该是从Swift 4开始的答案 - 完全由苹果支持,并希望具有高性能。 - Herbal7ea
还要注意谓词中返回的非可选键,否则您将看到错误:“没有更多上下文的表达式类型不明确...” - Asike
4
这段代码使用 Swift 5.2 中的 Dictionary(grouping: statEvents, by: \.name) 方法对 statEvents 数组中的元素按名称进行分组。 - Leo Dabus

34

Swift 4: 您可以使用Apple 开发者网站中的init(grouping:by:)

例子:

let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let studentsByLetter = Dictionary(grouping: students, by: { $0.first! })
// ["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]

那么在你的情况下

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })

这也适用于keypath:让字典 = 字典(分组:currentStat.statEvents,by:.name) - Jim Haungs

26

对于 Swift 3:

public extension Sequence {
    func categorise<U : Hashable>(_ key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        var dict: [U:[Iterator.Element]] = [:]
        for el in self {
            let key = key(el)
            if case nil = dict[key]?.append(el) { dict[key] = [el] }
        }
        return dict
    }
}

使用方法:

currentStat.statEvents.categorise { $0.name }
[  
  dinner: [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ], lunch: [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
  ]
]

9
非常感谢!如果能提供使用示例的话,将会非常有帮助 :) 谢谢! - Centurion
以下是一个使用示例:yourArray.categorise(currentStat.statEvents) { $0.name }。该函数将返回Dictionary<String,Array<StatEvents>>。 - Centurion

10

在Swift 4中,这个扩展具有最佳性能并有助于链接您的运算符。

extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        return Dictionary.init(grouping: self, by: key)
    }
}

例子:

struct Asset {
    let coin: String
    let amount: Int
}

let assets = [
    Asset(coin: "BTC", amount: 12),
    Asset(coin: "ETH", amount: 15),
    Asset(coin: "BTC", amount: 30),
]
let grouped = assets.group(by: { $0.coin })

创造:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]

你能写一个使用示例吗? - Utku Dalmaz
@duan,是否有可能忽略大小写,例如BTC和btc应该被视为相同的内容... - Moin Shirazi
1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }),但最好的方法是映射后再分组。 - duan

4

你也可以通过 KeyPath 进行分组,像这样:

public extension Sequence {
    func group<Key>(by keyPath: KeyPath<Element, Key>) -> [Key: [Element]] where Key: Hashable {
        return Dictionary(grouping: self, by: {
            $0[keyPath: keyPath]
        })
    }
}

使用 @duan 的加密示例:

struct Asset {
    let coin: String
    let amount: Int
}

let assets = [
    Asset(coin: "BTC", amount: 12),
    Asset(coin: "ETH", amount: 15),
    Asset(coin: "BTC", amount: 30),
]

使用方法如下:

let grouped = assets.group(by: \.coin)

产生相同的结果:
[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]

你可以传递一个谓词而不是键路径 func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) } 这将允许你调用 assets.grouped(by: \.coin)assets.grouped { $0.coin } - Leo Dabus

2

Swift 4

struct Foo {
  let fizz: String
  let buzz: Int
}

let foos: [Foo] = [Foo(fizz: "a", buzz: 1), 
                   Foo(fizz: "b", buzz: 2), 
                   Foo(fizz: "a", buzz: 3),
                  ]
// use foos.lazy.map instead of foos.map to avoid allocating an
// intermediate Array. We assume the Dictionary simply needs the
// mapped values and not an actual Array
let foosByFizz: [String: Foo] = 
    Dictionary(foos.lazy.map({ ($0.fizz, $0)}, 
               uniquingKeysWith: { (lhs: Foo, rhs: Foo) in
                   // Arbitrary business logic to pick a Foo from
                   // two that have duplicate fizz-es
                   return lhs.buzz > rhs.buzz ? lhs : rhs
               })
// We don't need a uniquing closure for buzz because we know our buzzes are unique
let foosByBuzz: [String: Foo] = 
    Dictionary(uniqueKeysWithValues: foos.lazy.map({ ($0.buzz, $0)})

1

嘿,如果您需要在分组元素时保持顺序而不是使用哈希字典,我已经使用了元组并保持了列表的顺序进行分组。

extension Sequence
{
   func zmGroup<U : Hashable>(by: (Element) -> U) -> [(U,[Element])]
   {
       var groupCategorized: [(U,[Element])] = []
       for item in self {
           let groupKey = by(item)
           guard let index = groupCategorized.firstIndex(where: { $0.0 == groupKey }) else { groupCategorized.append((groupKey, [item])); continue }
           groupCategorized[index].1.append(item)
       }
       return groupCategorized
   }
}

1
你可以使用来自https://github.com/apple/swift-collectionsOrderedDictionary来创建带有有序键的字典。
import OrderedCollections

OrderedDictionary(grouping: statEvents, by: \.name)

或者使用 Dictionary 创建具有无序键的字典。

Dictionary(grouping: statEvents, by: \.name)

1

在已接受的答案基础上进行扩展,以允许有序分组:

extension Sequence {
    func group<GroupingType: Hashable>(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] {
        var groups: [GroupingType: [Iterator.Element]] = [:]
        var groupsOrder: [GroupingType] = []
        forEach { element in
            let key = key(element)
            if case nil = groups[key]?.append(element) {
                groups[key] = [element]
                groupsOrder.append(key)
            }
        }
        return groupsOrder.map { groups[$0]! }
    }
}

然后它将适用于任何元组
let a = [(grouping: 10, content: "a"),
         (grouping: 20, content: "b"),
         (grouping: 10, content: "c")]
print(a.group { $0.grouping })

除了任何“struct”或“class”之外:
struct GroupInt {
    var grouping: Int
    var content: String
}
let b = [GroupInt(grouping: 10, content: "a"),
         GroupInt(grouping: 20, content: "b"),
         GroupInt(grouping: 10, content: "c")]
print(b.group { $0.grouping })

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