I have two optional strings, any of them can be None. I want to create their combination with a delimiter between them if they both exist.
I expect to be able to come with a more concise and nicer solution.
val seq = (Some("abc"), None)
val none = (None, None)
val both = (Some("abc"), Some("x"))
def concat(s: (Option[String], Option[String])): Option[String] = {
s match {
case (Some(a), Some(b)) => Some(a + ", " + b)
case (a, None) => a
case (None, b) => b
}
}
concat(seq)
concat(none)
concat(both)
Can this be rewritten somehow in a more elegant way? The input can be changed from Tuple to Seq if desired.
3 Answers 3
As you seem to have already noticed yourself, the fact that the input to the function is a pair of strings is somewhat awkward. It violates the Zero-One-Infinity Rule.
You might be worried about YAGNI or the Rule Of Three, but I would argue that it is okay to fudge those guidelines here a little bit, since
- the extension from two to many is obvious,
- simplifies the code, and
- is in service to another guideline.
One main question you need to ask yourself, which I feel is not really answered in the specification you gave in your question nor in the code snippet you are asking to have reviewed, relates to this statement from your question [bold emphasis mine]:
I want to create their combination with a delimiter between them if they both exist.
What, precisely do you mean by "if they both exist"? If we are looking at ways that an Option[String]
in Scala can exist or maybe not exist, I can think of many different scenarios:
- It can be
null
. (Unfortunately, since Scala is designed to tightly integrate with the underlying platform, and all of the "interesting" underlying platforms all have a concept ofnull
, it is hard to get rid of this.) - It can be
None
. - It can be
Some("")
, i.e. the empty string. - It can be
Some(" ")
, i.e. a string consisting purely of U+0020 Space characters. - It can be
Some(" ")
, i.e. a string consisting purely of whitespace characters other than U+0020 Space. (There are at least 16 different whitespace characters that are not U+0020 which I have found in about 30s of research.) - It can be
Some("")
, i.e. a string consisting purely of zero-width characters. (Note, this is not an empty string! It contains a U+200B Zero-width space. For added fun, this particular character is not considered Whitespace by the Unicode standard!)
And these are just some of the things I can think of off the top of my head.
For example, both your original code in the question and your revised code in the answer will return Some(", ")
when passed two empty strings, or Some(" , ")
when passed two strings consisting of one space character.
If you are willing to accept a very broad definition, then a very simple implementation could be something like this:
def concat(s: Option[String]*) = s.flatten.mkString(", ")
However, this will always give you a String
, it will not give you an Option[String]
. For your None, None
example, it will give you the empty string:
concat(None, None)
//=> ""
Any specification more complex than this very simple one, including the one from your question, will complicate the code, there is not much you can do about that. There is no getting around the fact that text processing looks deceptively simple (everybody knows what an "empty string" or a "character" is, right?) but is actually very complex.
<script src="https://scastie.scala-lang.org/JoergWMittag/1EScnd33SZ68MEXCCXjTQg.js"></script>
Alternatively,
You can redefine what concat does:
Using Seq
:
val seq: Seq[Option[String]] = Seq(None, Some("b"))
We can use foldLeft
to easily compute this (maybe a little bit overkill, but here goes):
// HelperFunc for appending commas if we have multiple values
def appendValues(total: Option[String], value: String, delimiter: String = ", "): String = {
total match {
case None => value
case Some(result) => s"$result$delimiter$value"
}
}
// HelperFunc for each op in the foldLeft
def op(total: Option[String], next: Option[String]): Option[String] ={
next match {
case Some(value) => Some(appendValues(total, value))
case None => total
}
}
def concat(seq: Seq[Option[String]]): Option[String] = {
seq.foldLeft(Option.empty[String])(op)
}
So finally:
concat(seq)
// Some(b)
Options can be concatenated to get an Iterable, which then makes things easier...
def concat(a:Option[String], b:Option[String]) =
Option(a ++ b).filter(_.nonEmpty).map(_.mkString(","))
This gives you...
concat(Some("a"), Some("b")) // Some("a,b")
concat(Some("a"), None) // Some("a")
concat(None, Some("b")) // Some("b")
concat(None, None) // None
val res = s.flatten.mkString(", ")
followed byOption.when(res.nonEmpty)(res)
\$\endgroup\$