ajmartin
ajmartin

Reputation: 2409

Regex for converting CamelCase to camel_case in java

I understand why the desired output is not given for converting using regex a string like FooBar to Foo_Bar which instead gives Foo_Bar_. I could have done something with String.substring substring(0, string.length() - 2) or just replaced the last character, but I think there is a better solution to such a scenario.

Here is the code:

String regex = "([A-Z][a-z]+)";
String replacement = "$1_";

"CamelCaseToSomethingElse".replaceAll(regex, replacement); 

/*
outputs: Camel_Case_To_Something_Else_
desired output: Camel_Case_To_Something_Else
*/

Question: Looking for a neater way to get the desired output?

Upvotes: 106

Views: 97475

Answers (16)

A little late to the party, but I'd still like to contribute my two cents.

You could also try jackson-databind, this way you dont need to config the source case, just the target, like this:

var namingStrategy = new PropertyNamingStrategies.SnakeCaseStrategy();
String solution = namingStrategy.translate("anythingYouWant"); //anything_you_want

Feel free to look at all the contructors in PropertyNamingStrategies for the differents namings strategies

Upvotes: 0

john16384
john16384

Reputation: 8054

You can use this regex:

(?<!^)(?=[A-Z](?![A-Z]|$))|(?<=[a-z])(?![a-z]|$)

In this code:

Pattern.compile("(?<!^)(?=[A-Z](?![A-Z]|$))|(?<=[a-z])(?![a-z]|$)")
    .splitAsStream(camelCaseInput)
    .map(String::toLowerCase)
    .collect(Collectors.joining("_"));

Note that this "breaks" when the input isn't a Java identifier or isn't strictly camel case, for example it will break on:

Input Output Remark
Number3inMiddleAgain number_3in_middle_again "in" should be "In"
with_underscore with__underscore "_" is not camel case
with%SYMBOLAndNumber90 with_%symbol_and_number_90 not a valid identifier
__abc__ __abc___ "_" is not camel case

It also makes no attempt to detect acronyms (it assumes any consecutive string of uppercase letters, except the last one (unless it is the end) is an acronym. This can break for some cases because it is context dependent or depends on knowing whether something is an acronym or not (ie. getUUIDs becomes get_uui_ds, but getUUIDAsString becomes get_uuid_as_string -- is UUI an acronym or is UUID an acronym?)

Explanation of regex:

It's looking for zero-width points where an underscore must appear:

  • (?<!^) Don't match if previous character was the start
  • A|B Followed by a match for A or B
  • A: (?=[A-Z](?![A-Z]|$)) Match if next character is a capital not followed by another capital or the end
  • B: (?<=[a-z])(?![a-z]|$) Match if previous character was a lower case character and next character is not a lower case character or the end

Upvotes: 0

Sergey Nemchinov
Sergey Nemchinov

Reputation: 1616

Yet another solution with Apache Commons.

import org.apache.commons.lang3.StringUtils;

public static String toLowerUnderscore(String str) {
    if (str == null) {
        return null;
    }
    String[] tokens = StringUtils.splitByCharacterTypeCamelCase(str);
    String joined = StringUtils.join(tokens, '\t');
    String replaced =  joined
            .replace("_\t", "_") // save beginning underscore
            .replace("\t_", "_") // save ending underscore
            .replace("\t", "_"); // replace other underscores
    return replaced.toLowerCase();
}

Test cases (thanks @Ali):

thisIsATest:          this_is_a_test
EndWithNumber3:       end_with_number_3
3ThisStartWithNumber: 3_this_start_with_number
Number3InMiddle:      number_3_in_middle
Number3inMiddleAgain: number_3_in_middle_again
MyUUIDNot:            my_uuid_not
HOLAMundo:            hola_mundo
holaMUNDO:            hola_mundo
with_underscore:      with_underscore
withAUniqueLetter:    with_a_unique_letter
123456789:            123456789
"   ":                "   "
_:                    _
__abc__:              __abc__
null:                 null

Upvotes: 1

Ali
Ali

Reputation: 579

Here my solution with 3 regular expression:

str.replaceAll("([^A-Z])([A-Z0-9])", "$1_$2") // standard replace
                   .replaceAll("([A-Z]+)([A-Z0-9][^A-Z]+)", "$1_$2") // last letter after full uppercase.
                    .replaceAll("([0-9]+)([a-zA-Z]+)", "$1_$2").toLowerCase(); // letters after numbers

The result:

thisIsATest: this_is_a_test
EndWithNumber3: end_with_number_3
3ThisStartWithNumber: 3_this_start_with_number
Number3InMiddle: number_3_in_middle
Number3inMiddleAgain: number_3_in_middle_again
MyUUIDNot: my_uuid_not
HOLAMundo: hola_mundo
holaMUNDO: hola_mundo
with_underscore: with_underscore
withAUniqueLetter: with_a_unique_letter


Edited:

To support numbers and another symbols, you can use this:

str.replaceAll("([^A-Z])([A-Z])", "$1_$2") // standard replace
                    .replaceAll("([A-Z]+)([^a-z][^A-Z]+)", "$1_$2") // last letter after full uppercase.
                    .toLowerCase()
                    .replaceAll("([^a-z]+)([a-z]+)", "$1_$2") // letters after non-letters.
                    .replaceAll("([a-z]+)([^a-z]+)", "$1_$2"); // letters before non-letters.

The result:

thisIsATest: "this_is_a_test"
EndWithNumber3: "end_with_number_3"
3ThisStartWithNumber: "3_this_start_with_number"
Number3InMiddle: "number_3_in_middle"
Number3inMiddleAgain: "number_3_in_middle_again"
MyUUIDNot: "my_uuid_not"
HOLAMundo: "hola_mundo"
holaMUNDO: "hola_mundo"
with_underscore: "with_underscore"
withAUniqueLetter: "with_a_unique_letter"
with%SYMBOLAndNumber90: "with_%_symbol_and_number_90"
http%: "http_%"
123456789: "123456789"
     : "     "
_: "_"
__abc__: "__abc__"

Upvotes: 1

Volodymyr D.
Volodymyr D.

Reputation: 1382

You can easily convert String to camel case using Stream API from Java 8 and method StringUtils.capitalize(..) from commons-lang

 public String toCamelCase(String str) {
    return Arrays.stream(str.split("_"))
        .map(StringUtils::capitalize)
        .collect(Collectors.joining());
}

Upvotes: -1

Govan
Govan

Reputation: 2129

I am writing this answer if somebody doesn't want to use Guava as below for any reason.

CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, "SomeInput");

In our case we had problem with storage. There is another special case with Guava: if we have "Ph_D" as input, then we are going to get "ph__d" with two underscores.

The code below worked as long as I tested it.

public static String camelCaseToLowerCaseWithUnderscore(String string) {
    if (string.matches(".*[a-z].*")) {
        final Matcher matcher = Pattern.compile("(_?[A-Z][a-z]?)").matcher(string);

        StringBuffer stringBuffer = new StringBuffer();
        matcher.find(); // This is just to escape the first group (beginning of string)
        while (matcher.find()) {
            final String group = matcher.group();
            if (!group.startsWith("_")) {
                matcher.appendReplacement(stringBuffer, "_" + group);
            }
        }
        matcher.appendTail(stringBuffer);
        return stringBuffer.toString().toLowerCase();
    }
    else {
        return string;
    }
}

Upvotes: 1

Thomas Decaux
Thomas Decaux

Reputation: 22671

Not sure it's possible to have something really solide with pure regex. Especially to support acronyms.

I have made a small function, inspired by @radzimir answer, that supports acronyms and no alphabetic character:

From https://gist.github.com/ebuildy/cf46a09b1ac43eea17c7621b7617ebcd:

private static String snakeCaseFormat(String name) {
    final StringBuilder result = new StringBuilder();

    boolean lastUppercase = false;

    for (int i = 0; i < name.length(); i++) {
        char ch = name.charAt(i);
        char lastEntry = i == 0 ? 'X' : result.charAt(result.length() - 1);
        if (ch == ' ' || ch == '_' || ch == '-' || ch == '.') {
            lastUppercase = false;

            if (lastEntry == '_') {
                continue;
            } else {
                ch = '_';
            }
        } else if (Character.isUpperCase(ch)) {
            ch = Character.toLowerCase(ch);
            // is start?
            if (i > 0) {
                if (lastUppercase) {
                    // test if end of acronym
                    if (i + 1 < name.length()) {
                        char next = name.charAt(i + 1);
                        if (!Character.isUpperCase(next) && Character.isAlphabetic(next)) {
                            // end of acronym
                            if (lastEntry != '_') {
                                result.append('_');
                            }
                        }
                    }
                } else {
                    // last was lowercase, insert _
                    if (lastEntry != '_') {
                        result.append('_');
                    }
                }
            }
            lastUppercase = true;
        } else {
            lastUppercase = false;
        }

        result.append(ch);
    }
    return result.toString();
}

Upvotes: 4

user180100
user180100

Reputation:

See this question and CaseFormat from guava

in your case, something like:

CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, "SomeInput");

Upvotes: 204

argenkiwi
argenkiwi

Reputation: 2427

I've had to implement this to convert some keys in camel case format to lower case with underscores. The regular expression I came up with is:

(?<!^|_|[A-Z])([A-Z])

In english it stands for capital letter which is not preceded by the start of the string, an underscore or another capital letter.

In the samples below, the character in bold are the ones that should produce a match using the aforementioned regular expression:

  • CamelCaseToSomethingElse
  • camelCaseToSomethingElse
  • camel_case_to_something_else
  • Camel_Case_To_Something_Else
  • CAMEL_CASE_TO_SOMETHING_ELSE

Notice the expression does not affect string that are already in lower case + underscore format.

The replacement pattern would be:

_l$1

Which means lower case of first capturing group, first capturing group being the capital letter. You could lower case the whole string afterwards as well to normalize the last two samples from the list above.

Upvotes: 0

radzimir
radzimir

Reputation: 1338

I can't provide RegEx, it would be insanely complex anyway.

Try this function with automatic recognition of acronyms.

Unfortunately Guava lib doesn't auto detect upper case acronyms, so "bigCAT" would be converted to "BIG_C_A_T"

/**
 * Convert to UPPER_UNDERSCORE format detecting upper case acronyms
 */
private String upperUnderscoreWithAcronyms(String name) {
    StringBuffer result = new StringBuffer();
    boolean begin = true;
    boolean lastUppercase = false;
    for( int i=0; i < name.length(); i++ ) {
        char ch = name.charAt(i);
        if( Character.isUpperCase(ch) ) {
            // is start?
            if( begin ) {
                result.append(ch);
            } else {
                if( lastUppercase ) {
                    // test if end of acronym
                    if( i+1<name.length() ) {
                        char next = name.charAt(i+1);
                        if( Character.isUpperCase(next) ) {
                            // acronym continues
                            result.append(ch);
                        } else {
                            // end of acronym
                            result.append('_').append(ch);
                        }
                    } else {
                        // acronym continues
                        result.append(ch);
                    }
                } else {
                    // last was lowercase, insert _
                    result.append('_').append(ch);
                }
            }
            lastUppercase=true;
        } else {
            result.append(Character.toUpperCase(ch));
            lastUppercase=false;
        }
        begin=false;
    }
    return result.toString();
}

Upvotes: 7

abinash sahu
abinash sahu

Reputation: 21

public class ReplaceFromCameltoSnake {
    public static void main(String args[]){
        String s1=" totalAmountWithoutDiscount";  
        String replaceString=s1.replaceAll("([A-Z]+)","\\_$1").toLowerCase(); 
        System.out.println(replaceString);  
    }
}

Upvotes: 2

clevertension
clevertension

Reputation: 7077

bind the lower case and upper case as two group,it will be ok

public  class Main
{
    public static void main(String args[])
    {
        String regex = "([a-z])([A-Z]+)";
        String replacement = "$1_$2";
        System.out.println("CamelCaseToSomethingElse"
                           .replaceAll(regex, replacement)
                           .toLowerCase());
    }
}

Upvotes: 75

Sandeep Vaid
Sandeep Vaid

Reputation: 1459

You can use below code snippet:

String replaceAll = key.replaceAll("(.)(\\p{Upper})", "$1_$2").toLowerCase();

Upvotes: 45

Brett Ryan
Brett Ryan

Reputation: 28255

Why not simply match prior character as a not start of line $?

String text = "CamelCaseToSomethingElse";
System.out.println(text.replaceAll("([^_A-Z])([A-Z])", "$1_$2"));

Note that this version is safe to be performed on something that is already camel cased.

Upvotes: 5

Has QUIT--Anony-Mousse
Has QUIT--Anony-Mousse

Reputation: 77454

Add a zero-width lookahead assertion.

http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html

Read the documentation for (?=X) etc.

Personally, I would actually split the string, then recombine it. This may even be faster when done right, and it makes the code much easier to understand than regular expression magic. Don't get me wrong: I love regular expressions. But this isn't really a neat regular expression, nor is this transformation a classic regexp task. After all it seems you also want to do lowercase?

An ugly but quick hack would be to replace (.)([A-Z]+) with $1_$2 and then lowercase the whole string afterwards (unless you can do perl-style extrended regexps, where you can lowercase the replacement directly!). Still I consider splitting at lower-to-upper transition, then transforming, then joining as the proper and most readable way of doing this.

Upvotes: 3

Jack
Jack

Reputation: 5768

([A-Z][a-z\d]+)(?=([A-Z][a-z\d]+))

Should search for a capital letter followed by lowercase letters. The positive lookahead will look for another word starting with a capital letter followed by lowercase letters but will NOT include it in the match.

Look here: http://regexr.com?30ooo

Upvotes: 1

Related Questions