Monday, September 5, 2016

Rollup "map" from an Array to a Dictionary

I've often found myself doing work taking a sequence (usually an Array) of things and turning them into a Dictionary (key -> value mapping). Whenever I find myself doing something similar a few too many times, I find it's time to write a bit of reusable code, hence rollup.
rollup is three functions to provide an interface for taking a sequence of items and making a dictionary out of them. Here are the three cases I handled:
  • A) I have a sequence of Keys, so I provide a function that maps each Key to a Value to fill out the dictionary.
  • B) I have a sequence of Values, so I provide a function that maps each Value to a Key to fill out the dictionary.
  • C) I have a sequence of Things, so I provide a function that maps each Thing to a Key & Value to fill out the dictionary.
Let's see how we might use each.
A) I have the Keys
someKeys.rollupKeys({ (key) in valueForKey(key) })

B) I have the Values
someValues.rollup({ (value) in value.keyAttribute })

C) I have some Things
someThings.rollup({ (thing) in (thing.keyAttribute, thing.valueAttribute) })

Why does (A) use the name rollupKeys() for it's function? The short answer is compiler confusion. Let's use a simple example and say I want to use String as the type for both keys and values. The compiler would have no way to tell which version of rollup() I was trying to use because the function from a Key to a Value is identical to a function from a Value to a Key in this case. So I'm choosing to make life simpler for (B) as that is the more frequent usage of this rollup() behavior in my own experience.

Here is the current code for Rollup as a Gist, and some important Note/Warning below…

// I found myself writing code to "map" an array to a dictionary. Rollup is my solution to provide a simpler interface to this boilerplate.
// I have a sequence of values, and a function to give a key for each value, give me the dictionary
// [V].rollup((V) -> K?) -> [K: V]
// I have a sequence of things, and a function to give a key and value for each thing, give me the dictionary
// [T].rollup((T) -> (K, V)?) -> [K: V]
// I have a sequence of keys, and a function to give a value for each key, give me the dictionary
// [K].rollupKeys((K) -> V?) -> [K: V]
extension SequenceType where Self.Generator.Element: Hashable
{
func rollupKeys<T>(mapping: (Self.Generator.Element) -> T?) -> [Self.Generator.Element: T]
{
var rollup: [Self.Generator.Element: T] = [:]
forEach { (t) in
if let value = mapping(t) {
rollup[t] = value
}
}
return rollup
}
}
extension SequenceType
{
func rollup<T: Hashable, U>(mapping: (Self.Generator.Element) -> (T, U)?) -> [T: U]
{
var rollup: [T: U] = [:]
forEach { (t) in
if let (key, value) = mapping(t) {
rollup[key] = value
}
}
return rollup
}
func rollup<T: Hashable>(keyForValue keyForValue: (Self.Generator.Element) -> T?) -> [T: Self.Generator.Element]
{
var rollup: [T: Self.Generator.Element] = [:]
forEach { (value) in
if let key = keyForValue(value) {
rollup[key] = value
}
}
return rollup
}
}
/* Example usage:
*/
import Foundation
struct Person
{
let name: String
}
let people: [Person] = []
let names: [String] = []
// get a dictionary of type [String: Person] where the key is the person's last name
let rollup = people.rollup { (person: Person) -> String? in return person.name.componentsSeparatedByString(" ").last }
// get a dictionary of type [String: Int] where the key is the person's name, and the value is how long their name is
let rollup2 = people.rollup { (person: Person) -> (String, Int)? in
return (person.name, person.name.characters.count)
}
// get a dictionary of type [String: Int] where the key is the name, and the value is how long their name is
let rollup3 = names.rollupKeys { (name: String) -> (Int) in
return name.characters.count
}
view raw Rollup.swift hosted with ❤ by GitHub

I should also note, one flaw with these as they are is that they do NOT gracefully handle situations where you have more than one Value for the same Key. These functions, as written today, will return the LAST Key->Value mapping seen by going thru the sequence in order, so you would lose any other values that shared a common key. Arguably, a better interface might be to have these functions be called flatrollup() and use rollup to map things into a dictionary of type [Key: [Value]] to handle the overflow problem. Then you would get an array of multiple items whenever you provided multiple values for the key. If you make such code, please tweet me and I would be happy to share your solution.

No comments:

Post a Comment