Can I replace groups in Java regex?

JavaRegexReplaceRegex Group

Java Problem Overview


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.

    }
 

Java Solutions


Solution 1 - Java

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
    String output = m.replaceFirst("number $3$1");  // number 46
}

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.

Solution 2 - Java

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.

Solution 3 - Java

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.

Solution 4 - Java

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

Solution 5 - Java

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, "**"));
  }

Solution 6 - Java

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();		
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionwokenaView Question on Stackoverflow
Solution 1 - JavaChadwickView Answer on Stackoverflow
Solution 2 - JavaacdcjuniorView Answer on Stackoverflow
Solution 3 - JavaYaroView Answer on Stackoverflow
Solution 4 - JavaydannegView Answer on Stackoverflow
Solution 5 - JavawhimmyView Answer on Stackoverflow
Solution 6 - JavaJonas_HessView Answer on Stackoverflow