131

I have this code, and I want to know, if I can replace only groups (not all pattern) in Java regex. Code:

 //...
 Pattern p = Pattern.compile("(\\d).*(\\d)");
    String input = "6 example input 4";
    Matcher m = p.matcher(input);
    if (m.find()) {

        //Now I want replace group one ( (\\d) ) with number 
       //and group two (too (\\d) ) with 1, but I don't know how.

    }
ekad
  • 14,436
  • 26
  • 44
  • 46
wokena
  • 1,301
  • 4
  • 15
  • 17

7 Answers7

156

Use $n (where n is a digit) to refer to captured subsequences in replaceFirst(...). I'm assuming you wanted to replace the first group with the literal string "number" and the second group with the value of the first group.

Pattern p = Pattern.compile("(\\d)(.*)(\\d)");
String input = "6 example input 4";
Matcher m = p.matcher(input);
if (m.find()) {
    // replace first number with "number" and second number with the first
    // the added group ("(.*)" which is $2) captures unmodified text to include it in the result
    String output = m.replaceFirst("number$2$1"); // "number example input 6"
}

Consider (\D+) for the second group instead of (.*). * is a greedy matcher, and will at first consume the last digit. The matcher will then have to backtrack when it realizes the final (\d) has nothing to match, before it can match to the final digit.

Edit

Years later, this still gets votes, and the comments and edits (which broke the answer) show there is still confusion on what the question meant. I've fixed it, and added the much needed example output.

The edits to the replacement (some thought $2 should not be used) actually broke the answer. Though the continued votes shows the answer hits the key point - Use $n references within replaceFirst(...) to reuse captured values - the edits lost the fact that unmodified text needs to be captured as well, and used in the replacement so that "only groups (not all pattern)".

The question, and thus this answer, is not concerned with iterating. This is intentionally an MRE.

Chadwick
  • 12,555
  • 7
  • 49
  • 66
  • 12
    Would have been nice if you would have posted an example output – winklerrr Aug 25 '15 at 10:33
  • 7
    This works on the first match, but wont work if there are many groups and you are iterating over them with a while(m.find()) – Hugo Zaragoza Jan 03 '16 at 21:50
  • 2
    I Agree with Hugo, this is a terrible way to implement the solution... Why on Earth is this the accepted answer and not acdcjunior's answer - which is the perfect solution: small amount of code, high cohesion and low coupling, much less chance (if not no chance) of unwanted side effects... *sigh*... – FireLight Jun 11 '17 at 09:07
  • This answer is currently not valid. The `m.replaceFirst("number $2$1");` should be `m.replaceFirst("number $3$1");` – Daniel Eisenreich Jan 10 '18 at 09:30
  • this is answering the first group, not the OP quesiton "if I can replace only groups" unlike @acdcjunior 's response – desgraci Sep 28 '21 at 06:35
  • 1
    if we want the output proposed in this answer "number example input 6" to avoid confusion with a group nr $n, you can name groups e.g. start, middle, end: ```Pattern p = Pattern.compile("(?\\d)(?.*)(?\\d)");``` then refer: ```m.replaceFirst("number${middle}${start}");``` – Piotr Boho Sep 01 '23 at 15:58
74

You could use Matcher#start(group) and Matcher#end(group) to build a generic replacement method:

public static String replaceGroup(String regex, String source, int groupToReplace, String replacement) {
    return replaceGroup(regex, source, groupToReplace, 1, replacement);
}

public static String replaceGroup(String regex, String source, int groupToReplace, int groupOccurrence, String replacement) {
    Matcher m = Pattern.compile(regex).matcher(source);
    for (int i = 0; i < groupOccurrence; i++)
        if (!m.find()) return source; // pattern not met, may also throw an exception here
    return new StringBuilder(source).replace(m.start(groupToReplace), m.end(groupToReplace), replacement).toString();
}

public static void main(String[] args) {
    // replace with "%" what was matched by group 1 
    // input: aaa123ccc
    // output: %123ccc
    System.out.println(replaceGroup("([a-z]+)([0-9]+)([a-z]+)", "aaa123ccc", 1, "%"));

    // replace with "!!!" what was matched the 4th time by the group 2
    // input: a1b2c3d4e5
    // output: a1b2c3d!!!e5
    System.out.println(replaceGroup("([a-z])(\\d)", "a1b2c3d4e5", 2, 4, "!!!"));
}

Check online demo here.

acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • 2
    This really should be the accepted answer it's the most complete and "ready to go" solution without introducing a level of coupling to the accompanying code. Although I would recommend changing the method names of one of those. At first glance it looks like a recursive call in the first method. – FireLight Jun 11 '17 at 09:09
  • Missed edit opportunity. Take back the part about the recursive call, didn't analyse the code properly. The overloads work well together – FireLight Jun 11 '17 at 09:56
  • This solution out-of-the-box is fit only to replace single occurence and one group, and because of copying of the full string with each replace would be highly suboptimal for any other purpose. But it's a good starting point. A pity Java hat a lot of nonsense, but is lacking basic string manipulation facilities. – 9ilsdx 9rvj 0lo Oct 30 '20 at 12:52
40

Sorry to beat a dead horse, but it is kind-of weird that no-one pointed this out - "Yes you can, but this is the opposite of how you use capturing groups in real life".

If you use Regex the way it is meant to be used, the solution is as simple as this:

"6 example input 4".replaceAll("(?:\\d)(.*)(?:\\d)", "number$11");

Or as rightfully pointed out by shmosel below,

"6 example input 4".replaceAll("\d(.*)\d", "number$11");

...since in your regex there is no good reason to group the decimals at all.

You don't usually use capturing groups on the parts of the string you want to discard, you use them on the part of the string you want to keep.

If you really want groups that you want to replace, what you probably want instead is a templating engine (e.g. moustache, ejs, StringTemplate, ...).


As an aside for the curious, even non-capturing groups in regexes are just there for the case that the regex engine needs them to recognize and skip variable text. For example, in

(?:abc)*(capture me)(?:bcd)*

you need them if your input can look either like "abcabccapture mebcdbcd" or "abccapture mebcd" or even just "capture me".

Or to put it the other way around: if the text is always the same, and you don't capture it, there is no reason to use groups at all.

Yaro
  • 668
  • 5
  • 7
4

replace the password fields from the input:

{"_csrf":["9d90c85f-ac73-4b15-ad08-ebaa3fa4a005"],"originPassword":["uaas"],"newPassword":["uaas"],"confirmPassword":["uaas"]}



  private static final Pattern PATTERN = Pattern.compile(".*?password.*?\":\\[\"(.*?)\"\\](,\"|}$)", Pattern.CASE_INSENSITIVE);

  private static String replacePassword(String input, String replacement) {
    Matcher m = PATTERN.matcher(input);
    StringBuffer sb = new StringBuffer();
    while (m.find()) {
      Matcher m2 = PATTERN.matcher(m.group(0));
      if (m2.find()) {
        StringBuilder stringBuilder = new StringBuilder(m2.group(0));
        String result = stringBuilder.replace(m2.start(1), m2.end(1), replacement).toString();
        m.appendReplacement(sb, result);
      }
    }
    m.appendTail(sb);
    return sb.toString();
  }

  @Test
  public void test1() {
    String input = "{\"_csrf\":[\"9d90c85f-ac73-4b15-ad08-ebaa3fa4a005\"],\"originPassword\":[\"123\"],\"newPassword\":[\"456\"],\"confirmPassword\":[\"456\"]}";
    String expected = "{\"_csrf\":[\"9d90c85f-ac73-4b15-ad08-ebaa3fa4a005\"],\"originPassword\":[\"**\"],\"newPassword\":[\"**\"],\"confirmPassword\":[\"**\"]}";
    Assert.assertEquals(expected, replacePassword(input, "**"));
  }
whimmy
  • 41
  • 2
3

You can use matcher.start() and matcher.end() methods to get the group positions. So using this positions you can easily replace any text.

ydanneg
  • 71
  • 3
3

Here is a different solution, that also allows the replacement of a single group in multiple matches. It uses stacks to reverse the execution order, so the string operation can be safely executed.

private static void demo () {

    final String sourceString = "hello world!";

    final String regex = "(hello) (world)(!)";
    final Pattern pattern = Pattern.compile(regex);

    String result = replaceTextOfMatchGroup(sourceString, pattern, 2, world -> world.toUpperCase());
    System.out.println(result);  // output: hello WORLD!
}

public static String replaceTextOfMatchGroup(String sourceString, Pattern pattern, int groupToReplace, Function<String,String> replaceStrategy) {
    Stack<Integer> startPositions = new Stack<>();
    Stack<Integer> endPositions = new Stack<>();
    Matcher matcher = pattern.matcher(sourceString);

    while (matcher.find()) {
        startPositions.push(matcher.start(groupToReplace));
        endPositions.push(matcher.end(groupToReplace));
    }
    StringBuilder sb = new StringBuilder(sourceString);
    while (! startPositions.isEmpty()) {
        int start = startPositions.pop();
        int end = endPositions.pop();
        if (start >= 0 && end >= 0) {
            sb.replace(start, end, replaceStrategy.apply(sourceString.substring(start, end)));
        }
    }
    return sb.toString();       
}
Jonas_Hess
  • 1,874
  • 1
  • 22
  • 32
0

Since Java 9 you can use Matcher.replaceAll. The usage is as follows:

Pattern p = Pattern.compile("(\\d)(.*)(\\d)");
String input = "6 example input 4";
Matcher matcher = p.matcher(input);
String output = matcher.replaceAll(matchResult -> "%s%s%s".formatted("number", matchResult.group(2), matchResult.group(1) ));

output should be equal to number example input 6

matchResult.group(0) is the whole pattern, so groups are indexed from 1

Maciek Murawski
  • 414
  • 4
  • 15