2

I would like to do a case-insensitive string replace-all in JavaScript without using a regex (or regex-style strings in calls to the replace method). I could not find a question or answer for this, but please link it if I missed it.

e.g., replace 'abc' with 'x' in:

Find aBc&def stuff ABCabc becomes Find x&def stuff xx

The result should retain the original case for the portions not replaced.

The string may have special characters in it, so this is why I'm avoiding the regex. My particular problem may be solvable with a regex, but I'm interested in avoiding it completely.

There are several questions and answers that use a regex, and include handling of the special characters. In particular, bobince's answer here https://stackoverflow.com/a/280837/292060 describes how it may be impossible without knowing or acting on specific conditions in the original string.

I'm thinking it will involve a loop and indexOf, and walking through the original string, building a result.

For the sake of this question, let's say performance is not a primary concern. e.g., looping characters is ok to do.

There are some existing questions that include regex for all the answers:

EDIT:
From some of the answers, some clarifications — I didn't spec these originally, but they are typical search/replace behavior:

Can replace with same string, e.g., replace 'abc' with 'Abc', say to fix the title case for a name.

The replacement shouldn't be re-checked, e.g., replacing 'ab' with 'abc' should work. e.g., replacing 'abc' with 'ab' in abcc becomes abc not ab.

I think these boil down to the replacement should be done, then move on in the string, without "looking back".

EDIT: Here are some test cases just for the record. I didn't get into empty strings, etc., which probably should get tested too. https://jsfiddle.net/k364st09/1/

("Find aBc&def abc", "abc", "xy")   - Find xy&def xy - general test
("Find aBc&def abc", "abc", "ABC")  - Find ABC&def ABC - replace same test, avoid infinite loop
("Find aBcc&def abc", "abc", "ab")  - Find abc&def ab - "move on" avoid double checking (fails if abcc becomes ab)
("abc def", "abc", "xy")            - xy def - Don't drop last characters.
("abcc def", "abc", "xy")           - xyc def  - Just a mix of "move on" and "don't drop last".
5
  • Good answers so far, I want to try them before selecting an answer. I want to make sure of the index as it replaces the different-sized replacement. Also whether the replacement should be "re-checked" (probably not, since that's not typical in a search/replace). Commented Apr 21, 2015 at 21:33
  • If I understand correctly why you don't want to use a regex (because a user could enter anhthing, which could contain regex characters) you could potentially escape regex characters. I know jquery-ui does this in its autocomplete. Commented Apr 22, 2015 at 15:22
  • It's mainly from this discussion from bobince's answer here stackoverflow.com/a/280837/292060. This is going to be wide-open input from users for a search engine, and I can't get consensus on what characters can be allowed and what can't. So I'm just looking to duck it. Commented Apr 22, 2015 at 17:21
  • Well, I went ahead and made an interactive version so you can try out a bunch of different strings. It shows you the results with the indexOf method and with an escaped regex replace. The regex code is a bit simpler, but this will help to see if you can break it. Commented Apr 22, 2015 at 18:10
  • I'll check it out, thanks. It may be "just" an exercise, but it's a good one. Commented Apr 22, 2015 at 18:19

5 Answers 5

2
  1. Start with an empty string and copy the original string.
  2. Find the index of the string to replace in the copy (setting them both to lowercase makes the search case-insensitive).
  3. If it's not in the copy, skip to step 7.
  4. Add everything from the copy up to the index, plus the replacement.
  5. Trim the copy to everything after the part you're replacing.
  6. Go back to step 2.
  7. Add what's left of the copy.

Just for fun I've created an interactive version where you can see the results of both a regex and indexOf, to see if escaping a regex breaks anything. The method used to escape the regex I took from jQuery UI. If you have it included on the page it can be found with $.ui.autocomplete.escapeRegex. Otherwise, it's a pretty small function.

Here's the non-regex function, but since the interactive section adds a lot more code I have the full code snippet hidden by default.

function insensitiveReplaceAll(original, find, replace) {
  var str = "",
    remainder = original,
    lowFind = find.toLowerCase(),
    idx;

  while ((idx = remainder.toLowerCase().indexOf(lowFind)) !== -1) {
    str += remainder.substr(0, idx) + replace;

    remainder = remainder.substr(idx + find.length);
  }

  return str + remainder;
}

// example call:
insensitiveReplaceAll("Find aBcc&def stuff ABCabc", "abc", "ab");

function insensitiveReplaceAll(original, find, replace) {
  var str = "",
    remainder = original,
    lowFind = find.toLowerCase(),
    idx;

  while ((idx = remainder.toLowerCase().indexOf(lowFind)) !== -1) {
    str += remainder.substr(0, idx) + replace;

    remainder = remainder.substr(idx + find.length);
  }

  return str + remainder;
}

function escapeRegex(value) {
  return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}

function updateResult() {
  var original = document.getElementById("original").value || "",
    find = document.getElementById("find").value || "",
    replace = document.getElementById("replace").value || "",
    resultEl = document.getElementById("result"),
    regexEl = document.getElementById("regex");

  if (original && find && replace) {
    regexEl.value = original.replace(new RegExp(escapeRegex(find), "gi"), replace);
    resultEl.value = insensitiveReplaceAll(original, find, replace);
  } else {
    regexEl.value = "";
    resultEl.value = "";
  }


}

document.addEventListener("input", updateResult);
window.addEventListener("load", updateResult);
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" />

<div class="input-group input-group-sm">
  <span class="input-group-addon">Original</span>
  <input class="form-control" id="original" value="Find aBcc&def stuff ABCabc" />
</div>

<div class="input-group input-group-sm">
  <span class="input-group-addon">Find</span>
  <input class="form-control" id="find" value="abc" />
</div>

<div class="input-group input-group-sm">
  <span class="input-group-addon">Replace</span>
  <input class="form-control" id="replace" value="ab" />
</div>

<div class="input-group input-group-sm">
  <span class="input-group-addon">Result w/o regex</span>
  <input disabled class="form-control" id="result" />
</div>

<div class="input-group input-group-sm">
  <span class="input-group-addon">Result w/ regex</span>
  <input disabled class="form-control" id="regex" />
</div>

Sign up to request clarification or add additional context in comments.

3 Comments

I like this for the simple explicit before-replace-after, but I edited the question to avoid "re-checking". This might just need the indexOf to start where it left off. That would also handle the infinite loop if the replacement is the same as the original.
@goodeye, Nice catch. I've fixed that. Also fixed another bug, where it wasn't setting the find string to lowercase, so it wasn't really case insensitive. Also could have run into issues if toLowerCase produced different characters (e.g. "ß".toUpperCase() === "SS"), but that is fixed now as well.
Thanks for the work. The only thing I could fail on the regex (and it's a stretch) is the html thing - e.g., if you had a <body> tag, and wanted to replace the words body with something - but that's really out of the realm of this. I think there are some tricky cases where you want to work with regex strings themselves, but again it's a reach. So, I agree regex would work in every practical case, but thank you for the attention on this - I do like avoiding it better anyway!
2

The approved solution calls toLowerCase inside the loop which is not efficient.

Below is an improved version:

function insensitiveReplaceAll(s, f, r) {
  const lcs=s.toLowerCase(), lcf = f.toLowerCase(), flen=f.length;
  let res='', pos=0, next=lcs.indexOf(lcf, pos);
  if (next===-1) return s;
  
  do {
    res+=s.substring(pos, next)+r;
    pos=next+flen;
  } while ((next=lcs.indexOf(lcf, pos)) !== -1);
  
  return res+s.substring(pos);
}

console.log(insensitiveReplaceAll("Find aBc&deF abcX", "abc", "xy"));
console.log(insensitiveReplaceAll("hello", "abc", "xy"));

Testing with jsPerf - https://jsperf.com/replace-case-insensitive-2/1 - shows it to be 37% faster.

Comments

1
var s="aBc&def stuff ABCabc"
var idx = s.toUpperCase().indexOf("ABC");
while(idx!==-1){
  s = s.substr(0,idx)+"x"+s.substr(idx+2);
  idx = s.toUpperCase().indexOf("ABC");
}

1 Comment

This is the basic idea, but the +2 should be +3 (and really not be hard-coded to this exact example). Also, it has an infinite loop if the replacement is the same as the original.
1
function replace(s, q, r) {
  var result = '';
  for (var i = 0; i < s.length; i++) {
    var j = 0;
    for (; j < q.length; j++) {
      if (s[i + j].toLowerCase() != q[j].toLowerCase()) break;
    }
    if (j == q.length) {
      i += q.length - 1;
      result += r;
    } else {
      result += s[i];
    }
  }
  return result;
}

The function takes in parameters:

  • s - The original string
  • q - The search query
  • r - The replacement string (for each instance of search query)

    1. It works by iterating through each position.

    2. At each position, it will attempt to check for a match (case insensitively through .toLowerCase()).

    3. Each match that it finds, it inserts the replacement string into the result. Otherwise, it simply copies non-matches into the result.

6 Comments

This is close, thanks. I did edit the question to not "re-check". Since this is walking through with an index, I think it just needs to move the index past the replacement.
I don't think that this "re-check"s as it always bases the search/replace on the original string, moving the index past the entire match when one occurs. Since the replacements don't modify the original string, a "re-check" should not occur.
I think it's close - there's something with the -1 adding to i. I can't quite get it in jsfiddle yet. e.g., replace("Find ABCc def", "abc", "ab") should produce Find abc, but produces Find ab. I'll list various test cases. (Also, abc def is dropping the last two chars).
Updated to always iterate through the entire source string. Should fix dropping characters since it would fail to copy the last few chars if it didn't match.
Thanks for the answer - I went with the other one just for the conciseness, but this one appeals for the index counting.
|
0

Hmm, if performance isn't a concern, you'd probably want to loop through the characters of the string to find your desired string for replacement. Something like this, maybe...

for (var x = 0; x < inputString.length-3; x++) {
    if (inputString.toLowerCase.substring(x, x+2) == 'abc') {
        inputString = inputString.substring(0, x-1) + 'x' + inputString.substring(x+3);
        x = x - 2 //because your replacement is shorter you need to back up where the index is
    }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.