How to map based on multiple lists with arbitrary elements? - scala

I have the following model:
case class Car(brand: String, year: Int, model: String, ownerId: String)
case class Person(firstName: String, lastName: String, id: String)
case class House(address: String, size: Int, ownerId: String)
case class Info(id: String, lastName: String, carModel: String, address: String)
I want to build a List[Info] based on the following lists:
val personL: List[Person] = List(Person("John", "Doe", "1"), Person("Jane", "Doe", "2"))
val carL: List[Car] = List(Car("Mercedes", 1999, "G", "1"), Car("Tesla", 2016, "S", "4"), Car("VW", 2015, "Golf", "2"))
val houseL: List[House] = List(House("Str. 1", 1000, "2"), House("Bvl. 3", 150, "8"))
The info should be gathered based on the personL, for example:
val info = personL.map { p =>
val car = carL.find(_.ownerId.equals(p.id))
val house = houseL.find(_.ownerId.equals(p.id))
val carModel = car.map(_.model)
val address = house.map(_.address)
Info(p.id, p.lastName, carModel.getOrElse(""), address.getOrElse(""))
}
Result:
info: List[Info] = List(Info(1,Doe,G,), Info(2,Doe,Golf,Str. 1))
Now I am wondering if there's an expression which is more concise than my map construct which solves exactly my problem.

Here is one option by building the maps from ownerid to model and address firstly, and then look up the info while looping through the person List:
val carMap = carL.map(car => car.ownerId -> car.model).toMap
// carMap: scala.collection.immutable.Map[String,String] = Map(1 -> G, 4 -> S, 2 -> Golf)
val addrMap = houseL.map(house => house.ownerId -> house.address).toMap
// addrMap: scala.collection.immutable.Map[String,String] = Map(2 -> Str. 1, 8 -> Bvl. 3)
personL.map(p => Info(p.id, p.lastName, carMap.getOrElse(p.id, ""), addrMap.getOrElse(p.id, "")))
// res3: List[Info] = List(Info(1,Doe,G,), Info(2,Doe,Golf,Str. 1))

I would say use for comprehensions. If you need exactly that result which in that case would resemble a left join then the for comprehension is still ugly:
for {
person <- persons
model <- cars.find(_.ownerId == person.id).map(_.model).orElse(Some("")).toList
address <- houses.find(_.ownerId == person.id).map(_.address).orElse(Some("")).toList
} yield Info(person.id, person.lastName, model, address)
Note that you can remove the .toList call in this exceptional case as the two Option generators appear after the collection generators.
If you can sacrifice the default model / address values then it looks simple enough:
for {
person <- persons
car <- cars if car.ownerId == person.id
house <- houses if house.ownerId == person.id
} yield Info(person.id, person.lastName, car.model, car.address)
Hope that helps.

May be converting the individual lists in hashmaps with a map function and look up by key instead of iterating all those lists for every element of person might help?

Related

replace list element with another and return the new list

I am kind of stuck with this, and I know this is a bloody simple question :(
I have a case class such as:
case class Students(firstName: String, lastName: String, hobby: String)
I need to return a new list but change the value of hobby based on Student name. For example:
val classToday = List(Students("John","Smith","Nothing"))
Say if student name is John I want to change the hobby to Soccer so the resulting list should be:
List(Students("John","Smith","Soccer")
I think this can be done via map? I have tried:
classToday.map(x => if (x.firstName == "John") "Soccer" else x)
This will just replace firstName with Soccer which I do not want, I tried setting the "True" condition to x.hobby == "Soccer" but that does not work.
I think there is a simple solution to this :(
The lambda function in map has to return a Students value again, not just "Soccer". For example, if you had to replace everyone's hobbies with "Soccer", this is not right:
classToday.map(x => "Soccer")
What you want is the copy function:
classToday.map(x => x.copy(hobby = "Soccer"))
Or for the original task:
classToday.map(x => if (x.firstName == "John") x.copy(hobby = "Soccer") else x)
You can use pattern-matching syntax to pretty up this type of transition.
val newList = classToday.map{
case s#Students("John",_,_) => s.copy(hobby = "Soccer")
case s => s
}
I suggest to make it more generic, you can create a map of names to hobbies:
For example:
val firstNameToHobby = Map("John" -> "Soccer", "Brad" -> "Basketball")
And use it as follows:
case class Students(firstName: String, lastName: String, hobby: String)
val classToday = List(Students("John","Smith","Nothing"), Students("Brad","Smith","Nothing"))
val result = classToday.map(student => student.copy(hobby = firstNameToHobby.getOrElse(student.firstName, "Nothing")))
// result = List(Students(John,Smith,Soccer), Students(Brad,Smith,Basketball))
It would be better if you can create a mapping between the firstName of the student with Hobby, then you can use it like this:
scala> val hobbies = Map("John" -> "Soccer", "Messi" -> "Soccer", "Williams" -> "Cricket")
hobbies: scala.collection.immutable.Map[String,String] = Map(John -> Soccer, Messi -> Soccer, Williams -> Cricket)
scala> case class Student(firstName: String, lastName: String, hobby: String)
defined class Student
scala> val students = List(Student("John", "Smith", "Nothing"), Student("Williams", "Lopez", "Nothing"), Student("Thomas", "Anderson", "Nothing"))
students: List[Student] = List(Student(John,Smith,Nothing), Student(Williams,Lopez,Nothing), Student(Thomas,Anderson,Nothing))
scala> students.map(student => student.copy(hobby = hobbies.getOrElse(student.firstName, "Nothing")))
res2: List[Student] = List(Student(John,Smith,Soccer), Student(Williams,Lopez,Cricket), Student(Thomas,Anderson,Nothing))

Zip two lists based on specific condition in Scala

I have two defined case classes and two lists like the following code.
case class Person(name: String, company: String, rank: Int, id: Long)
case class Employee(company: String, rank: Int, id: Long)
val persons = List(Person("Tom", "CompanyA", 1, null), Person("Jenny", "CompanyB", 1, null), Person("James", "CompanyA", 2, null))
val employees = List(Employee("CompanyA", 1, 1001), Employee("CompanyB", 1, 1002), Employee("CompanyA", 2, 1003))
since the combination of company and rank is unique, I want to use the information in employees so that I can combine the two lists into the following one (A list of Person with id fulfilled).
[Person("Tom", "CompanyA", 1, 1001), Person("Jenny", "CompanyB", 1, 1002), Person("James", "CompanyA", 2, 1003)]
I tried to implement it as this:
zipBasedOnCondition(persons, employees, (person, employee) => person.name == employee.name && person.rank === employee.rank)
However, I failed to come up with a solution to implement the zipBasedOnCondition function
Is there any solution to combine the two lists?
What you want can be achieved by:
for {
person <- persons
employee <- employees
if person.name == employee.name && person.rank === employee.rank
} yield person.copy(id = employee.id)
It has time complexity of O(persons.size*employees.size) but since List has no guarantees about things inside being sorted (and especially, being sorted by the things you want to compare against) you cannot optimize it anymore.
If you want, you could modify it so that it would took the first one of possible pairs, though how is beyond the scope of "zip with condition".
I'm still thinking of better solutions... but this should work:
def f(c: String): Option[Employee] = employees.filter(_.company == c).headOption
for {
p <- persons
e <- f(p.company)
} yield {
p.copy(id = e.id)
}
This would be a pretty generic approach:
def zipBasedOnCondition[A, B, C](as: List[A], bs: List[B], pred: (A, B) => Boolean, f: (A, B) => C): List[C] = {
as.map(a => f(a, bs.filter(b => pred(a, b)).head))
}
then you could call it like this:
zipBasedOnCondition[Person, Employee, Person](persons, employees, (p, e) => p.name == e.name && p.rank == e.rank, (a, b) => a.copy(id = b.id))
The implementation of zipBasedOnCondition would need improvement since it assumes that for every person object there is a corresponding employee object.
You are providing id fied as mandatory field in Person class and providing null value in person list that will give error.
First let's correct your Person class.
case class Person(name: String, company: String, rank: Int, id: Long = 0)
Now, Solution to your question.
def combineList( list1: List[Person], list2: List[Employee]): List[(Person)] = {
(for{
a <- list1
b <- list2
if (a.company == b.company && a.rank == b.rank)
} yield (a.copy(id = b.id)))
}
Output
List(Person(Tom,CompanyA,1,1001), Person(Jenny,CompanyB,1,1002), Person(James,CompanyA,2,1003))

Scala map function to remove fields

I have a list of Person objects with many fields and I can easily do:
list.map(person => person.getName)
In order to generate another collection with all the peoples names.
How can you use the map function to create a new collection with all the fields of the Person class, BUT their name though?
In other words, how can you create a new collection out of a given collection which will contain all the elements of your initial collection with some of their fields removed?
You can use unapply method of your case class to extract the members as tuple then remove the things that you don't want from the tuple.
case class Person(name: String, Age: Int, country: String)
// defined class Person
val personList = List(
Person("person_1", 20, "country_1"),
Person("person_2", 30, "country_2")
)
// personList: List[Person] = List(Person(person_1,20,country_1), Person(person_2,30,country_2))
val tupleList = personList.flatMap(person => Person.unapply(person))
// tupleList: List[(String, Int, String)] = List((person_1,20,country_1), (person_2,30,country_2))
val wantedTupleList = tupleList.map({ case (name, age, country) => (age, country) })
// wantedTupleList: List[(Int, String)] = List((20,country_1), (30,country_2))
// the above is more easy to understand but will cause two parses of list
// better is to do it in one parse only, like following
val yourList = personList.flatMap(person => {
Person.unapply(person) match {
case (name, age, country) => (age, country)
}
})
// yourList: List[(Int, String)] = List((20,country_1), (30,country_2))

Expanding a list of cars by a child collection as the key

I have a list of cars:
val cars = List(car1, car2, car3, car4, car5)
case class car(model: String, age: Int, partIds: Seq[String])
I now want to transform this list into a Map, where the key is the partId and the value is all the cars with that part.
val partMap: Map[String, List[Car]]
You will need to jump through some hoops by using intermediate types. The solution is to first get from your List[Car] into to List[PartId -> Car]. Dropping the Seq of parts makes your life easer. You can group your cars easily.
The mapValues is a function on Map. It will iterate over every tuple and will require some function that takes a type equal to the value of your Map...in my case before mapValues I had a Map[String, List[String -> Car]].
The mapValues wants a function with the signature (carMapping : List[(String, Car]) : A ... our desired type is of course List[Car]
here is a something on groupBy and a little about mapValues: http://markusjais.com/the-groupby-method-from-scalas-collection-library/
case class Car(model: String, age: Int, partIds: Seq[String])
object ListToMap extends App {
val car1 = Car("corolla", 1, Seq("b"))
val car2 = Car("camry", 5, Seq("b", "c"))
val car3 = Car("fit", 6, Seq("e"))
val car4 = Car("prelude", 2, Seq("e", "f"))
val car5 = Car("cobalt", 10, Seq("j"))
val cars = List(car1, car2, car3, car4, car5)
//For Every Car lets make the list of Tuples for PartId -> Car
def partMapping(car : Car) : Seq[(String, Car)] = car.partIds.map(part => part -> car)
def toPartMap(cars : List[Car]) : Map[String, List[Car]] =
cars
//Get the List of Tuples PartId -> Car and then flatten the internal list (same as map().flatten)
.flatMap(partMapping)
// group all the tuples by the partId
.groupBy(_._1)
// We currently have a Map[String, List[(partId -> Car)]] we need to clean that up a bit to remove the partId
.mapValues( carMapping => carMapping.map(_._2))
toPartMap(cars).foreach(println)
}
cars flatMap ( x => x.partIds map ((_, x))) groupBy (_._1) mapValues (_ map (_._2))

How to denormalize a nested Map in Scala, with the denormalized string containing only values?

I am new to Scala and FP in general and wanted to know how a nested collection can be denormalized in Scala. For example, if we had data of a question paper for an exam as:
Map("subject" -> "science", "questionCount" -> 10, "questions" ->
Map("1" ->
Map("ques" -> "What is sun?", "answers" ->
Map("1" -> "Star", "2" -> "Giant bulb", "3" -> "planet")
), "2" ->
Map("ques" -> "What is moon?", "answers" ->
Map("1" -> "Giant rock", "2" -> "White lamp", "3" -> "Planet")
)
)
)
When denormalized into strings, using only the values, it can be written as:
science,2,1,What is sun?,Star
science,2,1,What is sun?,Giant bulb
science,2,1,What is sun?,planet
science,2,2,What is moon?,Giant rock
science,2,2,What is moon?,White lamp
science,2,2,What is moon?,Plane
I understand that I can use map to process each item in a collection, it returns exactly the same number of items. Also, while flatMap can be used to process each item into multiple items and return more items than in the collection, I am unable to understand how I can do the denormalization using it.
Also, is it possible to do a partial denormalization like this?
science,2,1,What is sun?,[Star,Giant bulb,planet]
science,2,2,What is moon?,[Giant rock,White lamp,Planet]
Working with nested maps of type Map[String, Any] will never be pretty because you always have to cast the nested values.
If you want a String for every answer, you can flatMap over the questions and then map over the answers.
def normalize(map: Map[String, Any]): Seq[String] = {
val subject = map("subject")
val questions = map("questions").asInstanceOf[Map[String, Any]]
val size = questions.size // or map("questionCount") ?
val lines = questions.flatMap { case (id, q) =>
val question = q.asInstanceOf[Map[String, Any]]
val answers = question("answers").asInstanceOf[Map[String, Any]]
answers.values.map(answer =>
List(subject, size, id, question("ques"), answer) mkString ","
)
}
lines.toSeq
}
If you want a String per question (partial denormalization), you can just map over the questions.
def seminormalize(map: Map[String, Any]): Seq[String] = {
val subject = map("subject")
val questions = map("questions").asInstanceOf[Map[String, Any]]
val size = questions.size // or map("questionCount") ?
val lines = questions.map { case (id, q) =>
val question = q.asInstanceOf[Map[String, Any]]
val answers = question("answers").asInstanceOf[Map[String, Any]]
List(subject, size, id, question("ques"), answers.values.mkString("[", "," , "]")) mkString ","
}
lines.toSeq
}
We could use normalize and seminormalize as :
scala> normalize(map).foreach(println)
science,2,1,What is sun?,Star
science,2,1,What is sun?,Giant bulb
science,2,1,What is sun?,planet
science,2,2,What is moon?,Giant rock
science,2,2,What is moon?,White lamp
science,2,2,What is moon?,Planet
scala> seminormalize(map).foreach(println)
science,2,1,What is sun?,[Star,Giant bulb,planet]
science,2,2,What is moon?,[Giant rock,White lamp,Planet]
In the question entries of Map used differently:
Here they have to be sequences: Map("subject" -> "science", "questionCount" -> 10, ...
Here they have to create different rows: Map("1" -> "Star", "2" -> "Giant bulb",...
So, first I suggest to introduce notion of Sequence and notion of Choice
Then I would implement denormalization as follows:
object Runner3 extends App {
case class Sequence(items: List[(String, Any)])
case class Choices(items: List[Any])
val source = Sequence(List(
"subject" -> "science",
"questionCount" -> 2,
"questions" -> Choices(List(
Sequence(List(
"ques" -> "What is sun?",
"answers" -> Choices(List("Star", "Giant bulb", "planet")))),
Sequence(List(
"ques" -> "What is moon?",
"answers" -> Choices(List("Giant rock", "White lamp", "Planet"))))))))
def denormalize(seq: Sequence): List[List[String]] =
seq.items.map {
case (_, choices: Choices) => denormalize(choices)
case (_, otherValue) => List(List(otherValue.toString))
}.reduceLeft(
(list1: List[List[String]], list2: List[List[String]]) =>
for(item1 <- list1; item2 <- list2) yield item1 ::: item2
)
def denormalize(choices: Choices): List[List[String]] =
choices.items.flatMap {
case seq: Sequence => denormalize(seq)
case otherValue => List(List(otherValue.toString))
}
denormalize(source).foreach(line => println(line.mkString(",")))
}