Scala: fusionner des cartes par clé
Disons que j'ai deux cartes:
val a = Map(1 -> "one", 2 -> "two", 3 -> "three")
val b = Map(1 -> "un", 2 -> "deux", 3 -> "trois")
Je veux fusionner ces cartes par clé, en appliquant une fonction pour collecter les valeurs (dans ce cas particulier, je veux les collecter dans un seq, en donnant:
val c = Map(1 -> Seq("one", "un"), 2->Seq("two", "deux"), 3->Seq("three", "trois"))
On dirait qu'il devrait y avoir une belle façon idiomatique de le faire-des suggestions? Je suis heureux si la solution implique scalaz.
6 réponses
scala.collection.immutable.IntMap
a une méthode intersectionWith
qui fait exactement ce que vous voulez (je crois):
import scala.collection.immutable.IntMap
val a = IntMap(1 -> "one", 2 -> "two", 3 -> "three", 4 -> "four")
val b = IntMap(1 -> "un", 2 -> "deux", 3 -> "trois")
val merged = a.intersectionWith(b, (_, av, bv: String) => Seq(av, bv))
Cela vous donne IntMap(1 -> List(one, un), 2 -> List(two, deux), 3 -> List(three, trois))
. Notez qu'il ignore correctement la clé qui se produit uniquement dans a
.
Comme note de côté: je me suis souvent retrouvé à vouloir le unionWith
, intersectionWith
, etc. fonctions de Haskell Data.Map
à Scala. Je ne pense pas qu'il y ait une raison de principe qu'ils ne devraient être disponibles que sur IntMap
, au lieu de dans le trait de base collection.Map
.
val a = Map(1 -> "one", 2 -> "two", 3 -> "three")
val b = Map(1 -> "un", 2 -> "deux", 3 -> "trois")
val c = a.toList ++ b.toList
val d = c.groupBy(_._1).map{case(k, v) => k -> v.map(_._2).toSeq}
//res0: scala.collection.immutable.Map[Int,Seq[java.lang.String]] =
//Map((2,List(two, deux)), (1,List(one, un), (3,List(three, trois)))
Scalaz ajoute une méthode |+|
pour tout type A
pour lequel un Semigroup[A]
est disponible.
Si vous avez mappé vos cartes de sorte que chaque valeur soit une séquence à un seul élément, vous pouvez l'utiliser tout simplement:
scala> a.mapValues(Seq(_)) |+| b.mapValues(Seq(_))
res3: scala.collection.immutable.Map[Int,Seq[java.lang.String]] = Map(1 -> List(one, un), 2 -> List(two, deux), 3 -> List(three, trois))
Donc je n'étais pas très content de l'une ou l'autre solution (je veux construire un nouveau type, donc semigroup ne se sent pas vraiment approprié, et la solution D'Infinity semblait assez complexe), donc je suis allé avec ceci pour le moment. Je serais heureux de le voir amélioré:
def merge[A,B,C](a : Map[A,B], b : Map[A,B])(c : (B,B) => C) = {
for (
key <- (a.keySet ++ b.keySet);
aval <- a.get(key); bval <- b.get(key)
) yield c(aval, bval)
}
merge(a,b){Seq(_,_)}
Je voulais le comportement de ne rien renvoyer quand une clé n'était pas présente dans l'une ou l'autre carte (ce qui diffère des autres solutions), mais une façon de spécifier cela serait bien.
Voici ma première approche avant de chercher les autres solutions:
for (x <- a) yield
x._1 -> Seq (a.get (x._1), b.get (x._1)).flatten
Pour éviter les éléments qui n'existent que dans a ou b, un filtre est pratique:
(for (x <- a) yield
x._1 -> Seq (a.get (x._1), b.get (x._1)).flatten).filter (_._2.size == 2)
Aplatir est nécessaire, car B. get (X. _1) renvoie une Option. Pour faire aplatir le travail, le premier élément doit aussi être une option, donc nous ne pouvons pas simplement utiliser X. _2 ici.
Pour les séquences, cela fonctionne aussi:
scala> val b = Map (1 -> Seq(1, 11, 111), 2 -> Seq(2, 22), 3 -> Seq(33, 333), 5 -> Seq(55, 5, 5555))
b: scala.collection.immutable.Map[Int,Seq[Int]] = Map(1 -> List(1, 11, 111), 2 -> List(2, 22), 3 -> List(33, 333), 5 -> List(55, 5, 5555))
scala> val a = Map (1 -> Seq(1, 101), 2 -> Seq(2, 212, 222), 3 -> Seq (3, 3443), 4 -> (44, 4, 41214))
a: scala.collection.immutable.Map[Int,ScalaObject with Equals] = Map(1 -> List(1, 101), 2 -> List(2, 212, 222), 3 -> List(3, 3443), 4 -> (44,4,41214))
scala> (for (x <- a) yield x._1 -> Seq (a.get (x._1), b.get (x._1)).flatten).filter (_._2.size == 2)
res85: scala.collection.immutable.Map[Int,Seq[ScalaObject with Equals]] = Map(1 -> List(List(1, 101), List(1, 11, 111)), 2 -> List(List(2, 212, 222), List(2, 22)), 3 -> List(List(3, 3443), List(33, 333)))
val fr = Map(1 -> "one", 2 -> "two", 3 -> "three")
val en = Map(1 -> "un", 2 -> "deux", 3 -> "trois")
def innerJoin[K, A, B](m1: Map[K, A], m2: Map[K, B]): Map[K, (A, B)] = {
m1.flatMap{ case (k, a) =>
m2.get(k).map(b => Map((k, (a, b)))).getOrElse(Map.empty[K, (A, B)])
}
}
innerJoin(fr, en) // Map(1 -> ("one", "un"), 2 -> ("two", "deux"), 3 -> ("three", "trois")): Map[Int, (String, String)]