Search code examples
stringscalatail-recursion

Scala: How to uppercase every other letter


I was asked this question in an interview. She wanted the answer in scala not java. My answer was java centric. Note that you can NOT use the character index to decide whether to uppercase and lowercase. Can you find a better way to make it scala centric? I can't think of any StringOps builtin functions that would help here. Focus on removing the need for var. I rewrote it using tail recursion (afterwards) but the answer was inefficient so I'm sticking with this java centric answer.

object Cap extends App {
  def _capitalizeEveryOtherLetter(str: String): String = {
    val s2: Array[Char] = str.toCharArray
    var capitalize: Boolean = true
    val arr: Array[Char] = (for (c <- s2)  yield  {
      if (!Character.isLetter(c)) {
        c
      } else if (capitalize) {
        capitalize = false
        val newChar = Character.toUpperCase(c)
        newChar
      } else {
        capitalize = true
        val newChar = Character.toLowerCase(c)
        newChar
      }
    })
    new String(arr)
  }
  val s1: String = "*7yTuu(i&^giuisKJSU&Y"
  println(s"${_capitalizeEveryOtherLetter(s1)}")
}

The expected output is

*7YtUu(I&^gIuIsKjSu&Y

Similar but not the same as How to convert every other character to upper case in a string since it relies on indexing which is not valid for this question.


Solution

  • The scala way if you only had letters would be something like

    input.grouped(2).map(_.capitalize).mkString("")
    

    Skipping non-letter chars makes it somewhat more complicated: first figure out how each letter should be transformed, then replace them:

    val letters = input.filter(_.isLetter).grouped(2).flatMap(_.capitalize)
    input.map { c => if (c.isLetter) letters.next else c }
    

    But this is probably too "clever" for an interview :) (well, depends where you are interviewing).

    The "official" scala approach to "traverse/transform with state" is either (tail) recursion or (for simpler cases) foldLeft with an accumulator. And the trick to avoid both mutability and the inefficiency of appending things to strings or collections mentioned in the other answer is building a list backwards, and reversing it in the end.

    input.foldLeft[(Boolean, List[Char])](true -> Nil) { 
      case ((x, ac), c) if !c.isLetter => (x, c::ac)
      case ((true, ac), c) => (false, c.toUpper :: ac)
      case ((_, ac), c) => (true, c :: ac)
    }._2.reverse.mkString("")
    

    Since your question is labeled tail-recursion, here is a tail-recursive way (it's not really all that different from the foldLeft above):

       @tailrec
       def upSome(
           chars: List[Char], 
           state: Boolean = true, 
           result: List[Char]
       ): List[Char] = (state, chars) match { 
         case (_, Nil) => result.reverse
         case (_, c::tail) if !c.isLetter => upSome(tail, state, c::result)
         case (true, c::tail) => upSome(tail, false, c::result)
         case (_, c::tail) => upSome(tail, true, c::result)
       }
    

    You would use it as upSome(input.toList).mkString("")