Replacing values from list of custom objects with map values - scala

I have a quite odd problem to solve, I have a String, a custom Type and a Map of Maps.
The string needs to have a few values replaced based on mapping between a value in custom type (which is a key in the map of maps).
This is the current structure:
case class Students(favSubject: String)
val mapping: Map[String, Map[String, String]] = Map("John" -> Map("English" -> "Soccer"))
val studentInfo: List[Students] = List(Students("English"))
val data: String = "John is the favourite hobby"
I tried the following:
mapping.foldLeft(data){ case (outputString, (studentName, favSubject)) => outputString.replace(studentName, favSubject.getOrElse(studentInfo.map(x => x.favSubject).toString, "")) }
What I need to get is:
"Soccer is the favourite hobby"
What I get is:
" is the favourite hobby"
So looks like I am getting the map of maps traversal right but the getOrElse part is having issues.

What I would do, would be to first change the structure of mappings so it makes more sense for the problem.
val mapping: Map[String, Map[String, String]] = Map("John" -> Map("English" -> "Soccer"))
val mapping2 =
mapping.iterator.flatMap {
case (student, map) => map.iterator.map {
case (info, value) => (info, student, value)
}
}.toList
.groupBy(_._1)
.view
.mapValues { group =>
group.iterator.map {
case (_, student, value) => student -> value
}.toList
}.toMap
// mapping2: Map[String, List[(String, String)]] = Map("English" -> List(("John", "Soccer")))
Then I would just traverse the students informativo, making all the necessary replacements.
final case class StudentInfo(favSubject: String)
val studentsInformation: List[StudentInfo] = List(StudentInfo("English"))
val data: String = "John is the favourite hobby"
val result =
studentsInformation.foldLeft(data) { (acc, info) =>
mapping2
.getOrElse(key = info.favSubject, default = List.empty)
.foldLeft(acc) { (acc2, tuple) =>
val (key, replace) = tuple
acc2.replace(key, replace)
}
}
// result: String = "Soccer is the favourite hobby"

When you map() a List, you get a List back. It's toString has a format "List(el1,el2,...)". Surely you cannot use it as a key for your sub-map, you would want just el1.
Here is a version of the working code. It might not be a solution you are looking for(!), just a solution to your question:
case class Students(favSubject: String)
val mapping: Map[String, Map[String, String]] = Map("John" -> Map("English" -> "Soccer"))
val studentInfo: List[Students] = List(Students("English"))
val data: String = "John is the favourite hobby"
val res = mapping.foldLeft(data) {
case (outputString, (studentName, favSubjectDict)) =>
outputString.replace(
studentName,
favSubjectDict.getOrElse(studentInfo.map(x => x.favSubject).head, "?")
)
}
println(s"$res") //prints "Soccer is the favourite hobby"
val notMatchingSubject = studentInfo.map(x => x.favSubject).toString
println(s"Problem in previous code: '$notMatchingSubject' !== 'English'")
Try it here: https://scastie.scala-lang.org/flQNRrUQSXWPxSTXOPPFgA
The issue
It is a bit unclear why StudentInfo is a List in this form... If I guessed, it was designed to be a list of StudentInfo containing both, name and favSubject and you would need to search it by name to find favSubject. But it is just a guess.
I went with the simplest working solution, to get a .head (first element) of the sequence from the map. Which will always be "English" even if you add more Studends to the list.

Related

Read Hocon config as a Map[String, String] with key in dot notation and value

I have following HOCON config:
a {
b.c.d = "val1"
d.f.g = "val2"
}
HOCON represents paths "b.c.d" and "d.f.g" as objects. So, I would like to have a reader, which reads these configs as Map[String, String], ex:
Map("b.c.d" -> "val1", "d.f.g" -> "val2")
I've created a reader and trying to do it recursively:
import scala.collection.mutable.{Map => MutableMap}
private implicit val mapReader: ConfigReader[Map[String, String]] = ConfigReader.fromCursor(cur => {
def concat(prefix: String, key: String): String = if (prefix.nonEmpty) s"$prefix.$key" else key
def toMap(): Map[String, String] = {
val acc = MutableMap[String, String]()
def go(
cur: ConfigCursor,
prefix: String = EMPTY,
acc: MutableMap[String, String]
): Result[Map[String, Object]] = {
cur.fluent.mapObject { obj =>
obj.value.valueType() match {
case ConfigValueType.OBJECT => go(obj, concat(prefix, obj.pathElems.head), acc)
case ConfigValueType.STRING =>
acc += (concat(prefix, obj.pathElems.head) -> obj.asString.right.getOrElse(EMPTY))
}
obj.asRight
}
}
go(cur, acc = acc)
acc.toMap
}
toMap().asRight
})
It gives me the correct result but is there a way to avoid MutableMap here?
P.S. Also, I would like to keep implementation by "pureconfig" reader.
The solution given by Ivan Stanislavciuc isn't ideal. If the parsed config object contains values other than strings or objects, you don't get an error message (as you would expect) but instead some very strange output. For instance, if you parse a typesafe config document like this
"a":[1]
The resulting value will look like this:
Map(a -> [
# String: 1
1
])
And even if the input only contains objects and strings, it doesn't work correctly, because it erroneously adds double quotes around all the string values.
So I gave this a shot myself and came up with a recursive solution that reports an error for things like lists or null and doesn't add quotes that shouldn't be there.
implicit val reader: ConfigReader[Map[String, String]] = {
implicit val r: ConfigReader[String => Map[String, String]] =
ConfigReader[String]
.map(v => (prefix: String) => Map(prefix -> v))
.orElse { reader.map { v =>
(prefix: String) => v.map { case (k, v2) => s"$prefix.$k" -> v2 }
}}
ConfigReader[Map[String, String => Map[String, String]]].map {
_.flatMap { case (prefix, v) => v(prefix) }
}
}
Note that my solution doesn't mention ConfigValue or ConfigReader.Result at all. It only takes existing ConfigReader objects and combines them with combinators like map and orElse. This is, generally speaking, the best way to write ConfigReaders: don't start from scratch with methods like ConfigReader.fromFunction, use existing readers and combine them.
It seems a bit surprising at first that the above code works at all, because I'm using reader within its own definition. But it works because the orElse method takes its argument by name and not by value.
You can do the same without using recursion. Use method entrySet as following
import scala.jdk.CollectionConverters._
val hocon =
"""
|a {
| b.c.d = "val1"
| d.f.g = val2
|}""".stripMargin
val config = ConfigFactory.load(ConfigFactory.parseString(hocon))
val innerConfig = config.getConfig("a")
val map = innerConfig
.entrySet()
.asScala
.map { entry =>
entry.getKey -> entry.getValue.render()
}
.toMap
println(map)
Produces
Map(b.c.d -> "val1", d.f.g -> "val2")
With given knowledge, it's possible to define a pureconfig.ConfigReader that reads Map[String, String] as following
implicit val reader: ConfigReader[Map[String, String]] = ConfigReader.fromFunction {
case co: ConfigObject =>
Right(
co.toConfig
.entrySet()
.asScala
.map { entry =>
entry.getKey -> entry.getValue.render()
}
.toMap
)
case value =>
//Handle error case
Left(
ConfigReaderFailures(
ThrowableFailure(
new RuntimeException("cannot be mapped to map of string -> string"),
Option(value.origin())
)
)
)
}
I did not want to write custom readers to get a mapping of key value pairs. I instead changed my internal data type from a map to list of pairs (I am using kotlin), and then I can easily change that to a map at some later internal stage if I need to. My HOCON was then able to look like this.
additionalProperties = [
{first = "sasl.mechanism", second = "PLAIN"},
{first = "security.protocol", second = "SASL_SSL"},
]
additionalProducerProperties = [
{first = "acks", second = "all"},
]
Not the best for humans... but I prefer it to having to build custom parsing components.

how to access key values within nested map in Scala

I have a List of Maps. One of the maps has another map inside it (2 level deep). I need to access some of keys from the inner most map and finally change the values. The issue I'm facing is to retrieve the keys from the inner most map. I'm very new to Scala and tried different things without luck.
I have flatten the List to Map and then tried to retrieve the key, values. The thing is, I can print the entire inner map, but not sure how to iterate thru that.
Below is the code: at a very basic, I would like to retrieve the values corresponding to keys from innermost map; say for the keys "isStudentLoankRequested", "schoolStructure".
object ListToMaps {
def main(args: Array[String]) {
val dataInputKeysListvar = List(Map("identityKeyName" -> "MY_ID", "identityKeyValue" -> "abcd-1234-xyz"),
Map("identityKeyName" -> "OUR_ID", "identityKeyValue" -> "1234567890",
"identityInformation" -> Map("writeFrequency" -> "NEVER", "studentStatus" -> "SEP", "annualValue" -> 0,
"schoolStructure" -> "OTHER", "studentType" -> "FTS", "occupationDescription" -> "other",
"studentAccountBalanceRange" -> "string", "isStudentLoankRequested" -> "N", "schoolName" -> "myschool",
"parentsIncome" -> 100)),
Map("identityKeyName" -> "FINAL_DECISION_KEY", "identityKeyValue" -> "0000-ABCD-4567-IJKL"))
val x = dataInputKeysListvar.flatten.toMap
val y = x("identityInformation")
if (x.contains("identityInformation")){
println("value of y is" + y)
}
}
}
As you can see from the print stmt, I can print the entire map of the inner most map, but need help in terms of iterating thru that.
If you know at compile time which fields and values you need to change.
You can hard code the logic, like this:
def fixData(data: List[Map[String, Any]]): List[Map[String, Any]] =
data.map { outerMap =>
outerMap.get("identityInformation") match {
case Some(innerMap) =>
// Put as many key pairs you want to change.
// Note: if the key does not exists it will be added!
val updatedInnerMap = innerMap.asInstanceOf[Map[String, String]] ++ Map(
"isStudentLoankRequested" -> "No"
)
outerMap + ("identityInformation" -> updatedInnerMap)
case None =>
outerMap
}
}
If the key-values to change are dynamic, and / or some inner keys may not exists, or if the level of nesting can go on.
You can do something like this:
def updateMap(map: Map[String, Any], updates: Map[String, Any]): Map[String, Any] =
map.map {
case (key, value: Map[_, _]) =>
updates.get(key) match {
case Some(innerUpdates : Map[_, _]) =>
key -> updateMap(
map = value.asInstanceOf[Map[String, Any]],
updates = innerUpdates.asInstanceOf[Map[String, Any]]
)
case Some(newValue) =>
key -> newValue
case None =>
key -> value
}
case (key, value) =>
key -> updates.getOrElse(key, default = value)
}
def fixData(data: List[Map[String, Any]], updates: Map[String, Any]): List[Map[String, Any]] =
data.map(outerMap => updateMap(outerMap, updates))
Note: The above snippets use "unsafe" techniques like asInstanceOf because we lost type safety the moment you got a Map[String, Any]. Always that I see such structure, I think of JSON. I would suggest you to use an appropriate library for managing such kind of data, like circe, instead of writing code as the above.

Create a list with empty map

I have a JSON string which is parsed and a typecaseted to a map. I'm using this map to get a List[Map[String, Any]]. Here to make my code error free I have used getOrElse while type casting.
JSON string looks similar to
{
"map-key" : [
{
"list-object-1-key" : "list-object-1-value"
},
{
"list-object-2-key" : "list-object-2-value"
},
]
}
My code
val json = JSON.parseFull(string) match {
case Some(e) =>
val list = e.asInstanceOf[Map[String, Any]]
.getOrElse("map-key", List[Map[String,Any]]) // Error here
val info = list.asInstanceOf[List[Map[String, Any]]]
//iterate over each element in the list and perform my operations
case None => string
}
I can understand that whenever there is no result present in list object then info object is repeated code.
How can I improve this programme by giving the default value to list object?
Do it in more functional way, without asInstanceOf:
val parsed = JSON.parseFull(string)
parsed match {
case Some(e: Map[String, Any]) =>
e.get("map-key") match {
case Some(a: List[Any]) =>
a.foreach {
case inner: Map[String, Any] => println(inner.toList)
}
case _ =>
}
case None => string
}
Your default value is wrong. You're passing a type, not an empty list.
e.asInstanceOf[Map[String, Any]].getOrElse("map-key", List.empty[Map[String,Any]])
Unfortunately i don't have the environment at this machine but try something like that
first thing you need to convert json to map
def jsonStrToMap(jsonStr: String): Map[String, Any] = {
implicit val formats = org.json4s.DefaultFormats
parse(jsonStr).extract[Map[String, Any]]
}
and the second thing you will need to iterate over map to get values of list
val list= jsonStrToMap.map{ case(k,v) => (k.getBytes, v) }. toList

Transform a list to a map

I have the following types:
case class Category(id: Int, name: String)
case class Product(id: Int, location: Location, categoryIdList: Option[List[Int]])
Given a list of products
val products:List[Product] = loadProducts()
How can I product a map of categoryId to Location?
Map[categoryId, Location]
So my method would look something like this:
def getMap(products: List[Product]): Map[Int, Location] = {
??
}
I need to somehow iterate over the optional list of categoryIdList and then create a map from that with the Location property.
In order to convert a Seq to a Map we need to first convert it to be a Seq[(Int,Location)], that is a Seq of a Tuple2. Only then will the toMap method actually be available.
Edit: Okay here's an implementation based on each categoryId on the list, note that you shouldn't use an option of a list, since an empty state for a List is just an empty list.
def getMap(products: List[Product]): Map[Int, Location] = {
products.flatMap(toListTuple2).toMap
}
def toListTuple2(product: Product): List[(Int, Location)] = {
product.categoryIdList
.getOrElse(List())
.map(category => (category, product.location))
}
So here we first turn our product into a list of categoryIds and Locations and then flatmap it to a List of (Int, Location), which can then be turned into a Map by calling toMap.
This should do what you are asking for however solution doesn't address problems provided in comments:
def getMap(products: List[Product]): Map[Int, Location] = {
val locations = scala.collection.mutable.Map[Int, Location]()
for {
product <- products
if product.categoryIdList.nonEmpty
category <- product.categoryIdList.get
} {
locations(category) = product.location
}
locations.toMap
}
def getMap(products: List[Product]) = {
products.map(p => (p.categoryIdList.getOrElse(List.empty), p.location))
.flatMap(x => x._1.map(_ -> x._2))
.toMap
}

How to avoid any mutable things in this builder?

I have a simple Scala class like this:
class FiltersBuilder {
def build(filter: CommandFilter) = {
val result = collection.mutable.Map[String, String]()
if (filter.activity.isDefined) {
result += ("activity" -> """ some specific expression """)
} // I well know that manipulating option like this is not recommanded,
//it's just for the simplicity of the example
if (filter.gender.isDefined) {
result += ("gender" -> """ some specific expression """)
}
result.toMap //in order to return an immutable Map
}
}
using this class so:
case class CommandFilter(activity: Option[String] = None, gender: Option[String] = None)
The result content depends on the nature of the selected filters and their associated and hardcoded expressions (String).
Is there a way to transform this code snippet by removing this "mutability" of the mutable.Map?
Map each filter field to a tuple while you add the result to a Seq, then filter out the Nones with flatten finally convert the Seq of tuples to a Map with toMap.
For adding more fields to filter you just have to add a new line to the Seq
def build(filter: CommandFilter) = {
// map each filter filed to the proper tuple
// as they are options, map will transform just the Some and let the None as None
val result = Seq(
filter.activity.map(value => "activity" -> s""" some specific expression using $value """),
filter.gender.map(value => "gender" -> s""" some specific expression using $value """)
).flatten // flatten will filter out all the Nones
result.toMap // transform list of tuple to a map
}
Hope it helps.
Gaston.
Since there are at most 2 elements in your Map:
val activity = filter.activity.map(_ => Map("activity" -> "xx"))
val gender = filter.gender.map(_ => Map("gender" -> "xx"))
val empty = Map[String, String]()
activity.getOrElse(empty) ++ gender.getOrElse(empty)
I've just managed to achieve it with this solution:
class FiltersBuilder(commandFilter: CommandFilter) {
def build = {
val result = Map[String, String]()
buildGenderFilter(buildActivityFilter(result))
}
private def buildActivityFilter(expressions: Map[String, String]) =
commandFilter.activity.fold(expressions)(activity => result + ("activity" -> """ expression regarding activity """))
private def buildGenderFilter(expressions: Map[String, String]) =
commandFilter.gender.fold(expressions)(gender => result + ("gender" -> """ expression regarding gender """))
}
Any better way?