Zvi Mints
Zvi Mints

Reputation: 1142

Merge maps if key already exists with two levels

I wrote the following function:

private[this] def extractAerospikeBinsFromKafkaConnectorResource(connectors: List[KafkaConnector]): Map[AerospikeNamespace, Map[AerospikeSet, RecoverBins]] = {
    connectors.foldLeft(Map.empty[AerospikeNamespace, Map[AerospikeSet, RecoverBins]]) {
      case (acc, connector) =>
        acc ++
          TopicsConfig
            .parseYaml(connector.getSpec.getConfig.get("config-yaml").toString)
            .topics
            .values
            .map(topicConfig => topicConfig.mapping.namespace.value -> Map(topicConfig.mapping.set.value -> RecoverBins(topicConfig.mapping.bins)))
    }
  }
Map(namespace -> Map(test-recovery-set-single -> SingleBin))

I want to find a nice way to merge two maps of type Map[AerospikeNamespace, Map[AerospikeSet, RecoverBins]] into one, such that if they have same AerospikeNamespace, its will be contacted and if they have same AerospikeNamespace and AerospikeSet its will be contacted, for example:

input:

  1. Map(namespace -> Map(test-recovery-set-double -> SingleBin))
  2. Map(namespace -> Map(test-recovery-set-double -> MultiBin))
  3. Map(namespace -> Map(test-recovery-set-multi -> MultiBin))
  4. Map(namespace2 -> Map(test-recovery-set-other -> SingleBin))

output:

Map (
namespace -> Map(
        test-recovery-set-double -> List[MultiBin, SingleBin]),
        test-recovery-set-multi -> List[MultiBin]),
),
namespace2 -> Map(test-recovery-set-single -> SingleBin)
)

Upvotes: 1

Views: 302

Answers (2)

Jasper-M
Jasper-M

Reputation: 15086

The cats library provides this functionality virtually out of the box.

val maps = List(
  Map("namespace" -> Map("test-recovery-set-double" -> "SingleBin")),
  Map("namespace" -> Map("test-recovery-set-double" -> "MultiBin")),
  Map("namespace" -> Map("test-recovery-set-multi" -> "MultiBin")),
  Map("namespace2" -> Map("test-recovery-set-other" -> "SingleBin"))
)

import cats.implicits._

maps.foldMap(_.view.mapValues(_.view.mapValues(_ :: Nil).toMap).toMap)
/*
Map(
  namespace2 -> Map(
    test-recovery-set-other -> List(SingleBin)
  ),
  namespace -> Map(
    test-recovery-set-multi -> List(MultiBin),
    test-recovery-set-double -> List(SingleBin, MultiBin)
  )
)
*/

foldMap works by wrapping the inner values of your maps in a List and then folding them into a single Map by using the Monoid instance of Map[A, Map[B, List[C]]].

The only less elegant part of this code comes from mapValues in scala 2.13 being awkward to use.

Instead of using foldMap, it's not hard to implement yourself:

import cats.implicits._

val listMaps = maps.map(_.view.mapValues(_.view.mapValues(_ :: Nil).toMap).toMap)
listMaps.reduce(_ |+| _) // |+| is an alias for Monoid.combine

As pointed out in the comments by @LuisMiguelMejíaSuárez, you can also clean up the mapValues a bit with some more cats magic.

maps.foldMap(_.fmap(_.fmap(_ :: Nil)))
// or
maps.foldMap(_.nested.map(_ :: Nil).value)

Upvotes: 2

Tim
Tim

Reputation: 27421

Assuming Scala 2.13 then this helper function merges a list of Maps to a single Map:

def mergeMaps[T, U](maps: List[Map[T, U]]): Map[T, List[U]] =
  maps
    .flatMap(_.toList) // Convert each map to list and concatenate
    .groupMap(_._1)(_._2) // Group by key to new Map

It converts each map to a list of pairs, flattens the list, and then creates a new map by grouping on the first value in the pair and extracting the second value.

Now you just need to apply this to the outer map and then each inner map:

val maps = List(
  Map("namespace" -> Map("test-recovery-set-double" -> "SingleBin")),
  Map("namespace" -> Map("test-recovery-set-double" -> "MultiBin")),
  Map("namespace" -> Map("test-recovery-set-multi" -> "MultiBin")),
  Map("namespace2" -> Map("test-recovery-set-other" -> "SingleBin"))
)

mergeMaps(maps).map{ case (k, v) => k -> mergeMaps(v) }

The result is

Map(namespace2 -> Map(test-recovery-set-other -> List(SingleBin)), namespace -> Map(test-recovery-set-multi -> List(MultiBin), test-recovery-set-double -> List(SingleBin, MultiBin)))

Upvotes: 1

Related Questions