Count actual replacements

Overview

I’ve created a basic text editor with a “Replace all” feature. However, I’m facing an issue where the output indicates replacements were made even when the text remains unchanged. Here’s a simplified version of my code:

const textarea = document.getElementById('textarea');
const searchInput = document.getElementById('search');
const replaceInput = document.getElementById('replace');
const output = document.getElementById('output');
const replaceButton = document.getElementById('replaceAll');

replaceButton.addEventListener('click', () => {
  const searchValue = searchInput.value;
  const replaceValue = replaceInput.value;

  if (!searchValue) {
    output.textContent = 'No search term';
    return;
  }

  const regex = new RegExp(searchValue, 'g');
  const matches = textarea.value.match(regex);

  if (matches) {
    const count = matches.length;
    textarea.value = textarea.value.replace(regex, replaceValue);
    output.textContent = `${count} replacement${count === 1 ? '' : 's'}`;
  } else {
    output.textContent = 'No matches';
  }
});
textarea {
  width: 100%;
  height: 100px;
}

#output {
  color: #666;
}
<textarea id="textarea">apple orange apple orange</textarea>
<input type="text" id="search" placeholder="Search">
<input type="text" id="replace" placeholder="Replace">
<button id="replaceAll">Replace all</button>
<div id="output"></div>

Edge cases

Non-regex search

  • Text: apple orange apple orange
  • Search: apple
  • Replacement: apple
  • Expected output: “No replacement”
  • Current output: “2 replacements”

Regex search

  • Text: 9/1/2021, 3/1/2022
  • Search: (\d*)/\d*/(\d{4})
  • Replacement: $1/1/$2
  • Expected output: “No replacement”
  • Current output: “2 replacements”

Question

How can I modify the code to correctly handle these edge cases and display “No replacement” when the text remains unchanged after the replacement operation?

The issue occurs in the first case because .replace() executes when the search and replacement values are identical, leading to a reported match count even when the text remains unchanged.

To deal with this, you could add a guard clause:

if (searchValue === replaceValue) {
  output.textContent = 'No replacement';
  return;
}

Also in the case of the regex, matches are found (and counted) although nothing changes. Obviously the guard clause will fail when comparing (\d*)/\d*/(\d{4}) and $1/1/$2, so here I would just diff the input with the output and react accordingly:

const regex = new RegExp(searchValue, 'g');
const originalText = textarea.value;
const newText = originalText.replace(regex, replaceValue);

// Check if regex replacement caused any actual change
if (originalText === newText) {
  output.textContent = 'No replacement';
  return;
}

Input: 9/1/2021, 3/1/2022
Search: (\d*)/\d*/(\d{4})
Replace: $1/1/$2
Output: No replacement

Here’s the whole event handler:

replaceButton.addEventListener('click', () => {
  const searchValue = searchInput.value;
  const replaceValue = replaceInput.value;

  if (!searchValue) {
    output.textContent = 'No search term';
    return;
  }

  if (searchValue === replaceValue) {
    output.textContent = 'No replacement';
    return;
  }

  const regex = new RegExp(searchValue, 'g');
  const originalText = textarea.value;
  const newText = originalText.replace(regex, replaceValue);

  if (originalText === newText) {
    output.textContent = 'No replacement';
    return;
  }

  const matches = originalText.match(regex);
  const count = matches ? matches.length : 0;

  textarea.value = newText;
  output.textContent = `${count} replacement${count === 1 ? '' : 's'}`;
});

I’m not sure how viable this solution is for you. While it effectively prevents false positives, it may be inefficient for large text inputs.

As .replace() itself does not have a built-in way to ignore replacements if they result in identical output, an alternative approach might be to prevent unnecessary replacements by using a pre-check before modifying the text.

Something like this (untested):

let changed = false;
const newText = originalText.replace(regex, (match) => {
  if (match === replaceValue) return match; // Ignore identical replacements
  changed = true; // Flag that at least one change happened
  return replaceValue;
});

I would be interested to hear how others might tackle this.

2 Likes

To be entirely fair to the code… it did do 2 replacements. The end result was the same, but it did execute a replacement on the text.

James, your code handles the specific edge case when NO replacements were done, but wouldnt handle a case when SOME replacements were done, but not all replacements resulted in a change. (If the example is:
Input : 9/1/2021, 3/1/2022
Search : (\d*)/\d*/(\d{4})
Replace : $1/1/$2
, change the input to “9/1/2021, 3/2/2022”. I should get 1, not 2.)

What youd have to do is find all matches, duplicate and transform the array, and then count the number of differences elementwise.

1 Like

Oh yeah. Good catch :+1:

In this case the second approach I suggested would also work.

replaceButton.addEventListener('click', () => {
  const searchValue = searchInput.value;
  const replaceValue = replaceInput.value;
  const originalText = textarea.value;

  if (!searchValue) return (output.textContent = 'No search term');

  const regex = new RegExp(searchValue, 'g');
  let count = 0;

  const newText = originalText.replace(regex, (match, ...groups) => {
    const replacement = replaceValue.replace(/\$(\d+)/g, (_, n) => groups[n - 1] || '');
    if (match !== replacement) count++;
    return replacement;
  });

  output.textContent = count ? `${count} replacement${count === 1 ? '' : 's'}` : 'No replacement';
  if (count) textarea.value = newText;
});
4 Likes