Count adjacent repeating characters in a String using scala

Viewed 239

I need to print the adjacent repeating characters in a string along with their count using scala.

Eg: aaabbcda = 3a2b1c1d1a

what I tried is

object StringTransformation extends App {
  
   val str = "aaabbcda"
   strTransform(str)
  
   def strTransform(str:String) {
     var count = 1
     for (i <- 1 until str.length) {
        if(str.charAt(i-1) == str.charAt(i)){
            count += 1
        }else{
            print(count+String.valueOf(str.charAt(i-1)))      
            count=1
        }
     }
     print(count+str.takeRight(1))
   }
}

Although this is working, this is the programmed keeping imperative approach in mind. Can someone please suggest the functional approach using map() or some other methods.

7 Answers

Functional programming puts a big emphasis on breaking a problem down. In your case, your problem consists of three major steps

  1. Find and count adjacent characters
  2. Format the string with the counts before the characters
  3. Print the result to the screen

(1) can be a function of its own, (2) is pretty easy to do with map, and (3) is a simple println at the end of it all.

(1) will be the most complicated. I'm assuming we have a List (it's relatively simple to convert between different Scala collection types, and lists are great for practicing recursion).

// group() takes a list and a grouping function and returns the
// collection of adjacent elements, grouped by the function. In our
// case, the function will simply be the identity function, but we
// could also use it to define an equivalence class. For instance, if
// the function was toUpper or toLower, then our grouping would be
// case-insensitive.
def group[A, B](lst: List[A])(f: A => B): List[(B, Int)] = {
  // This is our recursive helper function. It's defined in here
  // because it's only useful in here. We keep track of the current
  // element we're looking at and how many times we've seen it.
  def go(curr: B, count: Int, xs: List[A]): List[(B, Int)] = {
    // This is roughly your imperative solution, but written
    // recursively and in more generality. We check if the next
    // element is equal to our "current" element and branch
    // accordingly.
    xs match {
      case Nil => List((curr, count))
      case x :: xs =>
        if (f(x) == curr) {
          go(curr, count + 1, xs)
        } else {
          (curr, count) :: go(f(x), 1, xs)
        }
    }
  }
  // If the list starts out empty, there's no need to recurse. If the
  // list is non-empty, then start at the beginning.
  lst match {
    case Nil => Nil
    case x :: xs => go(f(x), 1, xs)
  }
}

Now, to call it,

val str = "aaabbcda"
println(group(str.toList) { x => x }.map { case (b, n) => s"$n$b" }.mkString)

First, we convert the string to a list of characters with toList. Once we have a list, we call our group() function with the identity function for comparison key (so a character only ever compares equal to itself). After the group() call, we have a List((Char, Int)).

Then we call map on that list to format it as a string the way you wanted. This gives us a List(String), and mkString concatenates those strings together. Finally, we println the string.

This function isn't currently tail recursive. With a bit of effort, we could likely get it to be tail recursive, or, as indicated in the comments, it's a good exercise to try to convert the recursive function I defined to one using foldLeft. But this is a start; none of the code I just wrote relies on mutability or traditional imperative looping constructs. This is the sort of code you would see in a language like Haskell.

I believe this is mostly easily and readably accomplished with span and recursion:

def counts(s: String): String =
  if (s.isEmpty) ""
  else {
    val (segment, rest) = s.span(_ == s.head)
    segment.size.toString + s.head + counts(rest)
  }

scala 2.13.6> counts("aaabbcda")
val res5: String = 3a2b1c1d1a

Complementing Silvio's great answer.
I would like to present different approaches to solve this problem.

First, a solution using a tail-recursive algorithm:

object UsingTailRecursion {
  def apply(str: String): String = {
    @annotation.tailrec
    def loop(remaining: List[Char], currentChar: Char, currentCount: Int, acc: List[(Char, Int)]): String =
      remaining match {
        case char :: tail =>
          if (char == currentChar)
            loop(
              remaining = tail,
              currentChar,
              currentCount + 1,
              acc
            )
          else
            loop(
              remaining = tail,
              currentChar = char,
              currentCount = 1,
              (currentChar -> currentCount) :: acc
            )
        
        case Nil =>
          ((currentChar -> currentCount) :: acc).reverseIterator.map {
            case (char, count) =>
              s"${count}${char}"
          }.mkString
      }
    
    str.toList match {
      case char :: tail =>
        loop(
          remaining = tail,
          currentChar = char,
          currentCount = 1,
          acc = List.empty
        )
      
      case Nil =>
        ""
    }
  }
}

This one is pretty close to the imperative version of the algorithm, the focus here was just to remove mutability and ensure we do not blow up the stack if the string is too big.

Second, using foldLeft and some helper classes and methods to reduce boilerplate.

object UsingFoldLeft {
  private final case class MyState(currentChar: Char, currentCount: Int, acc: List[(Char, Int)]) {
    def nextState(nextChar: Char): MyState =
      if (nextChar == currentChar)
        this.copy(currentCount = this.currentCount + 1)
      else
        MyState(currentChar = nextChar, currentCount = 1, acc = (this.currentChar -> this.currentCount) :: this.acc)
    
    def result: String =
      ((currentChar -> currentCount) :: acc).reverseIterator.map {
        case (char, count) =>
          s"${count}${char}"
      }.mkString
  }
  
  def apply(str: String): String = {
    val result = str.foldLeft(Option.empty[MyState]) {
      case (Some(state), char) =>
        Some(state.nextState(nextChar = char))
      
      case (None, char) =>
        Some(MyState(currentChar = char, currentCount = 1, acc = List.empty))
    }
    
    result.fold(ifEmpty = "")(_.result)
  }
}

As you can see this is more concise and didn't require us to worry about things like iterating with the remaining elements.
(Note: For this particular problem I would say this is the solution with the best trade-off between learning-curve and advantages; however I present the next one for completeness).

Finally, a version using the cats library; precisely the State Monad and the traverse method.

object UsingStateMonad {
  import cats.data.{State, NonEmptyList}
  import cats.syntax.all._
  
  private final case class MyState(currentChar: Char, currentCount: Int, acc: List[(Char, Int)])
  
  private def nextState(nextChar: Char): State[MyState, Unit] = State.modify { s =>
    if (nextChar == s.currentChar)
      s.copy(currentCount = s.currentCount + 1)
    else
      MyState(currentChar = nextChar, currentCount = 1, acc = (s.currentChar -> s.currentCount) :: s.acc)
  }
  
  def stateToResult(state: MyState): String =
    ((state.currentChar -> state.currentCount) :: state.acc).reverseIterator.map {
      case (char, count) =>
        s"${count}${char}"
    }.mkString
  
  def apply(str: String): String =
    str.toList.toNel.fold(ifEmpty = "") {
      case NonEmptyList(char, tail) =>
        val initialState = MyState(currentChar = char, currentCount = 1, acc = List.empty)
        tail.traverse_(nextState).runS(initialState).map(stateToResult).value  
    }
}

As you can see, I am using things like NonEmptyList and State to better represent each operation's intention and allow me to scale the solution if needed.


As you can see, each step tries to reduce mechanical steps and rather focus more on the description of the problem.

The second and most important advantage of each following version is the ability to split the problem into more independent steps that can be understood independently and can be moved to independent functions that can be tested in isolation.


Feel free to ask any questions you may have.


You can see the code running here.

Here is a functional solution using foldLeft:

case class Helper(
  locked: Seq[(Char, Int)],
  current: Map[Char, Int]
)

object Helper {
  def empty: Helper = Helper(Seq.empty, Map.empty)
}


def strTransformFunc(str: String): String = {
  val helper = str.foldLeft(Helper.empty) {
    case (acc, nextChar) =>
      acc.current.get(nextChar) match {
        case Some(value) => acc.copy(current = acc.current + (nextChar -> (value + 1)))
        case None => acc.copy(locked = acc.locked ++ acc.current.toSeq, current = Map(nextChar -> 1))
      }
  }

  (helper.locked ++ helper.current).map { case (theChar, counter) => s"$counter$theChar" }.mkString
}

println(strTransformFunc("aaabbcda"))
res> "3a2b1c1d1a"

Here's a functional approach using recursion and a helper function to accumulate results. This defines Tuple2[Int, Char] Counter type that is used as an accumulator while processing the string. The various case statements check if the first element in the string is the same as the current value in the counter. If it is, then the count is increased by one and we move on to the next character. There are a couple cases to handle penultimate character and whether or not that is equal to the value in the counter.

This could be made tail recursive by making counter a List[Tuple2[Int, Char]] and updating the tuple in the function parameter when countAdjacentR(...) is called.

def countAdjacent(s: String): String = {
  type Counter = Tuple2[Int, Char]
  def countAdjacentR(s: String, counter: Counter): List[Counter] = s match {
    case s if s.isEmpty                            => List(counter)  
    case s if(s.length == 1 && s(0) == counter._2) =>            countAdjacentR(s.tail, (counter._1 + 1, s.head))
    case s if(s.length == 1 && s(0) != counter._2) => counter :: countAdjacentR(s.tail, (1, s.head))
    case s if(s(0) == counter._2)                  =>            countAdjacentR(s.tail, (counter._1 + 1, counter._2)  )
    case s                                         => counter :: countAdjacentR(s.tail, (1,s.head))
  }
  if(s.isEmpty)
    ""
  else
    val result = countAdjacentR(s, (0,s.head))
    result.map(t => t._1.toString.concat(t._2.toString)).mkString
}

Tests:

scala> countAdjacent("")
val res47: String = ""

scala> countAdjacent("a")
val res48: String = 1a

scala> countAdjacent("abc")
val res49: String = 1a1b1c

scala> countAdjacent("aaabbc")
val res50: String = 3a2b1c

scala> countAdjacent("aaabbcda")
val res51: String = 3a2b1c1d1a

I guess, I'll pile on :) Everything I see so far is either inefficient or overly complicated or both ... Here is a simple a straightforward solution. Basically, just do exactly what the problem statement said: count characters while they are repeating, then output the count and char.

 def pack(s: String) = (0.toChar :: s.toList).sliding(2)
    .foldRight(1 -> List.empty[String]) {
       case (next :: last :: Nil, (n, list)) if next == last => (n+1, list)
       case (next :: last :: Nil, (n, list)) => (1, n.toString :: last.toString :: list)
     }._2.mkString

The tricky piece is to "pad" the string on the left (with anything that you can't appear in the string itself ... technically, it's enough to just make sure it is not the same as the first character).

Then just traverse it right to left, "peaking" at the next character every time. If it is the same as the current one, you increment the counter, otherwise, add the counter and current char to output and reset.

The last pair will have the "sentinel" char that was prepended in the beginning, which won't match it's pair by construction, so it'll cause the final (non-dummy) char to be added to output before stopping.

via Regex magic.

"aaabbcda".split("(?<=(.)(?!\\1))")
          .map(x => s"${x.length}${x.head}")
          .mkString

//res0: String = 3a2b1c1d1a
Related