0

I'm trying to extract values from a tokenised String and create an (optional) case class instance from it.

The String takes the form of:

val text = "name=John&surname=Smith"

I have a Person class which will accept both values:

case class Person(name: String, surname: String)

I have some code which does the conversion:

def findKeyValue(values: Array[String])(prefix: String): Option[String] = 
     values.find(_.startsWith(prefix)).map(_.substring(prefix.length)) 

val fields: Array[String] = text.split("&")
val personOp = for {
   name <- findKeyValue(fields)("name=")
   surname <- findKeyValue(fields)("surname=")
} yield Person(name, surname)

While this yields the answer I need I was wondering:

  1. Is there a more efficient way to do this?
  2. Is there a more Functional Programming-centric way to do this?

Some constraints:

  1. The order of the name and surname fields in the text can change. The following is also valid:

    val text = "surname=Smith&name=John"
    
  2. There could be other fields which need to be ignored:

    val text = "surname=Smith&name=John&age=25"
    
  3. The solution needs to cater for when the text supplied is malformed or has none of the required fields.

  4. The solution can't use reflection or macros.

1
  • If the context is HTTP query param parsing, as Alvaro Carrasco mentioned, you're better off reusing an HTTP library method or if you can't, you must take URL encoding of parameters and the character set into account. If it's in a different context, elm's answer is pretty concise. And I wouldn't be worrying about efficiency unless the string has the potential to be huge and it's measured to be a bottleneck. Commented Nov 25, 2015 at 22:40

3 Answers 3

2

What would make it more efficient is if you parse it all the way into a Map[String,String] at the beginning (as opposed to an Array[String].

If you happen to have apache's http-client library as part of your dependencies already (good chance if you're using a web framework), i would use that:

import org.apache.http.client.utils.URLEncodedUtils
import java.nio.charset.StandardCharsets
import scala.collection.JavaConverters._

val values = URLEncodedUtils.parse(text, StandardCharsets.UTF_8)
  .asScala.map(x => x.getName -> x.getValue).toMap

val personOpt = 
  for {
    name <- values.get("name")
    surname <- values.get("surname")
  } yield Person(name, surname)

The reason for using a library is that assuming that this came from an http request of sorts, there's a good chance you might need to urldecode the keys and the values or other details that the library takes care of.

I think extractor version would be overkill, but here's what it would look like:

object PersonFromString {
  def unapply (s: String): Option[Person] = { ... same as above ... }
}
...
text match {
  case PersonFromString(person) => ... do something with it...
  ...
}
Sign up to request clarification or add additional context in comments.

4 Comments

I'm going to have to decode the data but I was going to use simple function to do that. I hadn't thought about using a library to do this but maybe that's cleaner. Hmm.
I thought about going directly to a Map[String, String] but the number of steps I needed to get there were more than the solution I came up with: def strToPair(line: String): Option[Tuple2[String, String]] = { val parts = line.split("=") if (parts.length == 2) Option(parts(0) -> parts(1)) else None } text.split("&").map(strToPair(_)).flatten.toMap
See my second comment to elm's answer
Yeah, collect seems like a nicer way to do it. I thought there'd be a nice way to create Tuples from a collection of values and then create a map from there. I guess you could use grouped(2) but if there are keys without values then you get an uneven collection.
0

I would say, the more idiomatic way to do such things is using Extractors.

Consider this answer: Read case class object from string in Scala (something like Haskell's "read" typeclass)

2 Comments

Extractors is an interesting way to do it. How would you account for the case where the order of fields is reversed? What would happen if there were additional fields? Eg. val text = "surname=Smith&name=John&age=25" I've added this constraint to the question.
@ssanj so, it depends on your implementation. You should implement your extractors in the way they know how to parse such strings. But if you want the same solution for all query parameters, it may be tricky.
0

On the parsing of the string, construct a Map[String,Option[String]] from attributes to values, for instance

val m = text.split("&")
            .map(_.split("="))
            .filter(_.size == 2)
            .map (xs => xs.head -> Some(xs.last))
            .toMap

to obtain for instance

Map(surname -> Some(Smith), name -> Some(John))

To fetch values for constructing the class instance (e.g. from a extractor as suggested already), use getOrElse like this

m.getOrElse("kjkj",None)
Option[String] = None

m.getOrElse("surname",None)
Option[String] = Some(Smith)

The case class needs be reformulated as

case class Person(name: Option[String], surname: Option[String])

where name and surname may be None whenever the string does not include such attributes/values which are therefore not present in the Map. Also note that to convey the filtering of arrays of size 2, pattern matching may be used.

3 Comments

Making it a Map[String, String] and using Map.get gives the same result in this case. Map[String, Option[String]] is required in the context where you need to differentiate the case of the key being present but the value is None from the case of the key not being present. Also, no need to change the case class: val p: Option[Person] = for (n <- m.get("name"); s <- m.get("surname")) yield Person(n, s)
I'd also colapse the filter and 2nd map to a collect that matches on the size of the array: text.split("&").map(_.split("=")).collect { case Array(k, v) => (k, v) }.toMap
As @KristianDomagala said you can just use Map.get and simply use a Map[String, String]. I like Kristian's use of collect as well.

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.