web-dev-qa-db-fra.com

Manière élégante d'inverser une carte à Scala

Apprendre Scala à l’heure actuelle et avoir besoin d’inverser une carte pour effectuer des recherches inversées de valeur -> clé. Je cherchais un moyen simple de le faire, mais je n'ai trouvé que:

(Map() ++ origMap.map(kvp=>(kvp._2->kvp._1)))

Quelqu'un a une approche plus élégante?

89
AlexeyMK

En supposant que les valeurs sont uniques, cela fonctionne:

(Map() ++ origMap.map(_.swap))

Sur Scala 2.8, cependant, c'est plus facile:

origMap.map(_.swap)

Cela fait partie de la raison pour laquelle Scala 2.8 dispose d’une nouvelle bibliothèque de collections.

158
Daniel C. Sobral

Mathématiquement, le mappage peut ne pas être inversible (injectif), par exemple à partir de Map[A,B], vous ne pouvez pas obtenir Map[B,A], mais vous obtenez plutôt Map[B,Set[A]], car différentes clés peuvent être associées aux mêmes valeurs. Donc, si vous êtes intéressé à connaître toutes les clés, voici le code: 

scala> val m = Map(1 -> "a", 2 -> "b", 4 -> "b")
scala> m.groupBy(_._2).mapValues(_.keys)
res0: Map[String,Iterable[Int]] = Map(b -> Set(2, 4), a -> Set(1))
40
Rok Kralj

Vous pouvez éviter les choses ._1 en effectuant une itération de plusieurs façons. 

Voici un moyen. Ceci utilise une fonction partielle qui couvre le seul et unique cas qui compte pour la carte: 

Map() ++ (origMap map {case (k,v) => (v,k)})

Voici un autre moyen:

import Function.tupled        
Map() ++ (origMap map tupled {(k,v) => (v,k)})

L'itération de la carte appelle une fonction avec un tuple à deux éléments et la fonction anonyme demande deux paramètres. Function.tupled effectue la traduction.

10
Lee Mighdoll

Je suis venu ici en cherchant un moyen d'inverser une carte de type Carte [A, Seq [B]] en Carte [B, Seq [A]], où chaque B de la nouvelle carte est associé à chaque A de l'ancienne carte. dont le B était contenu dans la séquence associée de A.

Par exemple.,
Map(1 -> Seq("a", "b"), 2-> Seq("b", "c"))
serait inverser à
Map("a" -> Seq(1), "b" -> Seq(1, 2), "c" -> Seq(2))

Voici ma solution:

val newMap = oldMap.foldLeft(Map[B, Seq[A]]().withDefaultValue(Seq())) {
  case (m, (a, bs)) => bs.foldLeft(m)((map, b) => map.updated(b, m(b) :+ a))
}

où oldMap est de type Map[A, Seq[B]] et newMap de type Map[B, Seq[A]]

Les foldLefts imbriqués me font un peu grincer des dents, mais c'est le moyen le plus simple que j'ai pu trouver pour accomplir ce type d'inversion. Quelqu'un a une solution plus propre?

6
Eddie Carlson

OK, donc c’est une très vieille question avec beaucoup de bonnes réponses, mais j’ai construit le couteau suisse suprême, le couteau suisse, l’inverseur Map et c’est le lieu de le poster.

C'est en fait deux inverseurs. Un pour les éléments de valeur individuels ...

//from Map[K,V] to Map[V,Set[K]], traverse the input only once
implicit class MapInverterA[K,V](m :Map[K,V]) {
  def invert :Map[V,Set[K]] =
    m.foldLeft(Map.empty[V, Set[K]]) {
      case (acc,(k, v)) => acc + (v -> (acc.getOrElse(v,Set()) + k))
    }
}

... et un autre, assez similaire, pour les collections de valeur.

import scala.collection.generic.CanBuildFrom
import scala.collection.mutable.Builder
import scala.language.higherKinds

//from Map[K,C[V]] to Map[V,C[K]], traverse the input only once
implicit class MapInverterB[K,V,C[_]](m :Map[K,C[V]]
                                     )(implicit ev :C[V] => TraversableOnce[V]) {
  def invert(implicit bf :CanBuildFrom[Nothing,K,C[K]]) :Map[V,C[K]] =
    m.foldLeft(Map.empty[V, Builder[K,C[K]]]) {
      case (acc, (k, vs)) =>
        vs.foldLeft(acc) {
          case (a, v) => a + (v -> (a.getOrElse(v,bf()) += k))
        }
    }.mapValues(_.result())
}

usage:

Map(2 -> Array('g','h'), 5 -> Array('g','y')).invert
//res0: Map(g -> Array(2, 5), h -> Array(2), y -> Array(5))

Map('q' -> 1.1F, 'b' -> 2.1F, 'c' -> 1.1F, 'g' -> 3F).invert
//res1: Map(1.1 -> Set(q, c), 2.1 -> Set(b), 3.0 -> Set(g))

Map(9 -> "this", 8 -> "that", 3 -> "thus", 2 -> "thus").invert
//res2: Map(this -> Set(9), that -> Set(8), thus -> Set(3, 2))

Map(1L -> Iterator(3,2), 5L -> Iterator(7,8,3)).invert
//res3: Map(3 -> Iterator(1, 5), 2 -> Iterator(1), 7 -> Iterator(5), 8 -> Iterator(5))

Map.empty[Unit,Boolean].invert
//res4: Map[Boolean,Set[Unit]] = Map()

Je préférerais que les deux méthodes soient dans la même classe implicite, mais plus je passais de temps à la regarder, plus la tâche était problématique.

2
jwvh

Vous pouvez inverser une carte en utilisant:

val i = origMap.map({case(k, v) => v -> k})

Le problème avec cette approche est que si vos valeurs, qui sont devenues les clés de hachage de votre carte, ne sont pas uniques, vous supprimerez les valeurs en double. Pour illustrer:

scala> val m = Map("a" -> 1, "b" -> 2, "c" -> 3, "d" -> 1)
m: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, c -> 3, d -> 1)

// Notice that 1 -> a is not in our inverted map
scala> val i = m.map({ case(k , v) => v -> k})
i: scala.collection.immutable.Map[Int,String] = Map(1 -> d, 2 -> b, 3 -> c)

Pour éviter cela, vous pouvez d'abord convertir votre carte en une liste de n-uplets, puis l'inverser afin de ne pas supprimer les valeurs en double:

scala> val i = m.toList.map({ case(k , v) => v -> k})
i: List[(Int, String)] = List((1,a), (2,b), (3,c), (1,d))
2
hohonuuli

En scala REPL:

scala> val m = Map(1 -> "one", 2 -> "two")
m: scala.collection.immutable.Map[Int,Java.lang.String] = Map(1 -> one, 2 -> two)

scala> val reversedM = m map { case (k, v) => (v, k) }
reversedM: scala.collection.immutable.Map[Java.lang.String,Int] = Map(one -> 1, two -> 2)

Notez que les valeurs en double seront écrasées par le dernier ajout à la carte:

scala> val m = Map(1 -> "one", 2 -> "two", 3 -> "one")
m: scala.collection.immutable.Map[Int,Java.lang.String] = Map(1 -> one, 2 -> two, 3 -> one)

scala> val reversedM = m map { case (k, v) => (v, k) }
reversedM: scala.collection.immutable.Map[Java.lang.String,Int] = Map(one -> 3, two -> 2)
1
yǝsʞǝlA
  1. Inverse est un meilleur nom pour cette opération qu'inverser (comme dans "inverse d'une fonction mathématique")

  2. Je fais souvent cette transformation inverse non seulement sur les cartes mais sur d'autres collections (y compris Seq). Je trouve préférable de ne pas limiter la définition de mon opération inverse aux cartes un-à-un. Voici la définition avec laquelle je travaille pour les cartes (veuillez suggérer des améliorations à mon implémentation).

    def invertMap[A,B]( m: Map[A,B] ) : Map[B,List[A]] = {
      val k = ( ( m values ) toList ) distinct
      val v = k map { e => ( ( m keys ) toList ) filter { x => m(x) == e } }
      ( k Zip v ) toMap
    }
    

S'il s'agit d'une carte un à un, vous vous retrouvez avec des listes de singleton qui peuvent être testées de manière triviale et transformées en une carte [B, A] plutôt qu'en une carte [B, List [A]].

0
Ashwin

À partir de Scala 2.13, afin d’échanger clé/valeurs sans perdre les clés associées aux mêmes valeurs, nous pouvons utiliser la méthode Maps new groupMap qui, comme son nom l’indique, est équivalente à groupBy et mapping sur groupé articles.

Map(1 -> "a", 2 -> "b", 4 -> "b").groupMap(_._2)(_._1)
// Map("b" -> List(2, 4), "a" -> List(1))

Ce:

  • groups éléments basés sur leur deuxième partie Tuple (_._2) (partie groupe de groupe Map)

  • maps les éléments groupés en prenant leur première partie Tuple (_._1) (partie map du groupeMap)

Cela peut être vu comme une version en un seul passage de map.groupBy(_._2).mapValues(_.map(_._1)).

0
Xavier Guihot

Nous pouvons essayer d'utiliser cette fonction foldLeft qui prendra en charge les collisions et inversera la carte en un seul parcours.

scala> def invertMap[A, B](inputMap: Map[A, B]): Map[B, List[A]] = {
     |     inputMap.foldLeft(Map[B, List[A]]()) {
     |       case (mapAccumulator, (value, key)) =>
     |         if (mapAccumulator.contains(key)) {
     |           mapAccumulator.updated(key, mapAccumulator(key) :+ value)
     |         } else {
     |           mapAccumulator.updated(key, List(value))
     |         }
     |     }
     |   }
invertMap: [A, B](inputMap: Map[A,B])Map[B,List[A]]

scala> val map = Map(1 -> 2, 2 -> 2, 3 -> 3, 4 -> 3, 5 -> 5)
map: scala.collection.immutable.Map[Int,Int] = Map(5 -> 5, 1 -> 2, 2 -> 2, 3 -> 3, 4 -> 3)

scala> invertMap(map)
res0: Map[Int,List[Int]] = Map(5 -> List(5), 2 -> List(1, 2), 3 -> List(3, 4))

scala> val map = Map("A" -> "A", "B" -> "A", "C" -> "C", "D" -> "C", "E" -> "E")
map: scala.collection.immutable.Map[String,String] = Map(E -> E, A -> A, B -> A, C -> C, D -> C)

scala> invertMap(map)
res1: Map[String,List[String]] = Map(E -> List(E), A -> List(A, B), C -> List(C, D))
0