1

I have a file name with this format yy_MM_someRandomString_originalFileName.

example:

02_01_fEa3129E_my Pic.png

I want replace the first 2 underscores with / so that the example becomes:

02/01/fEa3129E_my Pic.png

That can be done with replaceAll, but the problem is that files may contain underscores as well.

@Test
void test() {

    final var input = "02_01_fEa3129E_my Pic.png";

    final var formatted = replaceNMatches(input, "_", "/", 2);

    assertEquals("02/01/fEa3129E_my Pic.png", formatted);
}

private String replaceNMatches(String input, String regex,
                               String replacement, int numberOfTimes) {
    for (int i = 0; i < numberOfTimes; i++) {
        input = input.replaceFirst(regex, replacement);
    }
    return input;
}

I solved this using a loop, but is there a pure regex way to do this?

EDIT: this way should be able to let me change a parameter and increase the amount of underscores from 2 to n.

1
  • input.replaceFirst("^([^_]*)_([^_]*)_", "$1/$2/") Commented Jul 27, 2019 at 15:24

2 Answers 2

3

You could use 2 capturing groups and use those in the replacement where the match of the _ will be replaced by /

^([^_]+)_([^_]+)_

Replace with:

$1/$2/

Regex demo | Java demo

For example:

String regex = "^([^_]+)_([^_]+)_";
String string = "02_01_fEa3129E_my Pic.png";
String subst = "$1/$2/";

Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(string);
String result = matcher.replaceFirst(subst);

System.out.println(result);

Result

02/01/fEa3129E_my Pic.png
Sign up to request clarification or add additional context in comments.

2 Comments

Wouldn't I have to completely rewrite the regex if I wanted the first 3 underscores . instead?
@RyanS. You could add a third group and also refer to that in the replacement regex101.com/r/3l0fMR/2
2

Your current solution has few problems:

  1. It is inefficient - because each replaceFirst need to start from beginning of string so it needs to iterate over same starting characters many times.

  2. It has a bug - because of point 1. while iterating from beginning instead of last modified place, we can replace value which was inserted previously.

    For instance if we want to replace single character two times, each with X like abc -> XXc after code like

    String input = "abc";
    input = input.replaceFirst(".", "X"); // replaces a with X -> Xbc
    input = input.replaceFirst(".", "X"); // replaces X with X -> Xbc
    

    we will end up with Xbc instead of XXc because second replaceFirst will replace X with X instead of b with X.

To avoid that kind of problems you can rewrite your code to use Matcher#appendReplacement and Matcher#appendTail methods which ensures that we will iterate over input once and can replace each matched part with value we want

private static String replaceNMatches(String input, String regex,
                               String replacement, int numberOfTimes) {

    Matcher m = Pattern.compile(regex).matcher(input);
    StringBuilder sb = new StringBuilder();
    int i = 0;
    while(i++ < numberOfTimes && m.find() ){
        m.appendReplacement(sb, replacement); // replaces currently matched part with replacement, 
                                              // and writes replaced version to StringBuilder 
                                              // along with text before the match
    }
    m.appendTail(sb); //lets add to builder text after last match
    return sb.toString();
}

Usage example:

System.out.println(replaceNMatches("abcdefgh", "[efgh]", "X", 2)); //abcdXXgh

2 Comments

Brilliant! I was looking at this exact problem as well(looking for a generic solution). I should probably do research into the Matcher class
BTW appendReplacement also supports replacement notation supported by String#replaceAll and String#replaceFirst like $x to reuse match from group x which makes $ special character there. To escape it you would need to use \$ (written in string literal as "\\$"), or let Matcher generate escaped string with Matcher.quoteReplacement.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.