Table of Contents
This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 11.23, “How to Sort an Existing Scala Map by Key or Value”
Problem
You have an unsorted Scala Map and want to sort the elements in the map by the key or value.
Solution
Given a basic, immutable Map
:
scala> val grades = Map("Kim" -> 90, | "Al" -> 85, | "Melissa" -> 95, | "Emily" -> 91, | "Hannah" -> 92 | ) grades: scala.collection.immutable.Map[String,Int] = Map(Hannah -> 92, Melissa -> 95, Kim -> 90, Emily -> 91, Al -> 85)
You can sort the map by key, from low to high, using sortBy
:
scala> import scala.collection.immutable.ListMap import scala.collection.immutable.ListMap scala> ListMap(grades.toSeq.sortBy(_._1):_*) res0: scala.collection.immutable.ListMap[String,Int] = Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95)
You can also sort the keys in ascending or descending order using sortWith:
// low to high scala> ListMap(grades.toSeq.sortWith(_._1 < _._1):_*) res0: scala.collection.immutable.ListMap[String,Int] = Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95) // high to low scala> ListMap(grades.toSeq.sortWith(_._1 > _._1):_*) res1: scala.collection.immutable.ListMap[String,Int] = Map(Melissa -> 95, Kim -> 90, Hannah -> 92, Emily -> 91, Al -> 85)
You can sort the map by value using sortBy
:
scala> ListMap(grades.toSeq.sortBy(_._2):_*) res0: scala.collection.immutable.ListMap[String,Int] = Map(Al -> 85, Kim -> 90, Emily -> 91, Hannah -> 92, Melissa -> 95)
You can also sort by value in ascending or descending order using sortWith
:
// low to high scala> ListMap(grades.toSeq.sortWith(_._2 < _._2):_*) res0: scala.collection.immutable.ListMap[String,Int] = Map(Al -> 85, Kim -> 90, Emily -> 91, Hannah -> 92, Melissa -> 95) // high to low scala> ListMap(grades.toSeq.sortWith(_._2 > _._2):_*) res1: scala.collection.immutable.ListMap[String,Int] = Map(Melissa -> 95, Hannah -> 92, Emily -> 91, Kim -> 90, Al -> 85)
In all of these examples, you’re not sorting the existing map; the sort methods result in a new sorted map, so the output of the result needs to be assigned to a new variable.
Also, you can use either a ListMap
or a LinkedHashMap
in these recipes. This example shows how to use a LinkedHashMap
and assign the result to a new variable:
scala> val x = collection.mutable.LinkedHashMap(grades.toSeq.sortBy(_._1):_*) x: scala.collection.mutable.LinkedHashMap[String,Int] = Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95) scala> x.foreach(println) (Al,85) (Emily,91) (Hannah,92) (Kim,90) (Melissa,95)
Discussion
To understand these solutions, it’s helpful to break them down into smaller pieces. First, start with the basic immutable Map
:
scala> val grades = Map("Kim" -> 90, | "Al" -> 85, | "Melissa" -> 95, | "Emily" -> 91, | "Hannah" -> 92 | ) grades: scala.collection.immutable.Map[String,Int] = Map(Hannah -> 92, Melissa -> 95, Kim -> 90, Emily -> 91, Al -> 85)
Next, this is what grades.toSeq
looks like:
scala> grades.toSeq res0: Seq[(String, Int)] = ArrayBuffer((Hannah,92), (Melissa,95), (Kim,90), (Emily,91), (Al,85))
You make the conversion to a Seq
because it has sorting methods you can use:
scala> grades.toSeq.sortBy(_._1) res0: Seq[(String, Int)] = ArrayBuffer((Al,85), (Emily,91), (Hannah,92), (Kim,90), (Melissa,95)) scala> grades.toSeq.sortWith(_._1 < _._1) res1: Seq[(String, Int)] = ArrayBuffer((Al,85), (Emily,91), (Hannah,92), (Kim,90), (Melissa,95))
Once you have the map data sorted as desired, store it in a ListMap
to retain the sort order:
scala> ListMap(grades.toSeq.sortBy(_._1):_*) res0: scala.collection.immutable.ListMap[String,Int] = Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95)
The LinkedHashMap
also retains the sort order of its elements, so it can be used in all of the examples as well:
scala> import scala.collection.mutable.LinkedHashMap import scala.collection.mutable.LinkedHashMap scala> LinkedHashMap(grades.toSeq.sortBy(_._1):_*) res0: scala.collection.mutable.LinkedHashMap[String,Int] = Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95)
There are both mutable and immutable versions of a ListMap
, but LinkedHashMap
is only available as a mutable class. Use whichever is best for your situation.
About that _*
The _*
portion of the code takes a little getting used to. It’s used to convert the data so it will be passed as multiple parameters to the ListMap
or LinkedHashMap
. You can see this a little more easily by again breaking down the code into separate lines. The sortBy
method returns a Seq[(String, Int)]
, i.e., a sequence of tuples:
scala> val x = grades.toSeq.sortBy(_._1) x: Seq[(String, Int)] = ArrayBuffer((Al,85), (Emily,91), (Hannah,92), (Kim,90), (Melissa,95))
You can’t directly construct a ListMap
with a sequence of tuples, but because the apply
method in the ListMap
companion object accepts a Tuple2
varargs parameter, you can adapt x
to work with it, i.e., giving it what it wants:
scala> ListMap(x: _*) res0: scala.collection.immutable.ListMap[String,Int] = Map(Al -> 85, Emily -> 91, Hannah -> 92, Kim -> 90, Melissa -> 95)
Attempting to create the ListMap
without using this approach results in an error:
scala> ListMap(x) <console>:16: error: type mismatch; found : Seq[(String, Int)] required: (?, ?) ListMap(x) ^
Another way to see how _*
works is to define your own method that takes a varargs parameter. The following printAll
method takes one parameter, a varargs field of type String
:
def printAll(strings: String*) { strings.foreach(println) }
If you then create a List
like this:
// a sequence of strings val fruits = List("apple", "banana", "cherry") you won’t be able to pass that `List` into printAll; it will fail like the previous example: scala> printAll(fruits) <console>:20: error: type mismatch; found : List[String] required: String printAll(fruits) ^
But you can use _*
to adapt the List
to work with printAll
, like this:
// this works printAll(fruits: _*)
If you come from a Unix background, it may be helpful to think of _*
as a “splat” operator. This operator tells the compiler to pass each element of the sequence to printAll
as a separate argument, instead of passing fruits as a single List
argument.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |