1. Introduction

Camel case and snake case are two widely used naming conventions in programming. Camel case capitalizes the first letter of each word except the first one and combines them into a single string without spaces. On the other hand, snake case connects words using an underscore, with all characters typically in lowercase. For example, thisIsBaeldung is in camel case, whereas this_is_baeldung is in snake case.

Programming languages like Java, Scala, and Kotlin generally recommend using camel case, while Python and SQL prefer snake case. Both conventions might be used in JSON formats depending on the programmer’s preference. Consequently, converting between these two naming conventions is common and necessary when working with multiple tools, languages, and frameworks.

In this article, we’ll look at different ways to convert a string from camel case to snake case convention.

2. Understanding the Problem

Before starting with different implementations, let’s understand the requirements and assumptions in detail. When converting between camel and snake cases, we work with several assumptions.

Generally, the strings contain only alphabets, and we assume there are no spaces in camel or snake cases. However, special characters (excluding spaces) might also appear. For this tutorial, we’ll treat special characters like lowercase letters, and the snake case output will be generated in lowercase. Abbreviations are edge cases with no standard conversion approach. Furthermore, we expect the string to be a valid camel case – always starting with a lowercase character  In our simplified implementation, we don’t treat abbreviations specifically; thus, thisIsUSA would be converted to this_is_u_s_a.

3. Using Regular Expression

We can use regular expressions to convert from camel case to snake case. Let’s look at the implementation:

def usingRegex(camelCaseString: String): String = {
  val regex = "([A-Z])".r
  regex.replaceAllIn(
    camelCaseString,
    m => s"_${m.group(1).toLowerCase}"
  )
}

This function uses a regular expression to capture an upper character and convert it into a lower character by prefixing it with an underscore.

4. Using foldLeft()

Another way to perform this task is by using a foldLeft() to iterate over each character and prefixing with an underscore whenever an upper character is found. Let’s look at the implementation:

def usingFoldLeft(camelCaseString: String) = {
  camelCaseString.foldLeft("") { (acc, char) =>
    if (char.isUpper) {
      if (acc.isEmpty) char.toLower.toString
      else acc + "_" + char.toLower
    } else {
      acc + char
    }
  }
}

This achieves the same behavior as in the previous regular expression implementation.

5. Using Pattern Matching and Tail Recursion

Pattern matching in Scala is a powerful concept that can be applied in many scenarios, including string manipulation. By combining pattern matching with tail recursion, we can effectively convert a camel case string into a snake case string. Here is the implementation:

def usingPatterMatching(camelCase: String): String = {
  @tailrec
  def rec(chars: List[Char], acc: List[Char]): List[Char] = {
    chars match {
      case Nil                    => acc
      case a :: tail if a.isUpper => rec(tail, acc ++ Seq('_', a))
      case a :: tail              => rec(tail, acc ++ Seq(a))
    }
  }
  rec(camelCase.toList, Nil).mkString.toLowerCase
}

In this implementation, we convert the string to a List[Char] and apply pattern matching to check for uppercase characters. When we find an uppercase character, we add an underscore and this character to the accumulator. When the input list becomes empty, we return the accumulator. Finally, we combine the accumulated characters into a string using mkString() and convert it to lowercase.

6. Using flatMap()

We can iterate over each character of a string using flatMap() and apply a similar logic to convert to a snake case. Let’s look at the implementation:

def usingFlatMap(camelCase: String): String = {
  camelCase.flatMap { c =>
    if (c.isUpper) List('_', c) else List(c)
  }.mkString.toLowerCase
}

When we find an uppercase character, we return a List containing an underscore and this character. Otherwise, we return the same character. Finally, we combine the characters using mkString() and convert them to lowercase, just like in other methods.

7. Using collect()

We can use the collect() method to perform the same requirement:

def usingCollect(camelCase: String): String = {
  camelCase
    .collect {
      case c if c.isUpper => Seq('_', c)
      case c              => Seq(c)
    }.flatten.mkString.toLowerCase
}

This implementation is very similar to the flatMap() implementation but uses the collect() partial function to achieve the same.

8. Customization

We explored various implementations to convert a string from camel case to snake case, all based on the assumptions we defined at the beginning. While these implementations work well under typical conditions, there may be times when you need to tailor the approach to meet specific requirements. You can adjust the implementation in such cases to make it easier for you.

For example, our implementations fail if the input string starts with an uppercase character, causing the snake case output to start with an underscore. To address this, we can apply the function stripPrefix(“_”) on the result to remove the leading underscore if it exists.

In some scenarios, we should handle acronyms separately. For instance, we want to convert thisIsUSA to this_is_usa, treating USA as a single word instead of separating it with underscores. Let’s look at this implementation:

def handleAcronymsWithRegex(camelCaseString: String): String = {
  camelCaseString
    .replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2")
    .replaceAll("([a-z\\d])([A-Z])", "$1_$2")
    .toLowerCase
}

We modified the previous regular expressions to handle this requirement.

9. Testing the Implementations

To cover maximum scenarios, we can use parameterized tests from the ScalaTest:

private val table = Table(
  ("input", "expected"),
  ("thisIsCamelCase", "this_is_camel_case"),
  ("isThisCamel?", "is_this_camel?"),
  ("alllower", "alllower"),
  ("thisIsUSA", "this_is_u_s_a"),
  ("xmlHttpRequest", "xml_http_request"),
  ("convertXMLToJSON", "convert_x_m_l_to_j_s_o_n"),
  ("parseHTML", "parse_h_t_m_l"),
  ("classOfT", "class_of_t")
)
private val fns = Seq(
  ("usingRegex", CamelToSnakeConversion.usingRegex),
  ("usingFoldLeft", CamelToSnakeConversion.usingFoldLeft),
  ("usingFlatMap", CamelToSnakeConversion.usingFlatMap),
  ("usingPatterMatching", CamelToSnakeConversion.usingPatternMatching),
  ("usingCollect", CamelToSnakeConversion.usingCollect)
)
it should "convert camel to snake case" in {
  forAll(table) { (camel, expectedSnake) =>
    fns.map { (name, fn) =>
      withClue("function name: " + name) {
        fn(camel) shouldBe expectedSnake
      }
    }
  }
}

We can write similar tests for the other special cases:

it should "handle special case" in {
  Seq(
    ("HelloWorld", "hello_world"),
    ("thisIsUSA", "this_is_usa"),
    ("convertXMLToJSON", "convert_xml_to_json"),
    ("xmlToHTTPRequest", "xml_to_http_request"),
  ).map { (in, out) =>
    val output = CamelToSnakeConversion.handleAcronymsWithRegex(in)
    output shouldBe out
  }
}

This approach makes it simpler to test different cases without duplicating the tests.

10. Conclusion

In this article, we explored various methods to convert a camel case string to a snake case. We examined different techniques, including regular expressions, pattern matching, recursion, and higher-order functions such as flatMap() and collect(). Furthermore, we discussed different implementation variants based on custom requirements. We also wrote extensive tests to validate the implementations using table-driven property checks.

As always, the code used in this article is available over on GitHub.