Dictionary init(grouping:by:), 2 levels - simplifying code - swift

Dictionary(init(grouping:by:)) works fine when I need to group array elements by some property.
What I get is:
{
key1: [
value1,
value2,
value3
],
key2: [
value4,
value5,
value6
]
}
However, I need to transform the array further, grouping every partition into smaller groups so, that the resulting data structure is having two layers:
{
key1: {
anotherKey1: [
value1,
],
anotherKey2: [
value2,
value3]
},
key1: {
anotherKey3: [
value4,
],
anotherKey4: [
value5,
value6]
},
}
What is the simplest way of achieving this result? Currently I have to iterate over the result of the 1st dictionary initializer call:
let grouped = Dictionary(grouping: Array(source), by: {$0.key1)
var grouped2 = [KeyType1 : [KeyType2? : [ValueType]]]()
for pair in grouped {
if let key = pair.key {
grouped2[key] = Dictionary(grouping: pair.value, by: {$0.key1})
}
}
print(grouped2)
And this gets me exactly the result I want: two-level dictionary of arrays.
But I suspect, there is a simpler way of achieving the same result, without manually interating over every key/value pair.

You can do this by doing a init(grouping:by:) call, followed by a mapValues call, which further maps every "group" into another dictionary. And this dictionary is going to be created by init(grouping:by:) again, using the second grouping key.
i.e.
let grouped = Dictionary(grouping: source, by: firstKey)
.mapValues { Dictionary(grouping: $0, by: secondKey) }
For example, to group a bunch of numbers first by their % 2 values, then by their % 4 values:
let source = [1,2,3,4,5,6,7,8,9,10,11,12,13]
let grouped = Dictionary(grouping: source, by: { $0 % 2 })
.mapValues { Dictionary.init(grouping: $0, by: { $0 % 4 }) }
print(grouped)

Related

Sort an array of objects, following the order of another array that has a value of the objects of the first array

How could I sort an array of objects taking into account another array?
Example:
let sortArray = [90, 1000, 4520]
let notSortArrayObject = [ { id: 4520, value: 4 }, {id: 1000, value: 2}, {id: 90, value:10} ]
So I want an array equal notSortArrayObject but sortered by sortArray value
let sortArrayObject =[{id: 90, value:10} , {id: 1000, value: 2}, { id: 4520, value: 4 }]
Here's an approach. For each number in the "guide" array, find a match in the not sorted and add it to the sortedObjectArray. Note that this ignores any values which don't have matches in the other array.
var sortedObjectArray: [Object] = [] // whatever your object type is
for num in sortArray {
if let match = notSortArrayObject.first(where: {$0.id == num}) {
sortedObjectArray.append(match)
}
}
If you just want to sort by the id in ascending order, it's much simpler:
let sorted = notSortArrayObject.sorted(by: {$0.id < $1.id})
I noticed that sortArray is already sorted - you could just sort notSortArrayObject by id, but I assume that's not the point of what you're looking for.
So, let's say that sortArray = [1000, 4520, 90] (random order), to make things more interesting.
You can iterate through sortArray and append the values that match the id to sortArrayObject.
// To store the result
var sortArrayObject = [MyObject]()
// Iterate through the elements that contain the required sequence
sortArray.forEach { element in
// Add only the objects (only one) that match the element
sortArrayObject += notSortArrayObject.filter { $0.id == element }
}

How to compare array of dictionaries?

I have an array of dictionaries whose keys could vary depending on the incoming data. For example,
arrayOfDict = [["default": "Accessibility", "cn": "为每个人而设计"],
["br": "Acessibilidade", "hk_cn": "輔助使用", "default": "Accessibility"],
["hk_cn": "輔助", "default": "Accessibility", "pl": "Ułatwienia dostępu"]]
I want to loop through each of the dictionaries to find if any key has multiple values and display that multiple values to the user and then let the user choose one of the value, remaining values has to be deleted.(For example in the above array "hk_cn" has two different values)
And if the multiple values are same, I want to delete one of the key. (For ex: default has same value everywhere)
So finally, I should have a single dictionary from arrayOfDict.
So final result should be : if user has chosen first "hk_cn" value.
["default": "Accessibility", "cn": "为每个人而设计","hk_cn": "輔助使用", "br": "Acessibilidade", "pl": "Ułatwienia dostępu" ]
1. Flatten out the arrayOfDict to get a single Dictionary using flatMap(_:)
let combinedDict = arrayOfDict.flatMap{($0)}
2. Group the combinedDict element based on the key using init(grouping:by:)
let groupedDict = Dictionary(grouping: combinedDict) { $0.key }
3. Get the formattedDict using mapValues(_:)
let formattedDict = groupedDict.mapValues { Array(Set($0.map { $0.value })) }
formattedDict Output:
["pl": ["Ułatwienia dostępu"], "cn": ["为每个人而设计"], "br": ["Acessibilidade"], "default": ["Accessibility"], "hk_cn": ["輔助使用", "輔助"]]
Now you can prompt user using the formattedDict and ask for which value to keep for the respective keys.
Edit:
As per your comment, you can use allSatisfy(_:) on formattedDict to check if it contains single value for each key.
let formattedDict = groupedDict.mapValues { Set($0.map { $0.value }) }
if formattedDict.values.allSatisfy({ $0.count == 1 }) {
let newformattedDict = formattedDict.compactMapValues { $0.first }
} else {
//add your code to display data in table here...
}

How do I group by a key that could be nil and get a dictionary with a non-optional key type?

Let's say I have a model like this:
enum UKCountry : String {
case england, scotland, wales, northernIreland
init?(placeName: String) {
// magic happens here
}
}
struct LocalPopulation {
let place: String
let population: Int
}
The init(placeName:) takes an arbitrary string and figures out where it is. For example init(placeName: "London") gives .england. How it does this is not important.
I also have a [LocalPopulation] I want to process. Specifically, I want to get a [UKCountry: [LocalPopulation]]. I could do this:
let populations: [LocalPopulation] = [...]
let dict = Dictionary(grouping: populations, by: { UKCountry(placeName: $0.place)! })
But not all places in the populations is a place in the UK, so UKCountry.init would return nil, and it would crash. Note that I do not want any place that is not in the UK in the result. I could filter it before hand:
let populations: [LocalPopulation] = [...]
let filteredPopulations = populations.filter { UKCountry(placeName: $0.place) != nil }
let dict = Dictionary(grouping: populations, by: { UKCountry(placeName: $0.place)! })
But that means running UKCountry.init twice. Depending on how it is implemented, this could be costly. The other way I thought of is:
let populations: [LocalPopulation] = [...]
var dict = Dictionary(grouping: populations, by: { UKCountry(placeName: $0.place) })
dict[nil] = nil
let result = dict as! [UKCountry: [LocalPopulation]]
But that's a bit too "procedural"... What's more annoying is, in all the above attempts, I have used !. I know sometimes ! is unavoidable, but I'd like to avoid it whenever I can.
How can I do this Swiftily?
First, allow dictionaries to easily be created from grouped key-value pairs:
public extension Dictionary {
/// Group key-value pairs by their keys.
///
/// - Parameter pairs: Either `Swift.KeyValuePairs<Key, Self.Value.Element>`
/// or a `Sequence` with the same element type as that.
/// - Returns: `[ KeyValuePairs.Key: [KeyValuePairs.Value] ]`
init<Value, KeyValuePairs: Sequence>(grouping pairs: KeyValuePairs)
where
KeyValuePairs.Element == (key: Key, value: Value),
Self.Value == [Value]
{
self =
Dictionary<Key, [KeyValuePairs.Element]>(grouping: pairs, by: \.key)
.mapValues { $0.map(\.value) }
}
/// Group key-value pairs by their keys.
///
/// - Parameter pairs: Like `Swift.KeyValuePairs<Key, Self.Value.Element>`,
/// but with unlabeled elements.
/// - Returns: `[ KeyValuePairs.Key: [KeyValuePairs.Value] ]`
init<Value, KeyValuePairs: Sequence>(grouping pairs: KeyValuePairs)
where
KeyValuePairs.Element == (Key, Value),
Self.Value == [Value]
{
self.init( grouping: pairs.map { (key: $0, value: $1) } )
}
}
Then, create a key-value sequence that's like KeyValuePairs<Key, Value.Element>, where Key and Value belong to your desired [ UKCountry: [LocalPopulation] ] type – but you don't need to bother with labeled elements.
Dictionary(
grouping: populations.compactMap { population in
UKCountry(placeName: population.place).map { ($0, population) }
}
)
You can use a reduce to transform your [LocalPopulation] into a [UKCountry:[LocalPopulation]] by also filtering out all elements which aren't in the UK.
let dict = populations.reduce(into: [UKCountry:[LocalPopulation]](), { currentResult, population in
if let ukCountry = UKCountry(placeName: population.place) {
currentResult[ukCountry, default: []].append(population)
}
})

Sorting a 3 Digit Number vs 4 Digit Number

I was previously sorting a unique id that consisted of two letters followed by three numbers. This unique id is used to group the entries into a sectioned tableview.
For example [un101, un098, un100, un099, un999] using the below code was working perfectly.
self.figuresByLetter = Dictionary(grouping: self.figures, by: { String($0.number[2])}).sorted(by: {$0.0 < $1.0})
And resulted in [(key: "0", value: un098, un099), (key: "1", value: un100, un101), (key: "9", value: un999)]
When the number of entries surpassed 1,000, I added a leading zero to the previous entries. I tried updating the code to:
self.figuresByLetter = Dictionary(grouping: self.figures, by: { String($0.number[3])}).sorted(by: {$0.0 < $1.0})
Now the result is [(key: "0", value: un0098, un0099, un1000, un1001), (key: "1", value: un0100, un0101), (key: "9", value: un0999)]
What can I do so that the entries are in the correct order?
UPDATE:
I figured out an easier way to accomplish exactly what I was looking for.
self.figuresByLetter = Dictionary(grouping: self.figures, by: { String($0.number.prefix(4))}).sorted(by: {$0.0 < $1.0})
If "the two letters are the same for every entry" and if there are always four digits, then just sort:
let input = ["un0098", "un0099", "un1000", "un1001", "un1002", "un0100", "un0101"]
let output = input.sorted()
output // ["un0098", "un0099", "un0100", "un0101", "un1000", "un1001", "un1002"]
As you can see, that's the right answer.
Based on that, now you can proceed to group into a dictionary and sort by key, like this:
let input = ["un0098", "un0099", "un1000", "un1001", "un1002", "un0100", "un0101"]
let output = input.sorted()
let d = Dictionary(grouping: output, by: { s -> String in
let ix = s.index(s.startIndex, offsetBy:2)
let ix2 = s.index(ix, offsetBy:2)
return String(s[ix..<ix2])
})
let model = d.sorted{$0.key < $1.key}
Now model is suitable as a datasource for your table view:
[(key: "00", value: ["un0098", "un0099"]),
(key: "01", value: ["un0100", "un0101"]),
(key: "10", value: ["un1000", "un1001", "un1002"])]
If that isn't quite what you wanted, it should be easy to see how to tweak it. The structure and order are correct, so if the data isn't quite the desired format, you can now change it. For example, if you don't like the leading zero in the key names, you can easily get rid of it.

Invert keys for values in Swift dictionary, collecting duplicate values

I have the following dictionary:
let dict = ["key1": "v1", "key2": "v1", "key3": "v2"]
I want to swap the values for keys so that the result to be:
result = ["v1": ["key1", "key2"], "v2": ["key3"]]
How can I do this without using for loops (i.e. in a functional way)?
In Swift 4, Dictionary has the init(_:uniquingKeysWith:) initializer that should serve well here.
let d = [1 : "one", 2 : "two", 3 : "three", 30: "three"]
let e = Dictionary(d.map({ ($1, [$0]) }), uniquingKeysWith: {
(old, new) in old + new
})
If you did not have duplicate values in the original dict that needed to be combined, this would be even simpler (using another new initializer):
let d = [1 : "one", 2 : "two", 3 : "three"]
let e = Dictionary(uniqueKeysWithValues: d.map({ ($1, $0) }))
You can use grouping initializer in Swift 4:
let dict = ["key1": "v1", "key2": "v1", "key3": "v2"]
let result = Dictionary(grouping: dict.keys.sorted(), by: { dict[$0]! })
Two notes:
You can remove .sorted() if the order of keys in resulting arrays is not important.
Force unwrap is safe in this case because we're getting an existing dictionary key as the $0 parameter
This is a special application of the commonly-needed ability to group key-value pairs by their keys.
public extension Dictionary {
/// Group key-value pairs by their keys.
///
/// - Parameter pairs: Either `Swift.KeyValuePairs<Key, Self.Value.Element>`
/// or a `Sequence` with the same element type as that.
/// - Returns: `[ KeyValuePairs.Key: [KeyValuePairs.Value] ]`
init<Value, KeyValuePairs: Sequence>(grouping pairs: KeyValuePairs)
where
KeyValuePairs.Element == (key: Key, value: Value),
Self.Value == [Value]
{
self =
Dictionary<Key, [KeyValuePairs.Element]>(grouping: pairs, by: \.key)
.mapValues { $0.map(\.value) }
}
/// Group key-value pairs by their keys.
///
/// - Parameter pairs: Like `Swift.KeyValuePairs<Key, Self.Value.Element>`,
/// but with unlabeled elements.
/// - Returns: `[ KeyValuePairs.Key: [KeyValuePairs.Value] ]`
init<Value, KeyValuePairs: Sequence>(grouping pairs: KeyValuePairs)
where
KeyValuePairs.Element == (Key, Value),
Self.Value == [Value]
{
self.init( grouping: pairs.map { (key: $0, value: $1) } )
}
}
With that, just flip each key-value pair and go to town.
Dictionary( grouping: dict.map { ($0.value, $0.key) } )