JS object/JSON key wildcard

Viewed 563

I have an object that is evaluated from JSON that looks like this:

{
    "foo %s": "bar %s",
    "Hello %s world %s.": "We have multiple %s %s."
}

Using this object and the following inputs, I am trying to achieve something like this:

  • foo bar -> matches foo %s -> bar bar
  • Hello foo world bar. -> matches Hello %s world %s. -> We have multiple foo bar

So far I have the following code which works for one %s and only if it is surrounded by spaces.

let str = "input here";
let a = {
        "foo %s": "bar %s",
        "Hello %s world %s.": "We have multiple %s %s."
    },
    p = str.split(" "),
    result;
for (let j in p) { // loop indices
    let pp = p.map(u => u); // create local array
    pp[j] = "%s"; //replace index with %s
    if (a[pp.join(" ")]) { // check if this is in the object
        result = a[pp.join(" ")].replace("%s", p[j]); // join the array and replace %s from the result
        break;
    }
}

Any ideas or suggestions?

1 Answers

You could convert your object to regular expressions and then execute them in a call to String.prototype.replace.

Example with a single, defined, replacement operation:

console.log('Hello foo world bar.'.replace(/^Hello (.*?) world (.*?) bar\.$/, 'We have multiple $1 $2.'))
// Output: We have multiple foo bar.

Here we basically match against the whole string - if it matches the pattern, the whole contents will get replaced.

As you can see, to achieve this, you would need to transform the "Hello %s world %s." into /^Hello (.*?) world (.*?)\.$/ (with the (.*?) defining a non-greedy capture group containing any characters) and the "We have multiple %s %s." into "We have multiple $1 $2." (with $x referring to the x-th capture group's contents).

So you could have a function like this:

function replaceTemplates (str, templates) {
  // This helper function escapes any characters which would have a special
  // meaning in the regexp otherwise. $& is the match as a whole, so it replaces
  // each of these characters with themselves prepended by a backlash.
  const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  
  for (const [template, replacement] of Object.entries(templates)) {
    // We create a regexp from the template, replacing every %s with (.*?), wrapping it
    // in ^ and $ (start/end of string) and escaping all the other special characters.
    const re = new RegExp('^' + escapeRegExp(template).replace(/%s/g, '(.*?)') + '$')
    
    if (re.test(str)) {
      // To get the correct string for the right side of the string replace method,
      // we have to replace each %s in the replacement string by $1, $2, $3 etc. and
      // also replace single $ signs by $$ because the $ has a special meaning as shown
      // above, and then we can call the actual replace and return the result. But since
      // a $ in the replacement has to be escaped as just explained, we have to write
      // FOUR $ signs to get TWO at the end.
      let i = 1 // This is used as counter in the following replacing operations for $x
      const reReplacement = replacement
        .replace(/\$/g, '$$$$')
        .replace(/%s/g, () => '$' + i++) // By using a function, we can make $x dynamic
      
      return str.replace(re, reReplacement)
    }
  }
  
  return str
}

const templates = {
  'foo %s': 'bar %s',
  'Hello %s world %s.': 'We have multiple %s %s.'
}

console.log(replaceTemplates('foo bar', templates)) // Output: bar bar
console.log(replaceTemplates('Hello foo world bar.', templates)) // Output: We have multiple foo bar.

By the way: If pp = p.map(u => u) is supposed to create a copy, I'd suggest pp = p.slice() or just pp = [...p] instead.

Related