Skip to content

Instantly share code, notes, and snippets.

@sidharthkuruvila
Created July 21, 2012 06:30
Show Gist options
  • Save sidharthkuruvila/3154845 to your computer and use it in GitHub Desktop.
Save sidharthkuruvila/3154845 to your computer and use it in GitHub Desktop.
Utility to functions to convert between camel case and underscore separated names
/**
* Takes a camel cased identifier name and returns an underscore separated
* name
*
* Example:
* camelToUnderscores("thisIsA1Test") == "this_is_a_1_test"
*/
def camelToUnderscores(name: String) = "[A-Z\\d]".r.replaceAllIn(name, {m =>
"_" + m.group(0).toLowerCase()
})
/*
* Takes an underscore separated identifier name and returns a camel cased one
*
* Example:
* underscoreToCamel("this_is_a_1_test") == "thisIsA1Test"
*/
def underscoreToCamel(name: String) = "_([a-z\\d])".r.replaceAllIn(name, {m =>
m.group(1).toUpperCase()
})
@dmateusp
Copy link

dmateusp commented Jun 18, 2018

I had to support some cases where words were all upper case:

  /**
    * Converts from camelCase to snake_case
    * e.g.: camelCase => camel_case
    *
    * @param name the camelCase name to convert
    * @return snake_case version of the string passed
    */
  def camelToSnake(name: String): String = {
    @tailrec
    def go(accDone: List[Char], acc: List[Char]): List[Char] = acc match {
      case Nil => accDone
      case a::b::c::tail if a.isUpper && b.isUpper && c.isLower => go(accDone ++ List(a, '_', b, c), tail)
      case a::b::tail if a.isLower && b.isUpper => go(accDone ++ List(a, '_', b), tail)
      case a::tail => go(accDone :+ a, tail)
    }
    go(Nil, name.toList).mkString.toLowerCase
  }
  "camelToSnake" should "process right camel case + all upper case + mixed" in {
    camelToSnake("COLUMN") shouldBe "column"
    camelToSnake("someColumnNameRespectingCamel") shouldBe "some_column_name_respecting_camel"
    camelToSnake("columnWITHSomeALLUppercaseWORDS") shouldBe "column_with_some_all_uppercase_words"
  }

@acmitch
Copy link

acmitch commented Jun 23, 2018

@dmateusp 👍 exactly what I needed! Would be great if worked with numeric text:

actual: DirectlyEmployedOr1099Resources => directly_employed_or1099resources
expected: DirectlyEmployedOr1099Resources => directly_employed_or_1099_resources

@banshee
Copy link

banshee commented Jun 24, 2018

@dmateusp FYI, that causes a problem with things like camelToSnake("IsAString") => is_astring, when it should be is_a_string.

@ruloweb
Copy link

ruloweb commented Nov 1, 2018

I prefer not to use regex, this approach runs in n time, it's tail recursive and does not convert consecutive upper chars, e.g. ThisIsCAmel -> this_is_camel:

def camel2Underscore(s: String): String = {
  @tailrec def camel2Underscore(s: String, output: String, lastUppercase: Boolean): String =
    if (s.isEmpty) output
    else {
      val c = if (s.head.isUpper && !lastUppercase) "_" + s.head.toLower else s.head.toLower
      camel2Underscore(s.tail, output + c, s.head.isUpper && !lastUppercase)
    }

  camel2Underscore(s, "", true)
}

@amackillop
Copy link

amackillop commented Apr 10, 2019

@ruloweb This is nice but won't work if the two consecutive uppercase chars are at the start (ie. THisIsCamel) or if there are greater than two consecutive uppercase characters elsewhere (ie. ThisIsCAMel). Consequently, this will fail for converting all caps as well.

I adjusted the code to support the all caps case:

 def camel2Snake(str: String): String = {
    @tailrec
    def camel2SnakeRec(s: String, output: String, lastUppercase: Boolean): String =
      if (s.isEmpty) output
      else {
        val c = if (s.head.isUpper && !lastUppercase) "_" + s.head.toLower else s.head.toLower
        camel2SnakeRec(s.tail, output + c, s.head.isUpper && !lastUppercase)
      }
    if (str.forall(_.isUpper)) str.map(_.toLower)
    else {
      camel2SnakeRec(str, "", true)
    }
  }

@0dilon
Copy link

0dilon commented Dec 19, 2019

@amackillop, it doesnt't work for "THISIsADog" (it gives "t_hi_sis_adog")

i suggest the following code :

def camel2Snake(str: String): String = {

  val headInUpperCase = str.takeWhile(c => c.isUpper || c.isDigit)
  val tailAfterHeadInUppercase = str.dropWhile(c => c.isUpper || c.isDigit)

  if (tailAfterHeadInUppercase.isEmpty) headInUpperCase.toLowerCase else {
    val firstWord = if (!headInUpperCase.dropRight(1).isEmpty) {
      headInUpperCase.last match {
        case c: Any if (c.isDigit) => headInUpperCase
        case _ => headInUpperCase.dropRight(1).toLowerCase
      }
    } else {
      headInUpperCase.toLowerCase + tailAfterHeadInUppercase.takeWhile(c => c.isLower)
    }

    if (firstWord == str.toLowerCase) {
      firstWord
    } else {
      s"${firstWord}_${camel2Snake(str.drop(firstWord.length))}"
    }

  }
}

@nicusX
Copy link

nicusX commented Jan 2, 2020

@0dilon you version stack-overflows if the camelCase already contains an underscore.
e.g. camel2Snake("foo_BarBaz")

@jnewman
Copy link

jnewman commented May 18, 2021

Late to the party, but iterate w/ Chars seems fine too if you're ok w/ no handling -s in the Strings

val camelToKebab: String => String = _.foldLeft("") {
      case (acc, chr) if chr.isUpper => acc :+ '-' :+ chr.toLower
      case (acc, chr)                => acc :+ chr
    }

@Kalyan-D
Copy link

@0dilon you version stack-overflows if the camelCase already contains an underscore.

any update on this @0dilon

@umbarger
Copy link

umbarger commented Sep 21, 2021

val camelToSnake : String => String = _.foldLeft( "" ){ (acc, c) =>
    ( c.isUpper, acc.isEmpty, acc.takeRight(1) == "_" ) match {
      case (true, false, false) => acc + "_" + c.toLower
      case (true, _, _) => acc + c.toLower
      case (false, _, _) => acc + c
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment