0

I want to change the keys and values for the keys key1 and key2 only when their values are val1 and val2 (both these mappings should be present for the transformation to take place). I am able to do it using the following code, but I do not think this is very elegant or efficient.

Is there a better way to do the same thing, perhaps using just one .map function applied over map?

Code:

val map = Map(
  "key1" -> "val1",
  "key2" -> "val2",
  "otherkey1" -> "otherval1"
)

val requiredKeys = List("key1", "key2")

val interestingMap = map.filterKeys(requiredKeys.contains) // will give ("key1" -> "val1", "key2" -> "val2").

val changedIfMatched =
  if (interestingMap.get("key1").get.equalsIgnoreCase("val1") && interestingMap.get("key2").get.equalsIgnoreCase("val2"))
    Map("key1" -> "newval1", "key2" -> "newval2")
  else
    interestingMap

print(map ++ changedIfMatched) // to replace the old key->values with the new ones, if any.

Also can ++ operation to update the old key->value mappings be made more efficient?

2
  • The code doesn't appear to remove the old keys but the description says "changes the keys and values". Can you clarify whether you want to remove the old keys if there is a match? Or do you just want to add new keys under certain conditions? Commented Jan 29, 2019 at 9:21
  • Thanks @Tim for pointing it out. I changed the sample output to reflect that the old keys remain the same but their values are updated. I am sorry for the confusion it caused. However, for the answers which address the change in keys as well, you may keep them there if anyone else has a similar doubt. Commented Jan 29, 2019 at 13:01

5 Answers 5

2

Just do the check ahead of time:

 map
   .get("k1").filter(_.equalsIgnoreCase("v1"))
   .zip(map.get("k2").filter(_.equalsIgnoreCase("v2")))
   .headOption
   .fold(map) { _ =>
      map ++ Map("key1" -> "newVal1", "key2" -> "newVal2")
   }
Sign up to request clarification or add additional context in comments.

2 Comments

Tthis does not compile, the _ after { gives an error: Missing parameter type. @Dima
@smaug fixed it. Sorry about that.
2

Here's an approach that checks that both key value pairs match.

EDIT: Added a mapValues method to the Map class. This technique can be used to do further checks on the values of the map.

val m = Map("key1" -> "val1", "key2" -> "VAL2", "otherkey1" -> "otherval1")
val oldKVs = Map("key1" -> "val1", "key2" -> "val2")
val newKVs = Map("newkey1" -> "newval1", "newkey2" -> "newval2")

implicit class MapImp[T,S](m: Map[T,S]) {
  def mapValues[R](f: S => R) = m.map { case (k,v) => (k, f(v)) }
  def subsetOf(m2: Map[T,S]) = m.toSet subsetOf m2.toSet
}

def containsKVs[T](m: Map[T,String], sub: Map[T,String]) =
  sub.mapValues(_.toLowerCase) subsetOf m.mapValues(_.toLowerCase)

val m2 = if (containsKVs(m, oldKVs)) m -- oldKVs.keys ++ newKVs else m

println(m2)
// Map(otherkey1 -> otherval1, newkey1 -> newval1, newkey2 -> newval2)

It takes advantage of the fact that you can convert Maps into Sets of Tuple2.

3 Comments

Technically this doesn't do the case-insensitive comparison in the OP's code, but it is certainly a clean answer.
I believe that as we are working with sets in this answer, there is no scope for further checks like case-insensitivity. Thanks for the solution though.
case insensitivity is just the matter of one .lower case or .uppercase or equalsIgnoreCaseit's not a big deal. I don't understand why it seems a huge problem to @Tim it can be handled while giving the new key values to be added and while makeing the map itself.
0

I think this will be the most generic and resuable solution for the problem.

object Solution1 extends App {

  val map = Map(
    "key1" -> "val1",
    "key2" -> "val2",
    "otherkey1" -> "otherval1"
  )

  implicit class MapUpdate[T](map: Map[T, T]) {
    def updateMapForGivenKeyValues: (Iterable[(T, T)], Iterable[(T, T)]) => Map[T, T] =
      (fromKV: Iterable[(T, T)], toKV: Iterable[(T, T)]) => {

        val isKeyValueExist: Boolean = fromKV.toIterator.forall {
          (oldKV: (T, T)) =>
            map.toIterator.contains(oldKV)
        }

        if (isKeyValueExist) map -- fromKV.map(_._1) ++ toKV else map
      }
  }


  val updatedMap = map.updateMapForGivenKeyValues(List("key1" -> "val1", "key2" -> "val2"),
    List("newKey1" -> "newVal1", "newVal2" -> "newKey2"))

  println(updatedMap)

}

So the method updateMapForGivenKeyValues takes the List of old key value and new key value tuple. If all the key value pairs mentioned in the first parameter of the method exist in the map then only we will update the map with new key value pairs mentioned in the second parameter of the method. As the method is generic will can be used on any data type like String, Int, some case class etc.

we can easily re-use the method for different type of maps without even changing a single line of code.

2 Comments

This seems over-complicated. It also doesn't work because it doesn't do case-insensitive matching of values.
Thanks @RamanMishra for this solution, I will certainly try it out ;-)
0

Answer to modified question

val map = Map(
  "key1" -> "val1",
  "key2" -> "val2",
  "otherkey1" -> "otherval1"
)

val requiredVals = List("key1"->"val1", "key2"->"val2")
val newVals = List("newval1", "newval2")

val result =
  if (requiredVals.forall{ case (k, v) => map.get(k).exists(_.equalsIgnoreCase(v)) }) {
    map ++ requiredVals.map(_._1).zip(newVals)
  } else {
    map
  }

This solution use forall to check that all the key/value pairs in requiredKeys are found in the map by testing each pair in turn.

For each key/value pair (k, v) it does a get on the map using the key to retrieve the current value as Option[String]. This will be None if the key is not found or Some(s) if the key is found.

The code then calls exists on the Option[String]. This method will return false if value is None (the key is not found), otherwise it will return the result of the test that is passed to it. The test is _.equalsIgnoreCase(v) which does a case-insensitive comparison of the contents of the Option (_) and the value from the requireKeys list (v).

If this test fails then the original value of map is returned.

If this test succeeds then a modified version of the map is return. The expression requiredVals.map(_._1) returns the keys from the requireVals list, and the zip(newVals) associates the new values with the original keys. The resulting list of values is added to the map using ++ which will replace the existing values with the new ones.

Original answer

val map = Map(
  "key1" -> "val1",
  "key2" -> "val2",
  "otherkey1" -> "otherval1"
)

val requiredVals = Map("key1"->"val1", "key2"->"val2")
val newVals = Map("newkey1" -> "newval1", "newkey2" -> "newval2")

val result =
  if (requiredVals.forall{ case (k, v) => map.get(k).exists(_.equalsIgnoreCase(v)) }) {
    map -- requiredVals.keys ++ newVals
  } else {
    map
  }

Note that this replaces the old keys with the new keys, which appears to be what is described. If you want to keep the original keys and values, just delete "-- requiredVals.keys" and it will add the new keys without removing the old ones.

4 Comments

@Tim could you give a little explanation for the answer. Also is there a way to use .map instead of forall?
@smaug I've added some explanation of my code to the answer. map and forall have rather different purposes. map is used to replace each value in a collection with a new value, and the result is a new collection. forall summarises all the values in the collection into a single value. Since we are doing a yes/no test, using forall is a more natural choice.
So if you will see the document of exists it says it might not terminate for infinitely bigger collection.
@RamanMishra I am calling exists on an Option which is O(1) and guaranteed to terminate.
0

You can use the following code:

val interestingMap = 
    if(map.getOrElse("key1", "") == "val1" && map.getOrElse("key2", "") == "val2")
      map - "key1" - "key2" + ("key1New" -> "val1New") + ("key2New" -> "val2New")
    else map

The check part(if statement) can be tweaked to suit your specific need.

if any of these key-value pairs are not present in the map, the original map will be returned, otherwise, you will get a new map with two updates at the requested keys.

Regarding efficiency, as long as there are only two keys to be updated, I do not think there is a real performance difference between using + to add elements directly and using ++ operator to overwrite the keys wholesale. If your map is huge though, maybe using a mutable map proves to be a better option in the long run.

4 Comments

You need to change the key names as well as the values, so both these answers need to be modified.
@jrook actually OP want to change value as well as key
Actually I wanted to keep the old key same (see comment on question). But even when the key is to be updated, I think this will work, by adding another map element instead of updating the old one. Thanks Raman and Tim for pointing it out.
@smaug, See the updated answer. I just added two drops to drop the old keys before the + operator. The general idea still works which is that you can simply play around with - and + operators to get what you want.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.