6

I have a string that I'd like to get all possible replace-ment combinations on using the following substitutions:

var equiv = {
  "a": "4",
  "b": "8",
  "e": "3",
  "i": "1",
  "l": "1",
  "o": "0",
  "t": "7"
}

I would like to define a String.prototype function, something like:

String.prototype.l33tCombonations = function()
{
    var toReturn = [];

    for (var i in equiv)
    {
        // this.???
        // toReturn.push(this???)
    }

    return toReturn;
}

So I could feed in something like "tomato".l33tCombinations() and get back:

["tomato", "t0mato", "t0mat0", "tomat0", "toma7o", "t0ma7o", "t0m470", ...].

Order is not important. Thoughts?

adiga
  • 34,372
  • 9
  • 61
  • 83
xd1936
  • 1,038
  • 2
  • 9
  • 27

5 Answers5

3

I would use a recursive approach, that traverses the string char by char:

const toL33t = { "a": "4", "b": "8",  "e": "3",  "i": "1", "l": "1",  "o": "0",  "t": "7" };

function* l33t(string, previous = "") {
  const char = string[0];
  // Base case: no chars left, yield previous combinations
  if(!char) {
    yield previous;
    return;
  }
  // Recursive case: Char does not get l33t3d
  yield* l33t(string.slice(1), previous + char);
  // Recursive case: Char gets l33t3d
  if(toL33t[char])
    yield* l33t(string.slice(1), previous + toL33t[char]);
}

console.log(...l33t("tomato"));

If you really need it on the prototype thats also possibl3, but I wouldn't recommend that:

 String.prototype.l33t = function() {
   return [...l33t(this)];
 };

 console.log("stuff".l33t());
mhodges
  • 10,938
  • 2
  • 28
  • 46
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
2

You can do something like this using reduce

The idea is to loop through each character and add every combination to the accumulator. If you encounter a character which isn't part of equiv, just add the character to every item in the accumulator. If the character does exist in equiv, duplicate all the previous combinations and add another set of combinations with the equiv[<character>]

const equiv = {
  "a": "4",
  "b": "8",
  "e": "3",
  "i": "1",
  "l": "1",
  "o": "0",
  "t": "7"
}

const input = "tomato";

const output = [...input].reduce((acc, c, i) => {
  const r = equiv[c];
  
  if (i === 0) {
    return r ? [c, r] : [c];
  }

  const updated = acc.map(a => a + c);
  const newItems = r ? acc.map(a => a + r) : [];
  
  return [...updated, ...newItems]
}, [])

console.log(output)
adiga
  • 34,372
  • 9
  • 61
  • 83
  • Wrapped this up in a prototype function and it's blazing fast! Excellent answer, thank you! – xd1936 Jan 17 '19 at 18:51
2

You could take a cartesian product for generating the wanted values.

function leet(string) {
    const
        cartesian = (a, b) => a.reduce((r, v) => r.concat(b.map(w => [].concat(v, w))), []),
        code = { a: "4", b: "8", e: "3", i: "1", l: "1", o: "0", t: "7" };

    return Array
         .from(string, c => c in code ? [c, code[c]] : [c])
         .reduce(cartesian)
         .map(a => a.join(''));
}

console.log(leet('tomatoe'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
2

I have approached this in a recursive way, on each iteration of the recursion, a new character of the string is analyzed, if this character have a replacement, then both the character and the replacement are concatenated to all previous results making a new set of results, otherwise only the character is concatenated to all the previous results. Note, I have abuse of the spread operator for this approach.

var equiv = {a: "4", b: "8", e: "3", i: "1", l: "1", o: "0", t: "7"};

const genComb = (str, arr) =>
{
    if (!str) return arr; // Finish condition.
    let c = str[0];       // New char to be appended.
    let r = equiv[c];     // New char replacement.

    return genComb(
        str.slice(1),
        [...arr.map(e => e + c), ...(r ? arr.map(e => e + r) : [])]
    );
};

String.prototype.l33tCombinations = function()
{
   return genComb(this, [""], 0);
}

console.log("tomato".l33tCombinations());
Shidersz
  • 16,846
  • 2
  • 23
  • 48
1

I think this might yield the desired result! Loop through each letter and every time a new replacement is found, add a new word to toReturn and make sure to search each new word!

var equiv = {
  "a": "4",
  "b": "8",
  "e": "3",
  "i": "1",
  "l": "1",
  "o": "0",
  "t": "7"
}

String.prototype.l33tCombinations = function() {
  var toReturn = [this.toLowerCase()];

  for (let i = 0; i < toReturn.length; i++) {
  
    for (let j = 0; j < toReturn[i].length; j++) {
      if (equiv[toReturn[i][j]]) {
       let newWord = toReturn[i].split('');
       newWord[j] = equiv[newWord[j]];
       let newWordJoined = newWord.join('');
       if (!toReturn.includes(newWordJoined))
        toReturn.push(newWordJoined);
      }
    }
 
  }

  return toReturn;
}

console.log('tomato'.l33tCombinations());
Nick
  • 16,066
  • 3
  • 16
  • 32
  • This is my favorite answer as far as readability goes! Thanks for the clear answer :) – xd1936 Jan 17 '19 at 18:33
  • No problem! The fact that it has to use the array `includes` method bothers me and probably makes it more inefficient than the others. – Nick Jan 17 '19 at 18:37