27

I have a set of strings that I need to replace, but I need to keep the case of letters.

Both the input words and output words are of the same length.

For example, if I need to replace "abcd" with "qwer", then the following should happen:

"AbcD" translates to "QweR"
"abCd" translates to "qwEr"

and so on.

Right now I'm using JavaScript's replace, but capital letters are lost on translation.

r = new RegExp( "(" + 'asdf' + ")" , 'gi' );
"oooAsdFoooo".replace(r, "qwer");

Any help would be appreciated.

ignacio.munizaga
  • 1,553
  • 1
  • 23
  • 28
  • The target string and replacement are known or they will be dynamic? – pvnarula Jun 23 '13 at 19:53
  • Sorry, I don't understand your question. But this is the case: I am transcribing a text entered by the user. I have a list of rules, like "ca" transcribes to "kb", but I need to keep the capital letters, so "cA" transcribes to "kB". – ignacio.munizaga Jun 23 '13 at 20:01
  • 1
    I would do it in 2 steps, first "oooAsdFoooo".search(r) which returns the index and then handle the case. But I would be excited if there is a way with a regex only – bert Jun 23 '13 at 20:03
  • Do you have a list of specific letter to letter mappings? E.g a==q, A== Q . So if a is the input it is always q (upper or lowercase) – TommyBs Jun 23 '13 at 20:10
  • No @TommyBs, they depend on the surrounding letters, but for the sake of simplicity, let's assume that Yes, they always do. – ignacio.munizaga Jun 23 '13 at 20:24
  • 1
    I found a much simpler and more elegant solution to what seems to be the same problem here: https://stackoverflow.com/questions/28841045/replace-string-char-but-keep-the-case-type – Syknapse Jul 10 '19 at 16:02

12 Answers12

12

Here’s a helper:

function matchCase(text, pattern) {
    var result = '';

    for(var i = 0; i < text.length; i++) {
        var c = text.charAt(i);
        var p = pattern.charCodeAt(i);

        if(p >= 65 && p < 65 + 26) {
            result += c.toUpperCase();
        } else {
            result += c.toLowerCase();
        }
    }

    return result;
}

Then you can just:

"oooAsdFoooo".replace(r, function(match) {
    return matchCase("qwer", match);
});
Ry-
  • 218,210
  • 55
  • 464
  • 476
9

I'll leave this here for reference.

Scenario: case-insensitive search box on list of items, partial match on string should be displayed highlighted but keeping original case.

highlight() {
  const re = new RegExp(this.searchValue, 'gi'); // global, insensitive
  const newText = name.replace(re, `<b>$&</b>`);
  return newText;
}

the $& is the matched text with case

silvio
  • 5,651
  • 2
  • 17
  • 14
2
String.prototype.translateCaseSensitive = function (fromAlphabet, toAlphabet) {
    var fromAlphabet = fromAlphabet.toLowerCase(),
        toAlphabet = toAlphabet.toLowerCase(),
        re = new RegExp("[" + fromAlphabet + "]", "gi");

    return this.replace(re, function (char) {
        var charLower = char.toLowerCase(),
            idx = fromAlphabet.indexOf(charLower);

        if (idx > -1) {
            if (char === charLower) {
                return toAlphabet[idx];
            } else {
                return toAlphabet[idx].toUpperCase();
            }
        } else {
            return char;
        }
    });
};

and

"AbcD".translateCaseSensitive("abcdefg", "qwertyu")

will return:

"QweR"
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • 1
    This replaces individual characters though - the original question is talking about replacing substrings. In your exmaple, the input string doesn't contain "abcdefg", so nothing should be replaced. – jcsanyi Jun 23 '13 at 20:56
  • @jcsanyi Hm. I didn't understand it that way. But it's probable that you're right. – Tomalak Jun 23 '13 at 21:02
2

Here's a replaceCase function:

  1. We turn the input pattern into a regular expression
  2. We have a nested replacer function which iterates through every character
  3. We use regular expression /[A-Z]/ to identify capital letters, otherwise we assume everything is in lowercase

function replaceCase(str, pattern, newStr) {
  const rx = new RegExp(pattern, "ig")
  const replacer = (c, i) => c.match(/[A-Z]/) ? newStr[i].toUpperCase() : newStr[i]
  return str.replace(rx, (oldStr) => oldStr.replace(/./g, replacer) )
}

let out = replaceCase("This is my test string: AbcD", "abcd", "qwer")
console.log(out) // This is my test string: QweR
out = replaceCase("This is my test string: abCd", "abcd", "qwer")
console.log(out) // This is my test string: qwEr
Stephen Quan
  • 21,481
  • 4
  • 88
  • 75
  • May you please write this function in standard jQuery or vanilla JavaScript? EMCA 2015 is not compliant across the all the browsers we support. – Alexander Dixon May 18 '21 at 22:07
1

You could create your own replace function such as

 if(!String.prototype.myreplace){
String.prototype.myreplace = (function(obj){
    return this.replace(/[a-z]{1,1}/gi,function(a,b){
       var r = obj[a.toLowerCase()] || a;
        return a.charCodeAt(0) > 96? r.toLowerCase() : r.toUpperCase();
    });
});
}

This takes in a object that maps different letters. and it can be called such as follows

  var obj = {a:'q',b:'t',c:'w'};

  var s = 'AbCdea';
  var n = s.myreplace(obj);
  console.log(n);

This means you could potentially pass different objects in with different mappings if need be. Here's a simple fiddle showing an example (note the object is all lowercase but the function itself looks at case of the string as well)

TommyBs
  • 9,354
  • 4
  • 34
  • 65
  • It's not recommended to modify prototypes for maintainability. Create an exported function instead and require or import it. More details: https://flaviocopes.com/javascript-why-not-modify-object-prototype/ – Edie Lemoine Aug 12 '21 at 13:48
1

Expanding on Ryan O'Hara's answer, the below solution avoids using charCodes and the issues that maybe encountered in using them. It also ensures the replacement is complete when the strings are of different lengths.

function enforceLength(text, pattern, result) {
  if (text.length > result.length) {  
    result = result.concat(text.substring(result.length, text.length));
  }

  if (pattern.length > text.length) {
    result = result.substring(0, text.length);
  }

  return result;
}

function matchCase(text, pattern){
  var result = '';

  for (var i =0; i < pattern.length; i++){
    var c = text.charAt(i);
    var p = pattern.charAt(i);

    if(p === p.toUpperCase()) {
       result += c.toUpperCase();
    } else {
       result += c.toLowerCase();
    }
  }  
  return enforceLength(text, pattern, result);
}
Barry G
  • 221
  • 4
  • 14
1

This should replace while preserving the case. Please let me know if anyone finds any flaws in this solution. I hope this helps. Thank-you!

function myReplace(str, before, after) {

      var match=function(before,after){
        after=after.split('');
        for(var i=0;i<before.length;i++)
          {

            if(before.charAt(i)==before[i].toUpperCase())
              {
                after[i]=after[i].toUpperCase();
              }
            else  if(before.charAt(i)==before[i].toLowerCase())
              {
                after[i]=after[i].toLowerCase();
              }
            return after.join('');
          }

      };
           console.log(before,match(before,after));
          str =str.replace(before,match(before,after)); 


      return str;
    }

    myReplace("A quick brown fox jumped over the lazy dog", "jumped", "leaped");
Karan Jariwala
  • 727
  • 6
  • 17
1

I had a sentence where I had to replace each word with another word and that word can be longer/shorter than the word its replacing so its similar to the question but instead of a fixed length, they're dynamic.

My solution

For simplicity, I am focusing on a single word.

const oldWord = "tEsT";
const newWord = "testing";

Split both words so that I can iterate over each individual letters.

const oldWordLetters = oldWord.split("");
const newWordLetters = newWord.split("");

Now, I would iterate over the newWord letters and use its index to then get the corresponding oldWord letter in the same position. Then I would check if the old letter is capital and if it is then make the new letter in the same position capital as well.

for (const [i, letter] of newWordLetters.entries()) {
  const oldLetter = oldWordLetters[i];

  // stop iterating if oldWord is shorter (not enough letters to copy case).
  if (!oldLetter) {
    break;
  }

  const isCapital = oldLetter === oldLetter.toUpperCase();

  // make the new letter in the same position as the old letter capital
  if (isCapital) {
    newWordLetters[i] = letter.toUpperCase();
  }
}

The final world would be tEsTing after joining the letters again.

const finalWord = newWordLetters.join("");

console.log(finalWord); // "tEsTing"

Full code

const oldWord = "tEsT";
const newWord = "testing";

const oldWordLetters = oldWord.split("");
const newWordLetters = newWord.split("");

for (const [i, letter] of newWordLetters.entries()) {
  const oldLetter = oldWordLetters[i];

  // stop iterating if oldWord is shorter (not enough letters to copy case).
  if (!oldLetter) {
    break;
  }

  const isCapital = oldLetter === oldLetter.toUpperCase();

  // make the new letter in the same position as the old letter capital
  if (isCapital) {
    newWordLetters[i] = letter.toUpperCase();
  }
}

const finalWord = newWordLetters.join("");

console.log(finalWord);
Eray Chumak
  • 156
  • 1
  • 7
0

I think this could work

function formatItem(text, searchText){
    const search = new RegExp(escapeRegExp(searchText), 'iu')
    return text?.toString().replace(search, (m) => `<b>${m}</b>`)
}

function escapeRegExp(text) {
  return text?.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') ?? '';
}
Pedro
  • 1
0

Thank you for asking this. I had the same problem when I wanted to search text and replace certain words with links, which was a slightly more specific situation because it is replacing text strings with html strings. I'll put my solution here in case anyone who finds this is doing anything similar.

let elmt = document.getElementById('the-element');
let a = document.createElement('a');
a.href = "https://www.example.com";
let re = new RegExp('the string to find', 'gi');
elmt.innerHTML = elmt.innerHTML.replaceAll(re, function (match) {
    a.innerText = match;
    return a.outerHTML;
});

So the regular expression ensures that it searches for case-insensitive matches, and the function as the second argument of the replaceAll function specifies that it is supposed to set the innerText of the new tag equal to the old string verbatim, before then returning the outerHTML of the whole tag.

kloddant
  • 1,026
  • 12
  • 19
0

Here is a replaceAllCaseSensitive function. If your want, you can change replaceAll by replace.

const replaceAllCaseSensitive = (
  text, // Original string
  pattern, // RegExp with the pattern you want match. It must include the g (global) and i (case-insensitive) flags.
  replacement // string with the replacement
) => {
  return text.replaceAll(pattern, (match) => {
    return replacement
      .split("")
      .map((char, i) =>
        match[i] === match[i].toUpperCase() ? char.toUpperCase() : char
      )
      .join("");
  });
};

console.log(replaceAllCaseSensitive("AbcD abCd", /abcd/gi, "qwer"));
// outputs "QweR qwEr"

console.log(replaceAllCaseSensitive("AbcD abCd", /abcd/gi, "qwe"));
// outputs "Qwe qwE"

The function works even if replacement is shorter than match.

Rafael Sanabria
  • 202
  • 3
  • 8
0

I really like the answer from Stephen Quan but found it didn't handle cases where the replacement string is a different length from the match string. Here's my update to his answer

const replaceCase = (str, pattern, newStr) => {
  const rx = new RegExp(pattern, 'ig');
  const replacer = (c, i) => (c.match(/[A-Z]/) ? newStr[i].toUpperCase() : newStr[i] ?? '');
  const [match] = str.match(rx) ?? [];
  return str.replace(rx, (oldStr) => oldStr.replace(/./g, replacer)) + newStr.slice(match?.length ?? 0);
};

And in TypeScript for those who prefer

const replaceCase = (str: string, pattern: RegExp, newStr: string) => {
  const rx = new RegExp(pattern, 'ig');
  const replacer = (c: string, i: number) =>
    c.match(/[A-Z]/) ? newStr[i].toUpperCase() : newStr[i] ?? '';
  const [match] = str.match(rx) ?? [];
  return (
    str.replace(rx, (oldStr) => oldStr.replace(/./g, replacer)) + newStr.slice(match?.length ?? 0)
  );
};
craig
  • 121
  • 2
  • 3