web-dev-qa-db-fra.com

Scala: fusionner la carte

Comment puis-je fusionner des cartes comme ci-dessous:

Map1 = Map(1 -> Class1(1), 2 -> Class1(2))
Map2 = Map(2 -> Class2(1), 3 -> Class2(2))

Après avoir fusionné.

Merged = Map( 1 -> List(Class1(1)), 2 -> List(Class1(2), Class2(1)), 3 -> Class2(2))

Peut être List, Set ou toute autre collection ayant un attribut size.

33
Robinho

En utilisant la bibliothèque standard, vous pouvez le faire comme suit:

// convert maps to seq, to keep duplicate keys and concat
val merged = Map(1 -> 2).toSeq ++ Map(1 -> 4).toSeq
// merged: Seq[(Int, Int)] = ArrayBuffer((1,2), (1,4))

// group by key
val grouped = merged.groupBy(_._1)
// grouped: scala.collection.immutable.Map[Int,Seq[(Int, Int)]] = Map(1 -> ArrayBuffer((1,2), (1,4)))


// remove key from value set and convert to list
val cleaned = grouped.mapValues(_.map(_._2).toList)
// cleaned: scala.collection.immutable.Map[Int,List[Int]] = Map(1 -> List(2, 4))
58
drexin

Ceci est la mise en œuvre la plus simple que j'ai pu trouver,

val m1 = Map(1 -> "1", 2 -> "2")
val m2 = Map(2 -> "21", 3 -> "3")

def merge[K, V](m1:Map[K, V], m2:Map[K, V]):Map[K, List[V]] = 
  (m1.keySet ++ m2.keySet) map { i => i -> (m1.get(i).toList ::: m2.get(i).toList) } toMap

merge(m1, m2) // Map(1 -> List(1), 2 -> List(2, 21), 3 -> List(3))
22
tiran

Vous pouvez utiliser scalaz :

import scalaz._, Scalaz._

val m1 = Map('a -> 1, 'b -> 2)
val m2 = Map('b -> 3, 'c -> 4)

m1.mapValues{List(_)} |+| m2.mapValues{List(_)}
// Map('b -> List(2, 3), 'c -> List(4), 'a -> List(1))

Vous pouvez utiliser Set(_) au lieu de List(_) pour obtenir Sets comme valeurs dans Map.

Voir Semigroup dans Aide-mémoire scalaz (ou dans apprentissage scalaz ) pour plus de détails sur |+|.

Pour Int|+| Fonctionne comme +, Pour List - comme ++, Pour Map cela s'applique |+| Aux valeurs des mêmes clés.

16
senia

J'ai écrit un blog à ce sujet, consultez-le:

http://www.nimrodstech.com/scala-map-merge/

en utilisant essentiellement le groupe semi-scalaz, vous pouvez y parvenir assez facilement

ressemblerait à quelque chose comme:

  import scalaz.Scalaz._
  Map1 |+| Map2
10
Nimrod007

Une façon propre de le faire, avec chats :

import cats.implicits._

Map(1 -> "Hello").combine(Map(2 -> "Goodbye"))
//Map(2 -> Goodbye, 1 -> Hello)

Il est important de noter que les deux cartes doivent être du même type (dans ce cas, Map[Int, String]).

Explication longue:

combine n'est pas vraiment membre de Map. En important cats.implicits, vous mettez dans le champ d'application les instances monoïdes intégrées de cats, ainsi que certaines classes implicites qui permettent la syntaxe laconique.

Ce qui précède est équivalent à ceci:

Monoid[Map[Int, String]].combine(Map(1 -> "Hello"), Map(2 -> "Goodbye"))

Où nous utilisons la fonction fonction "invocateur" monoïde pour obtenir l'instance Monoid [Map [Int, String]] dans la portée et en utilisant sa fonction de combinaison.

10
David Castillo

Vous pouvez utiliser foldLeft pour fusionner deux cartes du même type

def merge[A, B](a: Map[A, B], b: Map[A, B])(mergef: (B, Option[B]) => B): Map[A, B] = {
  val (big, small) = if (a.size > b.size) (a, b) else (b, a)
  small.foldLeft(big) { case (z, (k, v)) => z + (k -> mergef(v, z.get(k))) }
}

def mergeIntSum[A](a: Map[A, Int], b: Map[A, Int]): Map[A, Int] =
  merge(a, b)((v1, v2) => v2.map(_ + v1).getOrElse(v1))

Exemple:

val a = Map("a" -> 1, "b" -> 5, "c" -> 6)
val b = Map("a" -> 4, "z" -> 8)
mergeIntSum(a, b)
res0: Map[String,Int] = Map(a -> 5, b -> 5, c -> 6, z -> 8)
3
x4444

A partir de Scala 2.13, Une autre solution uniquement basée sur la bibliothèque standard consiste à utiliser groupMap qui (comme son nom l'indique) est l'équivalent d'un groupBy suivi de mapValues:

// val m1 = Map(1 -> "a", 2 -> "b")
// val m2 = Map(2 -> "c", 3 -> "d")
(m1.toSeq ++ m2).groupMap(_._1)(_._2)
// Map[Int,Seq[String]] = Map(2 -> List("b", "c"), 1 -> List("a"), 3 -> List("d"))

Cette:

  • Concatène les deux cartes en tant que séquence de tuples (List((1,"a"), (2,"b"), (2,"c"), (3,"d"))). Par souci de concision, m2 Est implicitement converti en Seq pour s'adapter au type de m1.toSeq - mais vous pouvez choisir de le rendre explicite en en utilisant m2.toSeq.

  • groups éléments basés sur leur première partie Tuple (_._1) (partie de groupe du groupe Carte)

  • maps ont groupé les valeurs dans leur deuxième partie Tuple (_._2) (partie de la carte du groupe Carte )

3
Xavier Guihot

Solution pour combiner deux cartes: Map[A,B], le type de résultat: Map[A,List[B]] via le Scala Cats (version légèrement améliorée, offerte par @David Castillo)

// convertit chaque carte d'origine en Carte [A, Liste [B]]. // Ajoutez une instance de Monoid [List] dans la portée pour combiner les listes:

import cats.instances.map._ // for Monoid
import cats.syntax.semigroup._ // for |+|
import cats.instances.list._

val map1 = Map("a" -> 1, "b" -> 2)
  .mapValues(List(_))
val map2 = Map("b" -> 3, "d" -> 4)
  .mapValues(List(_))
map1 |+| map2
1
Alexandr

Si vous ne voulez pas jouer avec les cartes originales, vous pouvez faire quelque chose comme

val target = map1.clone()
val source = map2.clone()
source.foreach(e => target += e._1 -> e._2)
1
smishra
left.keys map { k => k -> List(left(k),right(k)) } toMap

Est concis et fonctionnera, en supposant que vos deux cartes sont left et right. Pas sûr de l'efficacité.

Mais votre question est un peu ambiguë, pour deux raisons. Vous ne spécifiez pas

  1. La relation de sous-typage entre les valeurs (c'est-à-dire class1, class2),
  2. Que se passe-t-il si les cartes ont des clés différentes

Pour le premier cas, considérons l'exemple suivant:

val left = Map("foo" ->1, "bar" ->2)
val right = Map("bar" -> 'a', "foo" -> 'b')

Ce qui se traduit par

res0: Map[String,List[Int]] = Map(foo -> List(1, 98), bar -> List(2, 97))

Remarquez comment les Chars ont été convertis en Ints, en raison de la hiérarchie de type scala. Plus généralement, si dans votre exemple class1 et class2 ne sont pas liés, vous obtiendrez un List[Any]; ce n'est probablement pas ce que vous vouliez.

Vous pouvez contourner ce problème en supprimant le constructeur List de ma réponse; cela renverra Tuples qui préservent le type:

res0: Map[String,(Int, Char)] = Map(foo -> (1,b), bar -> (2,a))

Le deuxième problème est ce qui se passe lorsque vous avez des cartes qui n'ont pas les mêmes clés. Cela se traduira par un key not found exception. Autrement dit, faites-vous une jointure gauche, droite ou intérieure des deux cartes? Vous pouvez lever l'ambiguïté du type de jointure en passant à right.keys ou right.keySet ++ left.keySet pour les jointures droite/intérieure respectivement. Ce dernier contournera le problème de clé manquante, mais ce n'est peut-être pas ce que vous voulez, c'est-à-dire que vous souhaitez plutôt une jointure gauche ou droite. Dans ce cas, vous pouvez envisager d'utiliser la méthode withDefault de Map pour vous assurer que chaque clé renvoie une valeur, par exemple None, mais cela nécessite un peu plus de travail.

1
Luciano
m2.foldLeft(m1.mapValues{List[CommonType](_)}) { case (acc, (k, v)) =>
  acc.updated(k, acc.getOrElse(k, List.empty) :+ v)
}

Comme indiqué par jwvh, le type de liste doit être spécifié explicitement si Class1 n'est pas un type supérieur lié à Class2. CommonType est un type qui est à la fois supérieur pour Class1 et Class2.

1
Alx

Il existe un module Scala appelé scala-collection-contrib , qui propose des méthodes très utiles comme mergeByKey.

Tout d'abord, nous devons ajouter une dépendance supplémentaire à build.sbt:

libraryDependencies += "org.scala-lang.modules" %% "scala-collection-contrib" % "0.1.0"

puis il est possible de faire une fusion comme ceci:

import scala.collection.decorators._

val map1 = Map(1 -> Class1(1), 2 -> Class1(2))
val map2 = Map(2 -> Class2(1), 3 -> Class2(2))

map1.mergeByKeyWith(map2){
  case (a,b) => a.toList ++ b.toList
}
0
Krzysztof Atłasik