I was trying to recursively create a key, value pair map from a hierarchical case class structure, but no luck.
case class Country(id: Option[Long], name: String, states: Option[List[State]])
case class State(id: Option[Long], name: String, cities: Option[List[City]])
case class City(id: Option[Long], name: String)
So I was trying to extract that in Lists and zip to get the key value pair, then trying to do some recursive computation to obtain the same in the Lists of objects.
val keys = country.getClass.getDeclaredFields.map(f => f.getName)
val values = country.productIterator
val m = (keys zip values.toList).toMap
This gives me 3 keys with their values.
My problem is to solve the recursive way in the List[City] inside State and List[State] inside Country.
Is there any good way to do that? I can't think in any good solution. I was trying to iterate over the map, matching where there is a List and iterate and trying to save this progress in a BufferList.
Has anyone trying to do something like this?
EDIT
I wasn't specific about the desirable output.
I want the child to be other maps, so getting the example from #Tienan Ren I tried to do something like this:
def toMap[T: TypeTag](clazz: scala.Product): Map[String, Any] = {
def recursion(key: String, list: List[T]): Map[String, Any] = {
list.toMap
}
val keys = clazz.getClass.getDeclaredFields.map(_.getName)
val values = (keys zip clazz.productIterator.toList) map {
case (key, value: List[T]) => recursion(key, value)
case (key, value) => (key, value)
}
values.toMap
}
Where the recursion should receive the list and back a Map.
Not sure why would you use Option[List[..]], empty list should be capable of representing the None case. Here is my implementation after removing Option for Lists.
case class Country(id: Option[Long], name: String, states: List[State])
case class State(id: Option[Long], name: String, cities: List[City])
case class City(id: Option[Long], name: String)
The toMap function:
import reflect.runtime.universe._
def toMap[T: TypeTag](c: scala.Product): Map[String, Any] = {
val keys = c.getClass.getDeclaredFields.map(_.getName)
val z = (keys zip c.productIterator.toList) map {
case (key, value: List[T]) if typeOf[T] <:< typeOf[scala.Product] =>
(key, value.map(v => toMap(v.asInstanceOf[scala.Product])))
case (key, value) => (key, value)
}
z.toMap
}
Output:
val country = Country(None, "US", List(State(None, "CA", City(None, "LA") :: Nil)))
println(toMap(country))
This gives you:
Map(id -> None, name -> US, states -> List(Map(id -> None, name -> CA, cities -> List(Map(id -> None, name -> LA)))))
Related
I have the following 3 case classes:
case class Profile(name: String,
age: Int,
bankInfoData: BankInfoData,
userUpdatedFields: Option[UserUpdatedFields])
case class BankInfoData(accountNumber: Int,
bankAddress: String,
bankNumber: Int,
contactPerson: String,
phoneNumber: Int,
accountType: AccountType)
case class UserUpdatedFields(contactPerson: String,
phoneNumber: Int,
accountType: AccountType)
this is just enums, but i added anyway:
sealed trait AccountType extends EnumEntry
object AccountType extends Enum[AccountType] {
val values: IndexedSeq[AccountType] = findValues
case object Personal extends AccountType
case object Business extends AccountType
}
my task is - i need to write a funcc Profile and compare UserUpdatedFields(all of the fields) with SOME of the fields in BankInfoData...this func is to find which fields where updated.
so I wrote this func:
def findDiff(profile: Profile): Seq[String] = {
var listOfFieldsThatChanged: List[String] = List.empty
if (profile.bankInfoData.contactPerson != profile.userUpdatedFields.get.contactPerson){
listOfFieldsThatChanged = listOfFieldsThatChanged :+ "contactPerson"
}
if (profile.bankInfoData.phoneNumber != profile.userUpdatedFields.get.phoneNumber) {
listOfFieldsThatChanged = listOfFieldsThatChanged :+ "phoneNumber"
}
if (profile.bankInfoData.accountType != profile.userUpdatedFields.get.accountType) {
listOfFieldsThatChanged = listOfFieldsThatChanged :+ "accountType"
}
listOfFieldsThatChanged
}
val profile =
Profile(
"nir",
34,
BankInfoData(1, "somewhere", 2, "john", 123, AccountType.Personal),
Some(UserUpdatedFields("lee", 321, AccountType.Personal))
)
findDiff(profile)
it works, but wanted something cleaner..any suggestions?
Each case class extends Product interface so we could use it to convert case classes into sets of (field, value) elements. Then we can use set operations to find the difference. For example,
def findDiff(profile: Profile): Seq[String] = {
val userUpdatedFields = profile.userUpdatedFields.get
val bankInfoData = profile.bankInfoData
val updatedFieldsMap = userUpdatedFields.productElementNames.zip(userUpdatedFields.productIterator).toMap
val bankInfoDataMap = bankInfoData.productElementNames.zip(bankInfoData.productIterator).toMap
val bankInfoDataSubsetMap = bankInfoDataMap.view.filterKeys(userUpdatedFieldsMap.keys.toList.contains)
(bankInfoDataSubsetMap.toSet diff updatedFieldsMap.toSet).toList.map { case (field, value) => field }
}
Now findDiff(profile) should output List(phoneNumber, contactPerson). Note we are using productElementNames from Scala 2.13 to get the filed names which we then zip with corresponding values
userUpdatedFields.productElementNames.zip(userUpdatedFields.productIterator)
Also we rely on filterKeys and diff.
A simple improvement would be to introduce a trait
trait Fields {
val contactPerson: String
val phoneNumber: Int
val accountType: AccountType
def findDiff(that: Fields): Seq[String] = Seq(
Some(contactPerson).filter(_ != that.contactPerson).map(_ => "contactPerson"),
Some(phoneNumber).filter(_ != that.phoneNumber).map(_ => "phoneNumber"),
Some(accountType).filter(_ != that.accountType).map(_ => "accountType")
).flatten
}
case class BankInfoData(accountNumber: Int,
bankAddress: String,
bankNumber: Int,
contactPerson: String,
phoneNumber: Int,
accountType: String) extends Fields
case class UserUpdatedFields(contactPerson: String,
phoneNumber: Int,
accountType: AccountType) extends Fields
so it was possible to call
BankInfoData(...). findDiff(UserUpdatedFields(...))
If you want to further-improve and avoid naming all the fields multiple times, for example shapeless could be used to do it compile time. Not exactly the same but something like this to get started. Or use reflection to do it runtime like this answer.
That would be a very easy task to achieve if it would be an easy way to convert case class to map. Unfortunately, case classes don't offer that functionality out-of-box yet in Scala 2.12 (as Mario have mentioned it will be easy to achieve in Scala 2.13).
There's a library called shapeless, that offers some generic programming utilities. For example, we could write an extension function toMap using Record and ToMap from shapeless:
object Mappable {
implicit class RichCaseClass[X](val x: X) extends AnyVal {
import shapeless._
import ops.record._
def toMap[L <: HList](
implicit gen: LabelledGeneric.Aux[X, L],
toMap: ToMap[L]
): Map[String, Any] =
toMap(gen.to(x)).map{
case (k: Symbol, v) => k.name -> v
}
}
}
Then we could use it for findDiff:
def findDiff(profile: Profile): Seq[String] = {
import Mappable._
profile match {
case Profile(_, _, bankInfo, Some(userUpdatedFields)) =>
val bankInfoMap = bankInfo.toMap
userUpdatedFields.toMap.toList.flatMap{
case (k, v) if bankInfoMap.get(k).exists(_ != v) => Some(k)
case _ => None
}
case _ => Seq()
}
}
I have the following case class in Scala:
case class Profile(userId: String, items: Map[String, Seq[Item]], subParts: Seq[SubPartAndCount], usedItems: Map[String, Long])
The SubPartandCount looks like this:
case class SubPartAndCount(subPart: String, subCount: Int)
So I want to filter a list of Profiles by the value of subPart within the SubPartAndCount case classes that are in the subParts list. I have been trying to figure this out with no success! Any help would be much appreciated!
Use .exists(A => Boolean) nested in a .filter(B => Boolean):
profileList.filter(_.subParts.exists(_.subPart == "Part A"))
Here is an example:
type Item = String
case class Profile(userId: String, items: Map[String, Seq[Item]], subParts: Seq[SubPartAndCount], usedItems: Map[String, Long])
case class SubPartAndCount(subPart: String, subCount: Int)
val subPartA = SubPartAndCount("Part A", 100)
val subPartB = SubPartAndCount("Part B", 200)
val exampleItems = Map("Hello" -> Seq("World", "Galaxy", "Universe"))
val exampleUsedItems = Map("Hello" -> 1L, "Goodbye" -> 2L)
val profileList = Seq(
Profile("good", exampleItems, Seq(subPartA, subPartB), exampleUsedItems),
Profile("bad", exampleItems, Seq(subPartB, subPartB), exampleUsedItems),
Profile("good", exampleItems, Seq(subPartB, subPartA), exampleUsedItems)
)
profileList.filter(_.subParts.exists(_.subPart == "Part A"))
Try it out!
This statement evaluates to a Seq of the two "good" Profiles.
Hope this helps.
I am trying to parse a csv row here and each field can be a different type. To handle the error accumulation I am using Either[String, B] where the String is an error message and B is the value. The issue here is that B can be different types, Option[Int], String, Array[String], resulting in my Map being type (String, Either[String,java.io.Serializable]) effectively making the Map unreusable. Is there a way (I'm definitely sure there is) to more elegantly accumulate errors while also reusing those values to populate properties on an object?
override def parseCsv(implicit fields: Map[String, String]): Either[String, User] = {
val parsedValues = Map(Headers.Id -> getFieldAsString(Headers.Id),
Headers.FirstName -> getFieldAsString(Headers.FirstName),
Headers.LastName -> getFieldAsString(Headers.LastName),
Headers.Email -> getFieldAsString(Headers.Email),
Headers.TimeZone -> getFieldAsString(Headers.TimeZone),
Headers.Region -> getOption(Headers.Region),
Headers.Phone -> getOption(Headers.Phone),
Headers.ProfileImage -> getFieldAsString(Headers.ProfileImage),
Headers.Roles -> getFieldAsArray(Headers.Roles))
val errors = parsedValues.collect { case (key, Left(errors)) => errors }
if (!errors.isEmpty) Left(errors.mkString(", "))
else {
val user = new User
user.id = getFieldAsString(Headers.Id).right.get
user.firstName = getFieldAsString(Headers.FirstName).right.get
user.lastName = getFieldAsString(Headers.LastName).right.get
user.email = getFieldAsString(Headers.Email).right.get
user.timeZone = getFieldAsString(Headers.TimeZone).right.get
user.phoneNumber = (for {
region <- getOption(Headers.Region).right.get
phone <- getOption(Headers.Phone).right.get
_ = validatePhoneNumber(phone, region)
} yield {
new PhoneNumber(region, phone)
}).orNull
user.profileImageUrl = getFieldAsString(Headers.ProfileImage).right.get
user.roles = getFieldAsArray(Headers.Roles).right.get
Right(user)
}
}
Create case classes for all types of Bs. These case classes must extend some common trait. While populating the user object just pattern match and retrieve values.
sealed trait Result {
val paramName: String
}
case class OptionInt(override val paramName: String, value: Option[Int]) extends Result
case class ArrayString(override val paramName: String, value: Array[String]) extends Result
case class StringValue(override val paramName: String, value: String) extends Result
now the final type would be like Either[String, Result]
after parsing the whole file create a List[Result]
If you are expecting age as Option[Int] and firstName as String then do this
list.foreach { result =>
result match {
case Option("age", value) => userId.age = value.getOrElse(defaultAge)
case StringValue("firstName", value) => userId.firstName = value
case StringValue("email", value) => userId.email = value
case _ => //do nothing
}
}
I have the followeing classes:
case class RolepermissionRow(roleid: Int, permissionid: Int)
and
case class RolePermissionJSON(
var id: Option[Long],
var num: Option[Int],
var name: Option[String],
var perms: Map[String, Boolean])
I create a map:
var s = Map("1" -> true)
I create a RolePermissionJSON:
val f = RolePermissionJSON(Some(0), Some(0), Some('test'), s)
And I would like to convert the perms Map to RolepermissionRow using the following code:
scala> f.perms.map { case (key, value) => if (value) RolepermissionRow(key.toInt, 1) }.toSeq
res7: Seq[Any] = List(RolepermissionRow(1,1))
The result is Seq[Any] but I would like to have Seq[RolepermissionRow]
Simple changes required:
val z = f.perms.map { case (key, value) if (value) => RolepermissionRow(key.toInt, 1) }.toSeq
Now description:
In your code resulted Seq contains of RolepermissionRows if value is true and Units otherwise.
You should filter out "empty" elements of map that gives you Unit.
UPD:
#nafg advice to use collect to prevent match error in runtime.
f.perms.filter(_._2).map{case (key, value) => RolepermissionRow(key.toInt, 1) }.toSeq
I think you should use filter firstly to filter the map avoid empty Map.
Is it possible to map the key value pairs of a Map to a Scala constructor with named parameters?
That is, given
class Person(val firstname: String, val lastname: String) {
...
}
... how can I create an instance of Person using a map like
val args = Map("firstname" -> "John", "lastname" -> "Doe", "ignored" -> "value")
What I am trying to achieve in the end is a nice way of mapping Node4J Node objects to Scala value objects.
The key insight here is that the constructor arguments names are available, as they are the names of the fields created by the constructor. So provided that the constructor does nothing with its arguments but assign them to fields, then we can ignore it and work with the fields directly.
We can use:
def setFields[A](o : A, values: Map[String, Any]): A = {
for ((name, value) <- values) setField(o, name, value)
o
}
def setField(o: Any, fieldName: String, fieldValue: Any) {
// TODO - look up the class hierarchy for superclass fields
o.getClass.getDeclaredFields.find( _.getName == fieldName) match {
case Some(field) => {
field.setAccessible(true)
field.set(o, fieldValue)
}
case None =>
throw new IllegalArgumentException("No field named " + fieldName)
}
Which we can call on a blank person:
test("test setFields") {
val p = setFields(new Person(null, null, -1), Map("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44))
p.firstname should be ("Duncan")
p.lastname should be ("McGregor")
p.age should be (44)
}
Of course we can do better with a little pimping:
implicit def any2WithFields[A](o: A) = new AnyRef {
def withFields(values: Map[String, Any]): A = setFields(o, values)
def withFields(values: Pair[String, Any]*): A = withFields(Map(values :_*))
}
so that you can call:
new Person(null, null, -1).withFields("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44)
If having to call the constructor is annoying, Objenesis lets you ignore the lack of a no-arg constructor:
val objensis = new ObjenesisStd
def create[A](implicit m: scala.reflect.Manifest[A]): A =
objensis.newInstance(m.erasure).asInstanceOf[A]
Now we can combine the two to write
create[Person].withFields("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44)
You mentioned in the comments that you're looking for a reflection based solution. Have a look at JSON libraries with extractors, which do something similar. For example, lift-json has some examples,
case class Child(name: String, age: Int, birthdate: Option[java.util.Date])
val json = parse("""{ "name": null, "age": 5, "birthdate": null }""")
json.extract[Child] == Child(null, 5, None)
To get what you want, you could convert your Map[String, String] into JSON format and then run the case class extractor. Or you could look into how the JSON libraries are implemented using reflection.
I guess you have domain classes of different arity, so here it is my advice. (all the following is ready for REPL)
Define an extractor class per TupleN, e.g. for Tuple2 (your example):
class E2(val t: Tuple2[String, String]) {
def unapply(m: Map[String,String]): Option[Tuple2[String, String]] =
for {v1 <- m.get(t._1)
v2 <- m.get(t._2)}
yield (v1, v2)
}
// class E3(val t: Tuple2[String,String,String]) ...
You may define a helper function to make building extractors easier:
def mkMapExtractor(k1: String, k2: String) = new E2( (k1, k2) )
// def mkMapExtractor(k1: String, k2: String, k3: String) = new E3( (k1, k2, k3) )
Let's make an extractor object
val PersonExt = mkMapExtractor("firstname", "lastname")
and build Person:
val testMap = Map("lastname" -> "L", "firstname" -> "F")
PersonExt.unapply(testMap) map {Person.tupled}
or
testMap match {
case PersonExt(f,l) => println(Person(f,l))
case _ => println("err")
}
Adapt to your taste.
P.S. Oops, I didn't realize you asked about named arguments specifically. While my answer is about positional arguments, I shall still leave it here just in case it could be of some help.
Since Map is essentially just a List of tuples you can treat it as such.
scala> val person = args.toList match {
case List(("firstname", firstname), ("lastname", lastname), _) => new Person(firstname, lastname)
case _ => throw new Exception
}
person: Person = Person(John,Doe)
I made Person a case class to have the toString method generated for me.