Skip to content

Instantly share code, notes, and snippets.

@sidharthkuruvila
Created July 21, 2012 06:30
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • 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()
})
@samthebest
Copy link

You shouldn't include digits when converting between camel and underscores, by definition there is no such thing as an upper case digit, and therefore it doesn't make sense to consider a digit a capitalised separate word.

@erikkaplun
Copy link

your algorithm doesn't correctly map _FOO_BAR to _FooBar and vice versa; here's my attempt at it:

def underscores2camel(name: String) = {
  assert(!(name endsWith "_"), "names ending in _ not supported by this algorithm")
  "[A-Za-z\\d]+_?|_".r.replaceAllIn(name, { x =>
    val x0 = x.group(0)
    if (x0 == "_") x0
    else x0.stripSuffix("_").toLowerCase.capitalize
  })
}
def camel2underscores(x: String) = {
  "_?[A-Z][a-z\\d]+".r.findAllMatchIn(x).map(_.group(0).toLowerCase).mkString("_")
}

@ry4nz
Copy link

ry4nz commented Mar 27, 2015

scala> camel2underscores("CamelCaseF")
res37: String = camel_case

@mrnovalles
Copy link

@eallik, your algorithm returns "" for a non-camelcased string

scala> camel2underscores("foobar")
res1: String = ""

@DeaconDesperado
Copy link

To avoid the leading _:

  def camelToUnderscores(name: String) = "[A-Z\\d]".r.replaceAllIn(name, {m =>
    if(m.end(0) == 1){
      m.group(0).toLowerCase()
    }else {
      "_" + m.group(0).toLowerCase()
    }
  })

@asheshambasta
Copy link

asheshambasta commented Aug 11, 2016

Maybe a little less concise but I prefer this;

def camel2Underscore(text: String) = text.drop(1).foldLeft(text.headOption.map(_.toLower + "") getOrElse "") {
  case (acc, c) if c.isUpper => acc + "_" + c.toLower
  case (acc, c) => acc + c
}

@talbs1986
Copy link

@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