Sweeper
Sweeper

Reputation: 273540

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?

Upvotes: 2

Views: 768

Answers (2)

user652038
user652038

Reputation:

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

Upvotes: 1

David Pasztor
David Pasztor

Reputation: 54755

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

Upvotes: 0

Related Questions