I have a large array and need to access it by a key (a lookup) so I need to create Dictionary. Is there a built in function in Swift 3.0 to do so, or do I need to write it myself?
First I will need it for a class with key "String" and later on maybe I will be able to write a template version for general purpose (all types of data and key).
Note for 2019. This is now simply built-in to Swift 5, uniqueKeysWithValues and similar calls.
Is that it (in Swift 4)?
let dict = Dictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Note:
As mentioned in the comment, using uniqueKeysWithValues would give a fatal error (Fatal error: Duplicate values for key: 'your_key':) if you have duplicated keys.
If you fear that may be your case, then you can use init(_:uniquingKeysWith:) e.g.
let pairsWithDuplicateKeys = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] // or `let pairsWithDuplicateKeys = array.map{ ($0.key, $0) }`
let firstValues = Dictionary(pairsWithDuplicateKeys, uniquingKeysWith: { (first, _) in first })
print(firstValues)
//prints ["a": 1, "b": 2]
let lastValues = Dictionary(pairsWithDuplicateKeys, uniquingKeysWith: { (_, last) in last })
print(lastValues)
//prints ["a": 3, "b": 4]
On Swift 4, you can achieve this by using Dictionary's grouping:by: initializer
For ex:
You have class named A
class A {
var name: String
init(name: String) {
self.name = name
}
// .
// .
// .
// other declations and implementions
}
Next, you have an array of objects of type A
let a1 = A(name: "Joy")
let a2 = A(name: "Ben")
let a3 = A(name: "Boy")
let a4 = A(name: "Toy")
let a5 = A(name: "Tim")
let array = [a1, a2, a3, a4, a5]
Let's say you want to create a Dictionary by grouping all the names by their first letter. You use Swifts Dictionary(grouping:by:) to achieve this
let dictionary = Dictionary(grouping: array, by: { $0.name.first! })
// this will give you a dictionary
// ["J": [a1], "B": [a2, a3], "T": [a4, a5]]
source
Note however that the resulting Dictionary "dictionary" is of type
[String : [A]]
it is not of type
[String : A]
as you may expect. (Use #uniqueKeysWithValues to achieve the latter.)
I think you're looking for something like this:
extension Array {
public func toDictionary<Key: Hashable>(with selectKey: (Element) -> Key) -> [Key:Element] {
var dict = [Key:Element]()
for element in self {
dict[selectKey(element)] = element
}
return dict
}
}
You can now do:
struct Person {
var name: String
var surname: String
var identifier: String
}
let arr = [Person(name: "John", surname: "Doe", identifier: "JOD"),
Person(name: "Jane", surname: "Doe", identifier: "JAD")]
let dict = arr.toDictionary { $0.identifier }
print(dict) // Result: ["JAD": Person(name: "Jane", surname: "Doe", identifier: "JAD"), "JOD": Person(name: "John", surname: "Doe", identifier: "JOD")]
If you'd like your code to be more general, you could even add this extension on Sequence instead of Array:
extension Sequence {
public func toDictionary<Key: Hashable>(with selectKey: (Iterator.Element) -> Key) -> [Key:Iterator.Element] {
var dict: [Key:Iterator.Element] = [:]
for element in self {
dict[selectKey(element)] = element
}
return dict
}
}
Do note, that this causes the Sequence to be iterated over and could have side effects in some cases.
As others already said, we need to understand which are the keys.
However I am trying to provide a solution to my interpretation of your question.
struct User {
let id: String
let firstName: String
let lastName: String
}
Here I am assuming that 2 users with the same id cannot exist
let users: [User] = ...
let dict = users.reduce([String:User]()) { (result, user) -> [String:User] in
var result = result
result[user.id] = user
return result
}
Now dict is a dictionary where the key is the user id and the value is the user value.
To access a user via its id you can now simply write
let user = dict["123"]
Update #1: General approach
Given an array of a given type Element, and a closure that determine the key of an Element, the following generic function will generate a Dictionary of type [Key:Element]
func createIndex<Key, Element>(elms:[Element], extractKey:(Element) -> Key) -> [Key:Element] where Key : Hashable {
return elms.reduce([Key:Element]()) { (dict, elm) -> [Key:Element] in
var dict = dict
dict[extractKey(elm)] = elm
return dict
}
}
Example
let users: [User] = [
User(id: "a0", firstName: "a1", lastName: "a2"),
User(id: "b0", firstName: "b1", lastName: "b2"),
User(id: "c0", firstName: "c1", lastName: "c2")
]
let dict = createIndex(elms: users) { $0.id }
// ["b0": {id "b0", firstName "b1", lastName "b2"}, "c0": {id "c0", firstName "c1", lastName "c2"}, "a0": {id "a0", firstName "a1", lastName "a2"}]
Update #2
As noted by Martin R the reduce will create a new dictionary for each iteration of the related closure. This could lead to huge memory consumption.
Here's another version of the createIndex function where the space requirement is O(n) where n is the length of elms.
func createIndex<Key, Element>(elms:[Element], extractKey:(Element) -> Key) -> [Key:Element] where Key : Hashable {
var dict = [Key:Element]()
for elm in elms {
dict[extractKey(elm)] = elm
}
return dict
}
let pills = ["12", "34", "45", "67"]
let kk = Dictionary(uniqueKeysWithValues: pills.map{ ($0, "number") })
["12": "number", "67": "number", "34": "number", "45": "number"]
swift5 swift4
The following converts an array to a dictionary.
let firstArray = [2,3,4,5,5]
let dict = Dictionary(firstArray.map { ($0, 1) } , uniquingKeysWith: +)
Swift 5
extension Array {
func toDictionary() -> [Int: Element] {
self.enumerated().reduce(into: [Int: Element]()) { $0[$1.offset] = $1.element }
}
}
This extension works for all sequences (including arrays) and lets you select both key and value:
extension Sequence {
public func toDictionary<K: Hashable, V>(_ selector: (Iterator.Element) throws -> (K, V)?) rethrows -> [K: V] {
var dict = [K: V]()
for element in self {
if let (key, value) = try selector(element) {
dict[key] = value
}
}
return dict
}
}
Example:
let nameLookup = persons.toDictionary{($0.name, $0)}
Just do it simply,
let items = URLComponents(string: "https://im.qq.com?q=13&id=23")!.queryItems!
var dic = [String: Any?]()
items.foreach {
dic[$0.name] = $0.value
}
reduce is not very suitable,
let dic: [String: Any?] = items.reduce([:]) { (result: [String: Any?], item: URLQueryItem) -> [String: Any?] in
var r = result
r[item.name] = item.value // will create an copy of result!!!!!!
return r
}
As i understand from you're question you would like to convert to Array to Dictionary.
In my case i create extension for the Array and keys for the dictionary will be indexes of the Array.
Example:
var intArray = [2, 3, 5, 3, 2, 1]
extension Array where Element: Any {
var toDictionary: [Int:Element] {
var dictionary: [Int:Element] = [:]
for (index, element) in enumerate() {
dictionary[index] = element
}
return dictionary
}
}
let dic = intArray.toDictionary
Compatible with Swift 5 Standard Library (Xcode 10.2+ , iOS 12.2).
Here's an example of usage of an initializer init(uniqueKeysWithValues:)
The input let array: [String] = Locale.isoRegionCodes is an array of ISO31661-2 codes represented by a string.
let countryCodeAndName: [String: String] = Dictionary(uniqueKeysWithValues: Locale.isoRegionCodes.map { ($0, Locale.current.localizedString(forRegionCode: $0) ?? "")} )
Returned dictionary, will list all regions with ISO31661-2 code as a key and a localized region name as a value.
Output:
...
"PL":"Poland"
"DE":"Germany"
"FR":"France"
"ES":"Spain"
...
Example 2:
let dictionary: [String: String] = Dictionary(uniqueKeysWithValues: [ ("key1", "value1"), ("key2", "value2")] )
Output:
["key1": "value1", "key2": "value2"]
Important:
Precondition: The sequence must not have duplicate keys.
Code below will crash an app:
let digitWords = ["one", "two", "three", "four", "five", "five"]
let wordToValue = Dictionary(uniqueKeysWithValues: zip(digitWords, 1...6))
with:
Fatal error: Duplicate values for key: 'five'
If you want to follow the pattern set out by map and reduce in swift you could do something nice and functional like this:
extension Array {
func keyBy<Key: Hashable>(_ keyFor: (Element) -> Key) -> [Key: Element] {
var ret = [Key: Element]()
for item in self{
ret[keyFor(item)] = item
}
return ret
}
}
Usage:
struct Dog {
let id: Int
}
let dogs = [Dog(id: 1), Dog(id: 2), Dog(id: 3), Dog(id: 4)]
let dogsById = dogs.keyBy({ $0.id })
// [4: Dog(id: 4), 1: Dog(id: 1), 3: Dog(id: 3), 2: Dog(id: 2)]
Swift way:
extension Sequence {
func toDictionary<Key: Hashable>(with selectKey: (Element) -> Key) -> [Key: Element] {
reduce(into: [:]) { $0[selectKey($1)] = $1 }
}
}
// let arr = [Person(id: 1, name: "Alan")]
// arr.toDictionary { $0.id }
// ==
// [1: Person(id: 1, name: "Alan")]
Related
I have a method which is supposed to return a Set of Strings. Here is a method description:
Returns: 10 product names containing the specified string.
If there are several products with the same name, producer's name is added to product's name in the format "<producer> - <product>",
otherwise returns simply "<product>".
Can't figure out how to check if there are duplicate names in the array and then edit them as required
What I've got so far:
struct Product {
let id: String; // unique identifier
let name: String;
let producer: String;
}
protocol Shop {
func addNewProduct(product: Product) -> Bool
func deleteProduct(id: String) -> Bool
func listProductsByName(searchString: String) -> Set<String>
func listProductsByProducer(searchString: String) -> [String]
}
class ShopImpl: Shop {
private var goodsInTheShopDictionary: [String: Product] = [:]
func addNewProduct(product: Product) -> Bool {
let result = goodsInTheShopDictionary[product.id] == nil
if result {
goodsInTheShopDictionary[product.id] = product
}
return result
}
func deleteProduct(id: String) -> Bool {
let result = goodsInTheShopDictionary[id] != nil
if result {
goodsInTheShopDictionary.removeValue(forKey: id)
}
return result
}
func listProductsByName(searchString: String) -> Set<String> {
var result = Set<String>()
let searchedItems = goodsInTheShopDictionary.filter{ $0.value.name.contains(searchString) }
let resultArray = searchedItems.map{ $0.value }
result = Set(searchedItems.map{ $0.value.name })
if result.count > 10 {
result.removeFirst()
}
return result
}
}
If you want to achieve this you would need to iterate over you resultArray and save producer and product into another array. On each iteration you would need to check if the array allready contains either the product name itself or an allready modified version.
A possible implementation would look like this:
var result = [(producer: String, product: String)]()
// iterate over the first 10 results
for item in resultArray.prefix(10){
if let index = result.firstIndex(where: { _ , product in
product == item.name
}){
// the result array allready contains the exact product name
// so we need to convert the name allready in the list
let oldProduct = (producer: result[index].producer, product: "\(result[index].producer) \(result[index].product)")
result[index] = oldProduct
// add the new one
result.append((producer: item.producer, product: "\(item.producer) \(item.name)"))
}
else if !result.filter({ $0.product.components(separatedBy: " ").contains(item.name)}).isEmpty {
// if the result array allready contains a modified version of the name
result.append((producer: item.producer, product: "\(item.producer) \(item.name)"))
} else{
// if the result array does not contain the product yet
result.append((producer: item.producer, product: "\(item.name)"))
}
}
let productNames = result.map{ $0.product}
Please be aware: As you are using a [String: Product], which is a unsorted dictionary, to hold your values this will yield different results (if the resultArray collection is larger than 10) each time you search.
Tested with searchString = name1:
var goodsInTheShopDictionary: [String: Product] = Dictionary(uniqueKeysWithValues: (0...20).map { index in
("\(index)",Product(id: "", name: "name\(index)", producer: "producer\(index)"))
})
goodsInTheShopDictionary["100"] = Product(id: "11", name: "name1", producer: "producer11")
goodsInTheShopDictionary["101"] = Product(id: "12", name: "name1", producer: "producer12")
Result:
["name13", "producer12 name1", "name10", "name19", "producer11 name1",
"name17", "name14", "name18", "producer1 name1", "name16"]
I want to subtract array1 by array2
Example:
var array1 = ["the", "people", "prefer", "to", "go", "to", "the","sun","beach"]
var array2 = ["the", "people", "prefer", "go", "to", "the", "moon","beach"]
I want Output:
["to","sun"]
What I am trying so far:
let reuslt = array1.filter { ! array2.contains($0) }
Output:
["sun"]
it's checking to contain a matching item removing all items if it matches but I want to remove one for one.
Just do it on the computer the way you would do it in your brain. Loop through array2 (not array1). For each element of array2, if that element has a firstIndex in array1, remove the element at that index from array1.
for word in array2 {
if let index = array1.firstIndex(of: word) {
array1.remove(at: index)
}
}
What you effectively want to do is this for loop
for item2 in array2 {
for i in 0..<array1.count {
if item2 == array1[i] {
array1.remove(at: i)
break
}
}
}
Filter works in exactly this way except it doesn't break on the first item but continues to remove all items.
You can also put this into a one liner like this with map instead of filter: array2.map({guard let i = array1.firstIndex(of: $0) else {return}; array1.remove(at: i)})
Like above answers, it is ok for a loop contains findIndex and remove from array.
But in other world, I think if the array is too large, the complexity of firstIndex(of:) and remove(at:) cause time and CPU too much for this task - Heat of device can raise a lots too. You can minimize it by using dictionary.
This is an another approach:
func findTheDiff(_ compareArr: [String], _ desArr: [String]) -> [String] {
var resultArr : [String] = []
var dict : [String: Int] = [:]
for word in compareArr {
if dict[word] == nil {
dict[word] = 1
} else {
dict[word] = dict[word]! + 1
}
}
for checkWord in desArr {
if dict[checkWord] != nil && dict[checkWord]! > 0 {
dict[checkWord] = dict[checkWord]! - 1
continue
}
resultArr.append(checkWord)
}
return resultArr
}
Usage:
var array1 = ["the", "people", "prefer", "to", "go", "to", "the","sun","beach"]
var array2 = ["the", "people", "prefer", "go", "to", "the", "moon","beach"]
var result = self.findTheDiff(array2, array1)
print(result) // ["to", "sun"]
You can find the complexity of firstIndex, remove(at:) below:
https://developer.apple.com/documentation/swift/array/firstindex(of:)
https://developer.apple.com/documentation/swift/array/remove(at:)-1p2pj
#Thang Phi's answer has the right idea. This is not different, but it works with a level of abstraction that incorporates the "counted set" idea for which Swift doesn't yet provide a built-in type:
import OrderedCollections
public extension Sequence where Element: Hashable {
/// A version of this sequence without the earliest occurrences of all `elementsToRemove`.
///
/// If `elementsToRemove` contains multiple equivalent values,
/// that many of the earliest occurrences will be filtered out.
func filteringOutEarliestOccurrences(from elementsToRemove: some Sequence<Element>) -> some Sequence<Element> {
var elementCounts = Dictionary(bucketing: elementsToRemove)
return lazy.filter {
do {
try elementCounts.remove(countedSetElement: $0)
return false
} catch {
return true
}
}
}
}
public extension Dictionary where Value == Int {
/// Create "buckets" from a sequence of keys,
/// such as might be used for a histogram.
/// - Note: This can be used for a "counted set".
#inlinable init(bucketing unbucketedKeys: some Sequence<Key>) {
self.init(zip(unbucketedKeys, 1), uniquingKeysWith: +)
}
/// Treating this dictionary as a "counted set", reduce the element's value by 1.
/// - Throws: If `countedSetElement` is not a key.
#inlinable mutating func remove(countedSetElement: Key) throws {
guard let count = self[countedSetElement] else { throw AnyError() }
self[countedSetElement] = count == 1 ? nil : count - 1
}
}
/// `zip` a sequence with a single value, instead of another sequence.
#inlinable public func zip<Sequence: Swift.Sequence, Constant>(
_ sequence: Sequence, _ constant: Constant
) -> some Swift.Sequence<(Sequence.Element, Constant)> {
zip(sequence, **ModuleName**.sequence(constant))
}
/// An infinite sequence of a single value.
#inlinable public func sequence<Element>(_ element: Element) -> some Sequence<Element> {
let getSelf: (Element) -> Element = \.self
return sequence(first: element, next: getSelf)
}
/// A nondescript error.
public struct AnyError: Error & Equatable {
public init() { }
}
Your example seems a little confusing / incomplete on the output. But sounds like you could do something like this:
extension Array where Element: Hashable {
func difference(from other: [Element]) -> [Element] {
let thisSet = Set(self)
let otherSet = Set(other)
return Array(thisSet.symmetricDifference(otherSet))
}
}
let names1 = ["the", "people", "prefer", "to", "go", "to", "the","sun","beach"]
let names2 = ["the", "people", "prefer", "go", "to", "the", "moon","beach"]
let difference = names1.difference(from: names2)
print(Array(difference)) // ["sun", "moon"]
Using an extension will of course make this code available to all of your arrays in your project. Since we convert the arrays into Sets, duplicates are removed, which may be a issue in your use case.
This Array extension was taken form: https://www.hackingwithswift.com/example-code/language/how-to-find-the-difference-between-two-arrays A vital resoruce for all things swift, especially SwiftUI.
Simple subtraction method that works for Equatable types including String.
extension Array where Element: Equatable {
func subtracting(_ array: Array<Element>) -> Array<Element> {
var result: Array<Element> = []
var toSub = array
for i in self {
if let index = toSub.firstIndex(of: i) {
toSub.remove(at: index)
continue
}
else {
result.append(i)
}
}
return result
}
}
let first = [1, 1, 2, 3, 3, 5, 6, 7, 7]
let second = [2, 2, 3, 4, 4, 5, 5, 6]
let result = first.subtracting(second)
//print result
//[1, 1, 3, 7, 7]
I just want convert an array of Player Names into a dictionary Scoreboard, giving everyone an initial score of 0.
Meaning...
var playerNames = ["Harry", "Ron", "Hermione"]
becomes...
var scoreBoard: [String:Int] = [ "Ron":0, "Harry":0, "Hermione":0 ]
This is my first time asking a question, but I’m totally completely stuck on what feels so simple and all how-to's/questions I've found are off in some way. I have tried using reduce in a variety of ways, but always end up short. Thanks in advance!
Here's a quick one liner that I like to use:
let scoreboard = playerNames.reduce(into: [String: Int]()) { $0[$1] = 0 }
reduce is definitely one of the more difficult builtin functions to use correctly, but it is what you want here.
let names = ["Harry", "Ron", "Hermione"]
let scoreboard: [String: Int] = names.reduce(into: [:], { result, next in
result[next] = 0
})
It takes 2 parameters: the initial value (in our case, an empty dictionary [:]), and a closure that updates the result with each element in the array. This closure has 2 parameters, result and next. We want to update result based on the next element. Our closure does this by setting result[next] to 0.
If the player names are known to be all different then you can do
let playerNames = ["Harry", "Ron", "Hermione", "Ron"]
var scoreBoard = Dictionary(uniqueKeysWithValues: zip(playerNames,
repeatElement(0, count: playerNames.count)))
print(scoreBoard) // ["Harry": 0, "Ron": 0, "Hermione": 0]
Here zip is used to create a sequence of player/score pairs, from which the dictionary is created.
Remark: Originally I had used AnySequence { 0 } to generate the zeros. Using repeatElement() instead was suggested by Alexander and has the advantage that the correct required capacity is passed to the dictionary intializer.
You can use reduce(into:) as you suspected. You simply need to declare the initial value as [String:Int]() to be an empty Dictionary of the required type, then simply set the value of all keys in playerNames to 0.
var playerNames = ["Harry", "Ron", "Hermione"]
var scoreBoard = playerNames.reduce(into: [String:Int](), { currentScores,player in
currentScores[player] = 0
})
Using reduce(into:_:):
var playerNames = ["Harry", "Ron", "Hermione"]
let playerScore = playerNames.reduce(into: [:]) { counts, playerNames in
counts[playerNames, default: 0] += 0
}
print(playerScore)
To keep a count of the players names (eg. duplicate names):
counts[myArray, default: 0] += 1
So for example if Ron had two entries before the game started (score > 0) then you would know.
Without using reduce(into:_:) method and as an extension:
var playerNames = ["Harry", "Ron", "Hermione"]
extension Sequence where Self.Iterator.Element: Hashable {
func freq() -> [Self.Iterator.Element: Int] {
return reduce([:]) {
( iter: [Self.Iterator.Element: Int], element) in
var dict = iter
dict[element] = 0
return dict
}
}
}
print(playerNames.freq())
// ["Harry": 0, "Hermione": 0, "Ron": 0]
keep a count (eg. duplicate names):
dict[element, default: -1 ] += 1
Here is how you can do that:
var playerNames = ["Harry", "Ron", "Hermione"]
var dictionary = [String: Int]()
for player in playerNames {
dictionary[player] = 0
}
Here is another way to do it:
// Implementation
extension Dictionary {
static func from(_ array: [Value], key: KeyPath<Value, Key>) -> Dictionary<Key, Value> {
var dict: Dictionary<Key, Value> = [:]
array.forEach { dict[$0[keyPath: key]] = $0}
return dict
}
}
/// Usage
let array: [String] = ["Banana", "Apple"]
Dictionary.from(array, key: \.self)
// or if you have a more complex object
struct Foo {
let id: Int
}
let array2: [Foo] = [Foo(id: 1), Foo(id: 2)]
Dictionary.from(array2, key: \.id)
Based on jmad8 answer
Details
Swift 5.3
Xcode 12.0.1 (12A7300)
Solution
extension Sequence {
func toDictionary<Key: Hashable, Value>(where closure: (Element) -> (Key, Value)) -> [Key: Value] {
reduce(into: [Key: Value]()) { (result, element) in
let components = closure(element)
result[components.0] = components.1
}
}
func toCompactDictionary<Key: Hashable, Value>(where closure: (Element) -> ((Key, Value)?)) -> [Key: Value] {
reduce(into: [Key: Value]()) { (result, element) in
guard let components = closure(element) else { return }
result[components.0] = components.1
}
}
}
Usage
// Sample 1
print(languages.toDictionary { (string) -> (Character, String) in
return (string.first!, string)
})
print(languages.toCompactDictionary { (string) -> (Character, String)? in
guard let character = string.first, character != Character("J") else { return nil }
return (character, string)
})
// Sample 2
print(languages.enumerated().toDictionary { (data) -> (Int, String) in
return (data.offset, data.element)
})
// Shorter version of sample 2
print(languages.enumerated().toDictionary { ($0.offset, $0.element) })
// Sample 3
struct Order {
let id: Int
let desctiption: String
}
let orders = [
Order(id: 0, desctiption: "Apple"),
Order(id: 1, desctiption: "Banana"),
Order(id: 2, desctiption: "watermelon")
]
print(orders.toDictionary { ($0.id, $0) })
I have an array of objects with property date.
What I want is to create array of arrays where each array will contain objects with the same date.
I understand, that I need something like .filter to filter objects, and then .map to add every thing to array.
But how to tell .map that I want separate array for each group from filtered objects and that this array must be added to "global" array and how to tell .filter that I want objects with the same date ?
It might be late but new Xcode 9 sdk dictionary has new init method
init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence
Documentation has simple example what this method does.
I just post this example below:
let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let studentsByLetter = Dictionary(grouping: students, by: { $0.first! })
Result will be:
["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]
improving on oriyentel solution to allow ordered grouping on anything:
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]! }
}
}
Then it will work on any tuple, struct or class and for any property:
let a = [(grouping: 10, content: "a"),
(grouping: 20, content: "b"),
(grouping: 10, content: "c")]
print(a.group { $0.grouping })
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 })
With Swift 5, you can group the elements of an array by one of their properties into a dictionary using Dictionary's init(grouping:by:) initializer. Once done, you can create an array of arrays from the dictionary using Dictionary's values property and Array init(_:) initializer.
The following Playground sample code shows how to group the elements of an array by one property into a new array of arrays:
import Foundation
struct Purchase: CustomStringConvertible {
let id: Int
let date: Date
var description: String {
return "Purchase #\(id) (\(date))"
}
}
let date1 = Calendar.current.date(from: DateComponents(year: 2010, month: 11, day: 22))!
let date2 = Calendar.current.date(from: DateComponents(year: 2015, month: 5, day: 1))!
let date3 = Calendar.current.date(from: DateComponents(year: 2012, month: 8, day: 15))!
let purchases = [
Purchase(id: 1, date: date1),
Purchase(id: 2, date: date1),
Purchase(id: 3, date: date2),
Purchase(id: 4, date: date3),
Purchase(id: 5, date: date3)
]
let groupingDictionary = Dictionary(grouping: purchases, by: { $0.date })
print(groupingDictionary)
/*
[
2012-08-14 22:00:00 +0000: [Purchase #4 (2012-08-14 22:00:00 +0000), Purchase #5 (2012-08-14 22:00:00 +0000)],
2010-11-21 23:00:00 +0000: [Purchase #1 (2010-11-21 23:00:00 +0000), Purchase #2 (2010-11-21 23:00:00 +0000)],
2015-04-30 22:00:00 +0000: [Purchase #3 (2015-04-30 22:00:00 +0000)]
]
*/
let groupingArray = Array(groupingDictionary.values)
print(groupingArray)
/*
[
[Purchase #3 (2015-04-30 22:00:00 +0000)],
[Purchase #4 (2012-08-14 22:00:00 +0000), Purchase #5 (2012-08-14 22:00:00 +0000)],
[Purchase #1 (2010-11-21 23:00:00 +0000), Purchase #2 (2010-11-21 23:00:00 +0000)]
]
*/
Abstracting one step, what you want is to group elements of an array by a certain property. You can let a map do the grouping for you like so:
protocol Groupable {
associatedtype GroupingType: Hashable
var grouping: GroupingType { get set }
}
extension Array where Element: Groupable {
typealias GroupingType = Element.GroupingType
func grouped() -> [[Element]] {
var groups = [GroupingType: [Element]]()
for element in self {
if let _ = groups[element.grouping] {
groups[element.grouping]!.append(element)
} else {
groups[element.grouping] = [element]
}
}
return Array<[Element]>(groups.values)
}
}
Note that this grouping is stable, that is groups appear in order of appearance, and inside the groups the individual elements appear in the same order as in the original array.
Usage Example
I'll give an example using integers; it should be clear how to use any (hashable) type for T, including Date.
struct GroupInt: Groupable {
typealias GroupingType = Int
var grouping: Int
var content: String
}
var a = [GroupInt(grouping: 1, content: "a"),
GroupInt(grouping: 2, content: "b") ,
GroupInt(grouping: 1, content: "c")]
print(a.grouped())
// > [[GroupInt(grouping: 2, content: "b")], [GroupInt(grouping: 1, content: "a"), GroupInt(grouping: 1, content: "c")]]
Rapheal's solution does work. However, I would propose altering the solution to support the claim that the grouping is in fact stable.
As it stands now, calling grouped() will return a grouped array but subsequent calls could return an array with groups in a different order, albeit the elements of each group will be in the expected order.
internal protocol Groupable {
associatedtype GroupingType : Hashable
var groupingKey : GroupingType? { get }
}
extension Array where Element : Groupable {
typealias GroupingType = Element.GroupingType
func grouped(nilsAsSingleGroup: Bool = false) -> [[Element]] {
var groups = [Int : [Element]]()
var groupsOrder = [Int]()
let nilGroupingKey = UUID().uuidString.hashValue
var nilGroup = [Element]()
for element in self {
// If it has a grouping key then use it. Otherwise, conditionally make one based on if nils get put in the same bucket or not
var groupingKey = element.groupingKey?.hashValue ?? UUID().uuidString.hashValue
if nilsAsSingleGroup, element.groupingKey == nil { groupingKey = nilGroupingKey }
// Group nils together
if nilsAsSingleGroup, element.groupingKey == nil {
nilGroup.append(element)
continue
}
// Place the element in the right bucket
if let _ = groups[groupingKey] {
groups[groupingKey]!.append(element)
} else {
// New key, track it
groups[groupingKey] = [element]
groupsOrder.append(groupingKey)
}
}
// Build our array of arrays from the dictionary of buckets
var grouped = groupsOrder.flatMap{ groups[$0] }
if nilsAsSingleGroup, !nilGroup.isEmpty { grouped.append(nilGroup) }
return grouped
}
}
Now that we track the order that we discover new groupings, we can return a grouped array more consistently than just relying on a Dictionary's unordered values property.
struct GroupableInt: Groupable {
typealias GroupingType = Int
var grouping: Int?
var content: String
}
var a = [GroupableInt(groupingKey: 1, value: "test1"),
GroupableInt(groupingKey: 2, value: "test2"),
GroupableInt(groupingKey: 2, value: "test3"),
GroupableInt(groupingKey: nil, value: "test4"),
GroupableInt(groupingKey: 3, value: "test5"),
GroupableInt(groupingKey: 3, value: "test6"),
GroupableInt(groupingKey: nil, value: "test7")]
print(a.grouped())
// > [[GroupableInt(groupingKey: 1, value: "test1")], [GroupableInt(groupingKey: 2, value: "test2"),GroupableInt(groupingKey: 2, value: "test3")], [GroupableInt(groupingKey: nil, value: "test4")],[GroupableInt(groupingKey: 3, value: "test5"),GroupableInt(groupingKey: 3, value: "test6")],[GroupableInt(groupingKey: nil, value: "test7")]]
print(a.grouped(nilsAsSingleGroup: true))
// > [[GroupableInt(groupingKey: 1, value: "test1")], [GroupableInt(groupingKey: 2, value: "test2"),GroupableInt(groupingKey: 2, value: "test3")], [GroupableInt(groupingKey: nil, value: "test4"),GroupableInt(groupingKey: nil, value: "test7")],[GroupableInt(groupingKey: 3, value: "test5"),GroupableInt(groupingKey: 3, value: "test6")]]
+1 to GolenKovkosty answer.
init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence
More Examples:
enum Parity {
case even, odd
init(_ value: Int) {
self = value % 2 == 0 ? .even : .odd
}
}
let parity = Dictionary(grouping: 0 ..< 10 , by: Parity.init )
Equilvalent to
let parity2 = Dictionary(grouping: 0 ..< 10) { $0 % 2 }
In your case:
struct Person : CustomStringConvertible {
let dateOfBirth : Date
let name :String
var description: String {
return "\(name)"
}
}
extension Date {
init(dateString:String) {
let formatter = DateFormatter()
formatter.timeZone = NSTimeZone.default
formatter.dateFormat = "MM/dd/yyyy"
self = formatter.date(from: dateString)!
}
}
let people = [Person(dateOfBirth:Date(dateString:"01/01/2017"),name:"Foo"),
Person(dateOfBirth:Date(dateString:"01/01/2017"),name:"Bar"),
Person(dateOfBirth:Date(dateString:"02/01/2017"),name:"FooBar")]
let parityFields = Dictionary(grouping: people) {$0.dateOfBirth}
Output:
[2017-01-01: [Foo, Bar], 2017-02-01: [FooBar] ]
This is a clean way to perform group by:
let grouped = allRows.group(by: {$0.groupId}) // Dictionary with the key groupId
Assuming you have array of contacts like :
class ContactPerson {
var groupId:String?
var name:String?
var contactRecords:[PhoneBookEntry] = []
}
To achieve this, add this extension:
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
}
}
ExSwift has an Array extension:
/**
Converts the array to a dictionary with keys and values supplied via the transform function.
:param: transform
:returns: A dictionary
*/
func toDictionary <K, V> (transform: (Element) -> (key: K, value: V)?) -> [K: V] {
var result: [K: V] = [:]
for item in self {
if let entry = transform(item) {
result[entry.key] = entry.value
}
}
return result
}
The ExSwift Wiki however only shows an example for the other toDictionary() method signature. I'm not yet very familiar with the way how these method signatures work. So I was wondering whether somebody can show me an example of how to use the above method call?
You need to provide a closure which takes as parameter element of the array and returns key - value pair.
Say you have an array of keys, and want to create a dictionary with default value for each key (let's say 0):
let keys = ["a", "b", "c"]
let dictionary = keys.toDictionary { element in
return (element, 0)
}
Following the already mentioned sample from the wiki, we could guess the use of the second toDictionary() method:
class Person {
let name: String, age: Int, id: String
init(_ name: String, _ age: Int, _ id: String){
self.name = name
self.age = age
self.id = id
}
}
let people = [
Person("bob", 25, "P1"),
Person("frank", 45, "P2"),
Person("ian", 35, "P3")
]
let dictionary = people.toDictionary { $0.name, $0.age }
// → ["bob": 25, "frank": 45, "ian": 35]