46

I've been trying to figure out how to map a set of characters in a string to another set similar to the tr function in Perl.

I found this site that shows equivalent functions in JS and Perl, but sadly no tr equivalent.

the tr (transliteration) function in Perl maps characters one to one, so

     data =~ tr|\-_|+/|;

would map

     - => + and _ => /

How can this be done efficiently in JavaScript?

qodeninja
  • 10,946
  • 30
  • 98
  • 152
  • Does this answer your question? [Replace multiple characters in one replace call](https://stackoverflow.com/questions/16576983/replace-multiple-characters-in-one-replace-call) – user202729 Dec 14 '21 at 04:29

11 Answers11

84

There isn't a built-in equivalent, but you can get close to one with replace:

data = data.replace(/[\-_]/g, function (m) {
    return {
        '-': '+',
        '_': '/'
    }[m];
});
Jonathan Lonowski
  • 121,453
  • 34
  • 200
  • 199
  • 1
    very nice with the callback function, what does return{..}[m] do? – qodeninja May 23 '12 at 23:08
  • 3
    @qodeninja The object (`{...}`) defines the character mapping, with expected matches as keys/properties and replacements as values. The current match, `m`, is then used lookup its own replacement (`[m]`) from the object, which is returned back to `replace` to perform the actual replacement. – Jonathan Lonowski May 23 '12 at 23:30
  • Apparently the answer is "yes," but not as succinctly as Perl. – Pete Alvin Apr 13 '14 at 19:56
  • 1
    When we have an array `arr` full of such substitutions, and the matches follow a simple pattern that can be given completely by a regex, it's nice to have a default in case the regex was a little to general and matched something that wasn't in `arr`. In that case, the function body should be `return arr[m] || m;` to just return the match itself without any substitution (rather than `undefined`). – Mike Aug 10 '17 at 18:14
  • 1
    Not sure about the performance of this, but defining the object ("dictionary") as a global variable may be faster (avoid recreate each time). – user202729 Dec 14 '21 at 04:27
8

Method:

String.prototype.mapReplace = function(map) {
    var regex = [];
    for(var key in map)
        regex.push(key.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"));
    return this.replace(new RegExp(regex.join('|'),"g"),function(word){
        return map[word];
    });
};

A perfect example:

var s = "I think Peak rocks!"
s.mapReplace({"I think":"Actually","rocks":"sucks"})
// console: "Actually Peak sucks!"
PeakJi
  • 1,547
  • 14
  • 23
  • 2
    @qodeninja It's the same technique, while this method allows to use a dynamic JSON kv mapping. It might be slower than the answer, because it generates the Regex and replacement dictionary for you. So use this method while you need dynamic replacement, use the answer if you only need to handle static wordset. – PeakJi Sep 10 '13 at 03:00
7

I can't vouch for 'efficient' but this uses a regex and a callback to provide the replacement character.

function tr( text, search, replace ) {
    // Make the search string a regex.
    var regex = RegExp( '[' + search + ']', 'g' );
    var t = text.replace( regex, 
            function( chr ) {
                // Get the position of the found character in the search string.
                var ind = search.indexOf( chr );
                // Get the corresponding character from the replace string.
                var r = replace.charAt( ind );
                return r;
            } );
    return t;
}

For long strings of search and replacement characters, it might be worth putting them in a hash and have the function return from that. ie, tr/abcd/QRST/ becomes the hash { a: Q, b: R, c: S, d: T } and the callback returns hash[ chr ].

Gerrit0
  • 7,955
  • 3
  • 25
  • 32
neniu
  • 419
  • 3
  • 8
  • 1
    +1, even though: Doesn't support "]", "\" and "^". Doesn't support ranges. Doesn't support "\" escapes. – ikegami May 23 '12 at 22:28
  • @ikegami see my attempt at beefing this function up here: https://codereview.stackexchange.com/questions/192385/attempt-at-perl-transliteration-function-in-javascript-with-flags – Jarede Apr 19 '18 at 09:29
3

This functions, which are similar how it's built in Perl.

function s(a, b){ $_ = $_.replace(a, b); }
function tr(a, b){ [...a].map((c, i) => s(new RegExp(c, "g"), b[i])); }

$_ = "Εμπεδοκλης ο Ακραγαντινος";

tr("ΑΒΓΔΕΖΗΘΙΚΛΜΝΟΠΡΣΤΥΦΧΩ", "ABGDEZITIKLMNOPRSTIFHO");
tr("αβγδεζηθικλμνοπρστυφχω", "abgdezitiklmnoprstifho");
s(/Ξ/g, "X"); s(/Ψ/g, "Ps");
s(/ξ/g, "x"); s(/ψ/g, "Ps");
s(/ς/g, "s");

console.log($_);
  • Seems the best answer... But please explain better what is `s()` and how to use `tr()`. The ideal is only one function `tr(x,from,to)`. – Peter Krauss Dec 29 '18 at 23:40
2

Another solution:

var data = data.replace(/[-_]/g, (match) => {
    return  '+/'['-_'.indexOf(match)];
});

A function in the 2nd parameter of Replace will be invoked for every match of regex in the first parameter and its return value is used as the replacement text.

David Najman
  • 487
  • 4
  • 7
1

This will map all as to b and all y to z

var map = { a: 'b', y: 'z' };
var str = 'ayayay';

for (var i = 0; i < str.length; i++)
    str[i] = map[str[i]] || str[i];

EDIT:

Apparently you can't do that with strings. Here's an alternative:

var map = { a: 'b', y: 'z' };
var str = 'ayayay', str2 = [];

for (var i = 0; i < str.length; i++)
    str2.push( map[str[i]] || str[i] );
str2.join('');
qwertymk
  • 34,200
  • 28
  • 121
  • 184
1

In Perl, one can also write

tr{-_}{+/}

as

my %trans = (
   '-' => '+',
   '_' => '/',
);

my $class = join '', map quotemeta, keys(%trans);
my $re = qr/[$class]/;

s/($re)/$trans{$1}/g;

This latter version can surely be implemented in JS without much trouble.

(My version lacks the duplication of Jonathan Lonowski's solution.)

ikegami
  • 367,544
  • 15
  • 269
  • 518
1

I wanted a function that allows passing a custom map object, so I wrote one based on Jonathan Lonowski's answer. If you are trying to replace special characters (the kind that need to be escaped in regular expressions) you'll have to do some more work.

const mapReplace = (str, map) => {
  const matchStr = Object.keys(map).join('|');
  if (!matchStr) return str;
  const regexp = new RegExp(matchStr, 'g');
  return str.replace(regexp, match => map[match]);
};

And it's used like this:

const map = { a: 'A', b: 'B', d: 'D' };
mapReplace('abcde_edcba', map);
// ABcDe_eDcBA
randomraccoon
  • 308
  • 1
  • 4
  • 14
1

Here is a function that receives text orig dest and replaces in text each character for the one in the corresponding position in dest.

Is is not good enough for cases where more than one character must be replaced by only one or vice-versa. It is not good enough for removing accents from Portuguese texts, which is my use case.

function tr(text, orig, dest) {
    console.assert(orig.length == dest.length);
    const a = orig.split('').map(i=> new RegExp(i, 'g'));
    const b = dest.split('');
    return a.reduce((prev, curr, idx) => prev.replace(a[idx], b[idx]), text );
}

How to use it:

var port  = "ÀÂÃÁÉÊÍÓÔÕÜÚÇáàãâêéíóõôúüç";
var ascii = "AAAAEEIOOOUUCaaaaeeiooouuc";
console.log(tr("não têm ações em seqüência", port, ascii)) ;
ndvo
  • 939
  • 11
  • 16
1

Similiar to Jonathan Lonowski answer but with words support, not just single tr chars

"aaabbccddeeDDDffd".replace( /(a|cc|DDD|dd)/g, m => ({'a':'B', 'cc':'DDD', 'DDD':'ZZZ', dd:'QQ'}[m]) ) 
// RESULT: "BBBbbDDDQQeeZZZffd"
1

With only one map:

const map = {
    '-': '+',
    '_': '/'
};

data = Object.entries(map).reduce((prev, entry) => prev.replace(...entry), data);
catwith
  • 875
  • 10
  • 13