Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternative to successive String.replace

I want to replace some strings in a String input :

string=string.replace("<h1>","<big><big><big><b>");
string=string.replace("</h1>","</b></big></big></big>");
string=string.replace("<h2>","<big><big>");
string=string.replace("</h2>","</big></big>");
string=string.replace("<h3>","<big>");
string=string.replace("</h3>","</big>");
string=string.replace("<h4>","<b>");
string=string.replace("</h4>","</b>");
string=string.replace("<h5>","<small><b>");
string=string.replace("</h5>","</b><small>");
string=string.replace("<h6>","<small>");
string=string.replace("</h6>","</small>");

As you can see this approach is not the best, because each time I have to search for the portion to replace etc, and Strings are immutable... Also the input is large, which means that some performance issues are to be considered.

Is there any better approach to reduce the complexity of this code ?

like image 700
TheByeByeMan Avatar asked Nov 04 '14 12:11

TheByeByeMan


People also ask

How do you replace a string with a different string?

Description: The WorksheetFunction.Replace method replaces a substring with a different string (ReplacementString). The replaced substring is determined based on its position within the string you work with (CharacterPosition) and the number of characters to replace (CharactersToReplace).

What is an alternative to string that is case-insensitive in C#?

What is an alternative to string.Replace that is case-insensitive in C#? Replace () method is a string method. This method is used to replace all the specified Unicode characters or specified string from the current string object and returns a new modified string. This method can be overloaded by passing arguments to it.

How to replace multiple characters in a string with VBA?

As expected, the macro replaces all occurrences of the character “a” with the character “e” within the string in cell A8. To replace multiple characters in a string with VBA, use a macro with the following statement structure:

How to replace a character in a string according to position?

To replace a character in a string within a cell according to its position with VBA, use a statement with the following structure: Cell.Value = WorksheetFunction.Replace(Cell.Value, CharacterPosition, CharactersToReplace, ReplacementString) Process Followed by VBA Code to Replace Character in String by Position


6 Answers

Although StringBuilder.replace() is a huge improvement compared to String.replace(), it is still very far from being optimal.

The problem with StringBuilder.replace() is that if the replacement has different length than the replaceable part (applies to our case), a bigger internal char array might have to be allocated, and the content has to be copied, and then the replace will occur (which also involves copying).

Imagine this: You have a text with 10.000 characters. If you want to replace the "XY" substring found at position 1 (2nd character) to "ABC", the implementation has to reallocate a char buffer which is at least larger by 1, has to copy the old content to the new array, and it has to copy 9.997 characters (starting at position 3) to the right by 1 to fit "ABC" into the place of "XY", and finally characters of "ABC" are copied to the starter position 1. This has to be done for every replace! This is slow.

Faster Solution: Building Output On-The-Fly

We can build the output on-the-fly: parts that don't contain replaceable texts can simply be appended to the output, and if we find a replaceable fragment, we append the replacement instead of it. Theoretically it's enough to loop over the input only once to generate the output. Sounds simple, and it's not that hard to implement it.

Implementation:

We will use a Map preloaded with mappings of the replaceable-replacement strings:

Map<String, String> map = new HashMap<>();
map.put("<h1>", "<big><big><big><b>");
map.put("</h1>", "</b></big></big></big>");
map.put("<h2>", "<big><big>");
map.put("</h2>", "</big></big>");
map.put("<h3>", "<big>");
map.put("</h3>", "</big>");
map.put("<h4>", "<b>");
map.put("</h4>", "</b>");
map.put("<h5>", "<small><b>");
map.put("</h5>", "</b></small>");
map.put("<h6>", "<small>");
map.put("</h6>", "</small>");

And using this, here is the replacer code: (more explanation after the code)

public static String replaceTags(String src, Map<String, String> map) {
    StringBuilder sb = new StringBuilder(src.length() + src.length() / 2);

    for (int pos = 0;;) {
        int ltIdx = src.indexOf('<', pos);
        if (ltIdx < 0) {
            // No more '<', we're done:
            sb.append(src, pos, src.length());
            return sb.toString();
        }

        sb.append(src, pos, ltIdx); // Copy chars before '<'
        // Check if our hit is replaceable:
        boolean mismatch = true;
        for (Entry<String, String> e : map.entrySet()) {
            String key = e.getKey();
            if (src.regionMatches(ltIdx, key, 0, key.length())) {
                // Match, append the replacement:
                sb.append(e.getValue());
                pos = ltIdx + key.length();
                mismatch = false;
                break;
            }
        }
        if (mismatch) {
            sb.append('<');
            pos = ltIdx + 1;
        }
    }
}

Testing it:

String in = "Yo<h1>TITLE</h1><h3>Hi!</h3>Nice day.<h6>Hi back!</h6>End";
System.out.println(in);
System.out.println(replaceTags(in, map));

Output: (wrapped to avoid scroll bar)

Yo<h1>TITLE</h1><h3>Hi!</h3>Nice day.<h6>Hi back!</h6>End

Yo<big><big><big><b>TITLE</b></big></big></big><big>Hi!</big>Nice day.
<small>Hi back!</small>End

This solution is faster than using regular expressions as that involves much overhead, like compiling a Pattern, creating a Matcher etc. and regexp is also much more general. It also creates many temporary objects under the hood which are thrown away after the replace. Here I only use a StringBuilder (plus char array under its hood) and the code iterates over the input String only once. Also this solution is much faster that using StringBuilder.replace() as detailed at the top of this answer.

Notes and Explanation

I initialized the StringBuilder in the replaceTags() method like this:

StringBuilder sb = new StringBuilder(src.length() + src.length() / 2);

So basically I created it with an initial capacity of 150% of the length of the original String. This is because our replacements are longer than the replaceable texts, so if replacing occurs, the output will obviously be longer than the input. Giving a larger initial capacity to StringBuilder will result in no internal char[] reallocation at all (of course the required initial capacity depends on the replaceable-replacement pairs and their frequency/occurrence in the input, but this +50% is a good upper estimation).

I also utilized the fact that all replaceable strings start with a '<' character, so finding the next potential replaceable position becomes blazing-fast:

int ltIdx = src.indexOf('<', pos);

It's just a simple loop and char comparisons inside String, and since it always starts searching from pos (and not from the start of the input), overall the code iterates over the input String only once.

And finally to tell if a replaceable String does occur at the potential position, we use the String.regionMatches() method to check the replaceable stings which is also blazing-fast as all it does is just compares char values in a loop and returns at the very first mismatching character.

And a PLUS:

The question doesn't mention it, but our input is an HTML document. HTML tags are case-insensitive which means the input might contain <H1> instead of <h1>.
To this algorithm this is not a problem. The regionMatches() in the String class has an overload which supports case-insensitive comparison:

boolean regionMatches(boolean ignoreCase, int toffset, String other,
                          int ooffset, int len);

So if we want to modify our algorithm to also find and replace input tags which are the same but are written using different letter case, all we have to modify is this one line:

if (src.regionMatches(true, ltIdx, key, 0, key.length())) {

Using this modified code, replaceable tags become case-insensitive:

Yo<H1>TITLE</H1><h3>Hi!</h3>Nice day.<H6>Hi back!</H6>End
Yo<big><big><big><b>TITLE</b></big></big></big><big>Hi!</big>Nice day.
<small>Hi back!</small>End
like image 183
icza Avatar answered Sep 24 '22 04:09

icza


For performance - use StringBuilder. For convenience you can use Map to store values and replacements.

Map<String, String> map = new HashMap<>();
map.put("<h1>","<big><big><big><b>");
map.put("</h1>","</b></big></big></big>");
map.put("<h2>","<big><big>");
...
StringBuilder builder = new StringBuilder(yourString);
for (String key : map.keySet()) {
    replaceAll(builder, key, map.get(key));
}

... To replace all occurences in StringBuilder you can check here: Replace all occurrences of a String using StringBuilder?

public static void replaceAll(StringBuilder builder, String from, String to)
{
    int index = builder.indexOf(from);
    while (index != -1)
    {
        builder.replace(index, index + from.length(), to);
        index += to.length(); // Move to the end of the replacement
        index = builder.indexOf(from, index);
    }
}
like image 37
Heisenberg Avatar answered Sep 22 '22 04:09

Heisenberg


Unfortunately StringBuilder doesn't provide a replace(string,string) method, so you might want to consider using Pattern and Matcher in conjunction with StringBuffer:

String input = ...;
StringBuffer sb = new StringBuffer();

Pattern p = Pattern.compile("</?(h1|h2|...)>");
Matcher m = p.matcher( input );
while( m.find() )
{
  String match = m.group();
  String replacement = ...; //get replacement for match, e.g. by lookup in a map

  m.appendReplacement( sb, replacement );
}
m.appendTail( sb );

You could do something similar with StringBuilder but in that case you'd have to implement appendReplacement etc. yourself.

As for the expression you could also just try and match any html tag (although that might cause problems since regex and arbitrary html don't fit very well) and when the lookup doesn't have any result you just replace the match with itself.

like image 42
Thomas Avatar answered Sep 25 '22 04:09

Thomas


The particular example you provide seems to be HTML or XHTML. Trying to edit HTML or XML using regular expressions is frought with problems. For the kind of editing you seem to be interested in doing you should look at using XSLT. Another possibility is to use SAX, the streaming XML parser, and have your back-end write the edited output on the fly. If the text is actually HTML, you might be better using a tolerant HTML parser, such as JSoup, to build a parsed representation of the document (like the DOM), and manipulate that before outputting it.

like image 21
Raedwald Avatar answered Sep 24 '22 04:09

Raedwald


StringBuilder is backed by a char array. So, unlike String instances, it is mutable. Thus, you can call indexOf() and replace() on the StringBuilder.

like image 24
TheLostMind Avatar answered Sep 25 '22 04:09

TheLostMind


I would do something like this

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        if (tagEquals(str, i, "h1")) {
            sb.append("<big><big><big><b>");
            i += 2;
        } else (tagEquals(s, i, "/h1")) { 
            ...
        } else {
            sb.append(str.charAt(i));
        }
    }

tagEquals is a func which checks a tag name

like image 3
Evgeniy Dorofeev Avatar answered Sep 24 '22 04:09

Evgeniy Dorofeev