Swift - Group an array of items by week of year - arrays

I'm currently trying to manipulate an array of objects that takes a date, double, and string values for the whole year. Im trying to group/add all items that share the same week and name. Currently it doesn't do that, it just adds up all the qty of each item for the week and ends up printing that output twice.
I've tried using multiple .mapvalues and got close to what my goal, but I'm confused how to add/group together items that share the same week and name for each location.
Current OutPut:
["Location1": ["Location1": [1: 12.0], "Location#2": [3: 16.0]], "Location2": ["Location1": [1: 12.0], "Location#2": [3: 16.0]]]
Goal:
["Location1": [1: ["Apple": 5, "Pear": 7]], "Location2": [2: ["Apple": 7, "Pear": 9]]
//["Location": weekOfYear: ["itemName": qtySold]]
import Foundation
let cal = Calendar.current
extension Date {
var week: Int {
return cal.component(.weekOfYear, from: self)
}
var weekAndYear: DateComponents {
return cal.dateComponents([.weekOfYear, .yearForWeekOfYear], from: self)
}
var yearForWeekOfYear: Int {
return cal.component(.yearForWeekOfYear, from: self)
}
}
struct sortedDates {
var date: Date
var itemName: String
var itemSold: Double
}
//Current Dates in String Form
let date1 = "\(2023)-\(01)-\(04)"
let date2 = "\(2023)-\(01)-\(05)"
let date3 = "\(2023)-\(01)-\(06)"
let date4 = "\(2023)-\(01)-\(17)"
let date5 = "\(2023)-\(01)-\(18)"
let dateFormatter = DateFormatter()
//Dates in String Form converted to Date
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateForm1 = dateFormatter.date(from: date1)
let dateForm2 = dateFormatter.date(from: date2)
let dateForm3 = dateFormatter.date(from: date3)
let dateForm4 = dateFormatter.date(from: date4)
let dateForm5 = dateFormatter.date(from: date5)
//locations and Items
let allLocations = ["Location1", "Location2"]
let allItems = ["Apple", "Pear"]
//List of Data
let allInfo: [String: [sortedDates]] = ["Location1": [sortedDates(date: dateForm1 ?? Date(), itemName: "Apple", itemSold: 3.0), sortedDates(date: dateForm2 ?? Date(), itemName: "Pear", itemSold: 7), sortedDates(date: dateForm3 ?? Date(), itemName: "Apple", itemSold: 2)], "Location#2": [sortedDates(date: dateForm4 ?? Date(), itemName: "Apple", itemSold: 7), sortedDates(date: dateForm5 ?? Date(), itemName: "Pear", itemSold: 9)]]
//Dict in which final results will be held.
var tempDict: [String: Any] = [:]
//Loop through each location and groups items together
for curLocation in allLocations {
let grouppedByYearThenWeek = allInfo
.mapValues{(yearArray: [sortedDates]) -> Dictionary<Int, Double> in
return Dictionary(grouping: yearArray, by: { $0.date.week })
.mapValues{ (value: [sortedDates]) in
return value.map{ $0.itemSold }.reduce(0,+)
}
}
if tempDict[curLocation] == nil {
tempDict[curLocation] = []
}
tempDict[curLocation] = grouppedByYearThenWeek
}
print(tempDict)

I solved this by breaking it down into two problems to solve and then combined the solutions. First we have the arrays in the starting dictionary allInfo, for instance
let array = [SortedDate(date: dateForm4, itemName: "Apple", itemSold: 7), SortedDate(date: dateForm5, itemName: "Pear", itemSold: 9)]
We can sum the values per name using reduce(into:) and group by week by using Dictionary(grouping:by:)
Dictionary(grouping: array, by: \.date.week).mapValues { $0.reduce(into: [:]) { $0[$1.itemName, default: 0] += $1.itemSold} }
This would give us the correct result for that particular value in the array
[3: ["Apple": 7.0, "Pear": 9.0]]
Now we only need to apply this to each value in the dictionary
var result: [String: [Int: [String: Double]]] = [:]
allInfo.forEach { (key, value) in
result[key] = Dictionary(grouping: value, by: \.date.week).mapValues { values in
values.reduce(into: [:]) { $0[$1.itemName, default: 0] += $1.itemSold }
}
}
["Location1": [1: ["Apple": 5.0, "Pear": 7.0]], "Location2": [3: ["Apple": 7.0, "Pear": 9.0]]]

Related

compare multiple arrays for same elements in swift

I'm new to swift and programming in general. I have multiple arrays of names in a database and I need to check for same names in these arrays.
I've found some solutions to compare two arrays, but not multiple so I wrote some additional code.
But performance wise it's not the best practice I think. And also not the best way to add first all the names and then remove the duplicates..
Does anyone has any better ideas/solutions for my problem?
Code:
import UIKit
let array1 = ["Max", "Peter","Kathrin", "Sara", "Kirsten", "Mike", "Elon"] // Peter, Kathrin, Mike, Sara
let array2 = ["Pamela", "Chris", "James", "Sebastian", "Mike"] // Mike, Chris
let array3 = ["John", "Daniel", "Susan", "Mathias", "Mike", "Donald"] // Mike
let array4 = ["Tim", "Kathrin", "Alan", "Chris", "Amy", "Sara"] // Kathrin, Chris
let array5 = ["Cara", "Charly", "Emily", "Maja", "Peter", "Sara"] // Peter, Sara
// Output should be: Peter, Kathrin, Mike, Sara, Chris
var array = [Array<String>]()
array.append(array1)
array.append(array2)
array.append(array3)
array.append(array4)
array.append(array5)
var names = [String]()
for i in 0...array.count - 2 {
for z in 1...array.count - 1 {
if z + i < array.count {
let commonElements = Array(Set(array[i]).intersection(Set(array[z+i])))
names.append(contentsOf: commonElements)
}
}
}
print(names.removeDuplicates())
Extension:
extension Array where Element: Hashable {
func removeDuplicates() -> [Element] {
var result = [Element]()
for value in self {
if result.contains(value) == false {
result.append(value)
}
}
return result
}
}
If your intent is to just check if a name occurs in more than one collection I think the best way to approach this is creating a single collection with all the names and filter the duplicates as shown in this post
let array1 = ["Max", "Peter","Kathrin", "Sara", "Kirsten", "Mike", "Elon"]
let array2 = ["Pamela", "Chris", "James", "Sebastian", "Mike"]
let array3 = ["John", "Daniel", "Susan", "Mathias", "Mike", "Donald"]
let array4 = ["Tim", "Kathrin", "Alan", "Chris", "Amy", "Sara"]
let array5 = ["Cara", "Charly", "Emily", "Maja", "Peter", "Sara"]
var names: [String] = []
names.append(contentsOf: array1)
names.append(contentsOf: array2)
names.append(contentsOf: array3)
names.append(contentsOf: array4)
names.append(contentsOf: array5)
extension RangeReplaceableCollection where Element: Hashable {
var duplicates: Self {
var set: Set<Element> = []
var filtered: Set<Element> = []
return filter { !set.insert($0).inserted && filtered.insert($0).inserted }
}
}
// Output should be: Peter, Kathrin, Mike, Sara, Chris
print(names.duplicates) // ["Mike", "Kathrin", "Chris", "Sara", "Peter"]

Swift: Remove repeated Index and create a new dictionary with reversed index and value

I have a dictionary with two Int’s A:B, and I want create a new dictionary that includes B as an index (with no repetition) and A as a value (only for repeated B’s):
var myDict : [Int:Int] = [12:2345, 14:2345, 99:1111, 67:1111, 77:7657, 132:3345, 199:6778]
Desired output:
var newDict: [Int:[Int]] = [2345: [ 12 , 14 ] , 1111: [ 99 , 67] ]
Note: the original dictionary contains over a thousand entries.
You loop through the first dict by enumerating it, that way you can switch the values in to the new dict
var newDict: [Int:[Int]] = [:]
let myDict : [Int:Int] = [12:2345, 14:2345, 99:1111, 67:1111, 77:7657, 132:3345, 199:6778]
for values in myDict.enumerated() {
var newValue = newDict[values.element.value] ?? []
newValue.append(values.element.key)
newDict[values.element.value] = newValue
}
newDict = newDict.filter { (key, value) -> Bool in
value.count > 1
}
Here is the power of swift:
let newDict = Dictionary(grouping: myDict, by: {$0.value}).filter({$0.value.count > 1}).mapValues({$0.map({$0.key})})
print(newDict)
Output: [1111: [67, 99], 2345: [12, 14]]
Please use this code:
var myDict: [Int:Int] = [12:2345, 14:2345, 99:1111, 67:1111, 77:7657, 132:3345, 199:6778]
let values = myDict.values
let tempValueSet = Set<Int>(values)
let uniqueValues = Array(tempValueSet)
var result = [Int: [Int]]()
for item in uniqueValues {
result[item] = myDict.allKeys(forValue: item)
}
print(result)
And this is Dictionary extension:
extension Dictionary where Value: Equatable {
func allKeys(forValue val: Value) -> [Key] {
return self.filter { $1 == val }.map { $0.0 }
}
}
Desired output:
[6778: [199], 1111: [99, 67], 7657: [77], 3345: [132], 2345: [12, 14]]
For more reference about this extension : https://stackoverflow.com/a/27218964/2284065
If you don't want to use extension then you can use this code too :
var myDict: [Int:Int] = [12:2345, 14:2345, 99:1111, 67:1111, 77:7657, 132:3345, 199:6778]
let values = myDict.values
let tempValueSet = Set<Int>(values)
let uniqueValues = Array(tempValueSet)
var result = [Int: [Int]]()
for item in uniqueValues {
result[item] = (myDict as NSDictionary).allKeys(for: item) as! [Int]
}
print(result)

How to group array data based on Date item into a dictionary

I have an array of Item objects:
struct Item {
let date: Date
let amount: Double
}
How can I group this items into a dictionary with Date as key and sum of amount for that date in Swift 3 ?
Here is an example:
let data = [Item(date:"2017.02.15", amount: 25),
Item(date:"2017.02.14", amount: 50),
Item(date:"2017.02.11", amount: 35),
Item(date:"2017.02.15", amount: 15)]
Result should be:
["2017.02.15": 40, "2017.02.14": 50, "2017.02.11": 35]
Given we fix your example up somewhat (using String as Date example), you could simply pass over the elements in data and add to (and optionally increase) the corresponding key-value pair in the dict.
struct Item {
let date: String
let amount: Double
}
let data = [Item(date: "2017.02.15", amount: 25),
Item(date: "2017.02.14", amount: 50),
Item(date: "2017.02.11", amount: 35),
Item(date: "2017.02.15", amount: 15)]
var dict: [String: Double] = [:]
for item in data {
dict[item.date] = (dict[item.date] ?? 0) + item.amount
}
print(dict) // ["2017.02.14": 50.0, "2017.02.15": 40.0, "2017.02.11": 35.0]
With Swift 4, according to your needs, you may choose one of the following solutions in order to solve your problem.
#1 Using Dictionary subscript(_:default:)
Dictionay has a subscript called subscript(_:default:). subscript(_:default:) has the following declaration:
subscript(key: Dictionary.Key, default defaultValue: #autoclosure () -> Dictionary.Value) -> Dictionary.Value { get set }
Accesses the element with the given key, or the specified default value, if the dictionary doesn’t contain the given key.
The following Playground example shows how to use subscript(_:default:) in order to solve your problem:
struct Item {
let date: String
let amount: Double
}
let data = [
Item(date:"2017.02.15", amount: 25),
Item(date:"2017.02.14", amount: 50),
Item(date:"2017.02.11", amount: 35),
Item(date:"2017.02.15", amount: 15)
]
var dictionary = [String: Double]()
for item in data {
dictionary[item.date, default: 0.0] += item.amount
}
print(dictionary) // ["2017.02.11": 35.0, "2017.02.15": 40.0, "2017.02.14": 50.0]
#2 Using Dictionary init(_:uniquingKeysWith:) initializer
Dictionay has a init(_:uniquingKeysWith:) initializer. init(_:uniquingKeysWith:) has the following declaration:
init<S>(_ keysAndValues: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows where S : Sequence, S.Element == (Key, Value)
Creates a new dictionary from the key-value pairs in the given sequence, using a combining closure to determine the value for any duplicate keys.
The following Playground example shows how to use init(_:uniquingKeysWith:) in order to solve your problem:
struct Item {
let date: String
let amount: Double
}
let data = [
Item(date:"2017.02.15", amount: 25),
Item(date:"2017.02.14", amount: 50),
Item(date:"2017.02.11", amount: 35),
Item(date:"2017.02.15", amount: 15)
]
let tupleArray = data.map({ ($0.date, $0.amount) })
let dictonary = Dictionary(tupleArray, uniquingKeysWith: { (current, new) in
current + new
})
//let dictonary = Dictionary(tupleArray, uniquingKeysWith: +) // also works
print(dictonary) // prints ["2017.02.11": 35.0, "2017.02.15": 40.0, "2017.02.14": 50.0]
I thought this solution may be better since your property date actually is a Date
Solution:
struct Item {
let date: Date
let amount: Double
}
var data = [Item(date:createDate(stringDate: "2017.02.15"), amount: 25),
Item(date:createDate(stringDate: "2017.02.14"), amount: 50),
Item(date:createDate(stringDate: "2017.02.11"), amount: 35),
Item(date:createDate(stringDate: "2017.02.15"), amount: 15)]
data = sumAmounts(data)
print(data)
//[
// Item(date: 2017-02-15 04:00:00 +0000, amount: 40.0),
// Item(date: 2017-02-14 04:00:00 +0000, amount: 50.0),
// Item(date: 2017-02-11 04:00:00 +0000, amount: 35.0)
//]
Helper methods:
func createDate(stringDate: String) -> Date {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy.MM.dd"
return formatter.date(from: stringDate)!
}
func sumAmounts(_ data: [Item]) -> [Item] {
var dict = [Double: Double]()
data.forEach { item in
let key = item.date.timeIntervalSince1970
var amount = dict[key] ?? 0
amount += item.amount
dict[key] = amount
}
var newData = [Item]()
for (key, val) in dict {
newData.append(Item(date: Date.init(timeIntervalSince1970: key), amount: val))
}
return newData
}

Group elements of an array by some property

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

Swift 3: Array to Dictionary?

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

Resources