3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ If we change it from "Tuple to Seq", as you suggest, then it can be done with 2 lines of code: val res = s.flatten.mkString(", ") followed by Option.when(res.nonEmpty)(res) \$\endgroup\$ – jwvh Nov 16 '20 at 20:14
2
\$\begingroup\$

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 of null, 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)
//=> ""

[Scastie link]

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>

\$\endgroup\$
1
\$\begingroup\$

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)
\$\endgroup\$
0
\$\begingroup\$

I came up with a solution which I like a bit more, based on sequence flattening, but still, I guess someone might be able to provide a better alternate answer.

val seq = Seq(Some("abc"), None)
val none = Seq(None, None)
val both = Seq(Some("abc"), Some("x"))

def concat(s: Seq[Option[String]]): Option[String] = {
  val r = s.flatMap(_.toSeq)
  if (r.nonEmpty) Some(r.mkString(", "))
  else None
}

concat(seq)
concat(none)
concat(both)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.