case class DNA(strand: String) {
def nucleotideCounts: Either[String, Map[Char, Int]] = {
val output = Map('A' -> 0, 'C' -> 0, 'G' -> 0, 'T' -> 0)
strand.foldLeft(("", output)) { (tup, chr) =>
tup match {
case (errStr, output) if output.contains(chr) =>
(errStr, output + (chr -> (output(chr) + 1)))
case (errStr, output) => (s"invalid nucleotide '$chr'", output)
}
} match {
case (errStr, output) if errStr.isEmpty() => Right(output)
case (errStr, _) => Left(errStr)
}
}
}
This approach starts by defining a Map.
The foldLeft() method is then called on the input String.
It is initalized with en empty String intended for the Left error value and
the Map intended for the Right value,
both wrapped together in a tuple.
The tuple and each character from the input String are passed into the lambda.
The match is used to destructure the tuple and checks to see if the character is valid.
If the Map contains() the character as a key, then a new tuple is created, using the
the error String as is and a new Map created by using the + alias for the updated() method.
Recreating an immutable Map instead of changing a mutable Map is how the approach maintains immutability.
If the Map does not contain the character, then a new tuple is created with a new error String and the Map as is.
After the foldLeft() has done, its final tuple is checked by the match.
If the pattern matching determines the error String is empty, then the Map is returned from nucleotideCounts(),
wrapped as a Right value.
Otherwise, the error String is returned from nucleotideCounts(), wrapped as a Left value.