Format a JavaScript string using placeholders and an object of substitutions?

170,230

Solution 1

The requirements of the original question clearly couldn't benefit from string interpolation, as it seems like it's a runtime processing of arbitrary replacement keys.

However, if you just had to do string interpolation, you can use:

const str = `My name is ${replacements.name} and my age is ${replacements.age}.`

Note the backticks delimiting the string, they are required.


For an answer suiting the particular OP's requirement, you could use String.prototype.replace() for the replacements.

The following code will handle all matches and not touch ones without a replacement (so long as your replacement values are all strings, if not, see below).

var replacements = {"%NAME%":"Mike","%AGE%":"26","%EVENT%":"20"},
    str = 'My Name is %NAME% and my age is %AGE%.';

str = str.replace(/%\w+%/g, function(all) {
   return replacements[all] || all;
});

jsFiddle.

If some of your replacements are not strings, be sure they exists in the object first. If you have a format like the example, i.e. wrapped in percentage signs, you can use the in operator to achieve this.

jsFiddle.

However, if your format doesn't have a special format, i.e. any string, and your replacements object doesn't have a null prototype, use Object.prototype.hasOwnProperty(), unless you can guarantee that none of your potential replaced substrings will clash with property names on the prototype.

jsFiddle.

Otherwise, if your replacement string was 'hasOwnProperty', you would get a resultant messed up string.

jsFiddle.


As a side note, you should be called replacements an Object, not an Array.

Solution 2

How about using ES6 template literals?

var a = "cat";
var b = "fat";
console.log(`my ${a} is ${b}`); //notice back-ticked string

More about template literals...

Solution 3

Currently there is still no native solution in Javascript for this behavior. Tagged templates are something related, but don't solve it.

Here there is a refactor of alex's solution with an object for replacements.

The solution uses arrow functions and a similar syntax for the placeholders as the native Javascript interpolation in template literals ({} instead of %%). Also there is no need to include delimiters (%) in the names of the replacements.

There are two flavors (three with the update): descriptive, reduced, elegant reduced with groups.

Descriptive solution:

const stringWithPlaceholders = 'My Name is {name} and my age is {age}.';

const replacements = {
  name: 'Mike',
  age: '26',
};

const string = stringWithPlaceholders.replace(
  /{\w+}/g,
  placeholderWithDelimiters => {
    const placeholderWithoutDelimiters = placeholderWithDelimiters.substring(
      1,
      placeholderWithDelimiters.length - 1,
    );
    const stringReplacement = replacements[placeholderWithoutDelimiters] || placeholderWithDelimiters;
    return stringReplacement;
  },
);

console.log(string);

Reduced solution:

const stringWithPlaceholders = 'My Name is {name} and my age is {age}.';

const replacements = {
  name: 'Mike',
  age: '26',
};

const string = stringWithPlaceholders.replace(/{\w+}/g, placeholder =>
  replacements[placeholder.substring(1, placeholder.length - 1)] || placeholder
);

console.log(string);

UPDATE 2020-12-10

Elegant reduced solution with groups, as suggested by @Kade in the comments:

const stringWithPlaceholders = 'My Name is {name} and my age is {age}.';

const replacements = {
  name: 'Mike',
  age: '26',
};

const string = stringWithPlaceholders.replace(
  /{(\w+)}/g, 
  (placeholderWithDelimiters, placeholderWithoutDelimiters) =>
    replacements[placeholderWithoutDelimiters] || placeholderWithDelimiters
);

console.log(string);

UPDATE 2021-01-21

Support empty string as a replacement, as suggested by @Jesper in the comments:

const stringWithPlaceholders = 'My Name is {name} and my age is {age}.';

const replacements = {
  name: 'Mike',
  age: '',
};

const string = stringWithPlaceholders.replace(
  /{(\w+)}/g, 
  (placeholderWithDelimiters, placeholderWithoutDelimiters) =>
  replacements.hasOwnProperty(placeholderWithoutDelimiters) ? 
    replacements[placeholderWithoutDelimiters] : placeholderWithDelimiters
);

console.log(string);

Solution 4

You can use JQuery(jquery.validate.js) to make it work easily.

$.validator.format("My name is {0}, I'm {1} years old",["Bob","23"]);

Or if you want to use just that feature you can define that function and just use it like

function format(source, params) {
    $.each(params,function (i, n) {
        source = source.replace(new RegExp("\\{" + i + "\\}", "g"), n);
    })
    return source;
}
alert(format("{0} is a {1}", ["Michael", "Guy"]));

credit to jquery.validate.js team

Solution 5

As with modern browser, placeholder is supported by new version of Chrome / Firefox, similar as the C style function printf().

Placeholders:

  • %s String.
  • %d,%i Integer number.
  • %f Floating point number.
  • %o Object hyperlink.

e.g.

console.log("generation 0:\t%f, %f, %f", a1a1, a1a2, a2a2);

BTW, to see the output:

  • In Chrome, use shortcut Ctrl + Shift + J or F12 to open developer tool.
  • In Firefox, use shortcut Ctrl + Shift + K or F12 to open developer tool.

@Update - nodejs support

Seems nodejs don't support %f, instead, could use %d in nodejs. With %d number will be printed as floating number, not just integer.

Share:
170,230

Related videos on Youtube

Joby Joseph
Author by

Joby Joseph

Javascript Developer

Updated on March 12, 2022

Comments

  • Joby Joseph
    Joby Joseph about 2 years

    I have a string with say: My Name is %NAME% and my age is %AGE%.

    %XXX% are placeholders. We need to substitute values there from an object.

    Object looks like: {"%NAME%":"Mike","%AGE%":"26","%EVENT%":"20"}

    I need to parse the object and replace the string with corresponding values. So that final output will be:

    My Name is Mike and my age is 26.

    The whole thing has to be done either using pure javascript or jquery.

    • Joel Coehoorn
      Joel Coehoorn over 12 years
      That looks more like an object than an array
    • nnnnnn
      nnnnnn over 12 years
      What have you tried so far? Have you looked at the string .replace() method? (Also, you don't have an array there, you have an object.)
    • davidchambers
      davidchambers over 12 years
      That's pretty ugly. Surely you'd be just as well served by {NAME: "Mike", AGE: 26, EVENT: 20}? You'd still require that these keys appear bookended by percent signs in the input string, of course.
  • mu is too short
    mu is too short over 12 years
  • nnnnnn
    nnnnnn over 12 years
    +1. Nice. Though you might want to say return replacements[all] || all to cover %NotInReplacementsList% cases.
  • hafichuk
    hafichuk over 12 years
    Thanks for the spanking guys - appreciated ;)
  • RobG
    RobG over 12 years
    Cool, Alex did pretty much exactly the same thing but in fewer lines of code (though ternary operators are likely slower than if..else).
  • RobG
    RobG over 12 years
    Hey, I gave you a +1! You both did a replace function, yours isn't exactly the same but pretty similar. Your RegExp is different too, the OP would be better off using %% or $$ or similar as delimiters - a single % or % is likely to occur in a string normally, but doubles are unlikely.
  • Michael Härtl
    Michael Härtl about 9 years
    This will not work if the replacement value is falseish. So it's better to use this return statement: return all in params ? params[all] : all;
  • user3167101
    user3167101 about 9 years
    @MichaelHärtl Shouldn't your replacements all be strings? If you want to replace with empty string, better off checking with other means.
  • Michael Härtl
    Michael Härtl about 9 years
    @alex I had the situation where replacements could also be integers, and even 0. In this case it didn't work.
  • nabrown
    nabrown over 8 years
    You definitely wouldn't want to load this plugin just for this, but I'm already using this to validate a form on the page...so thanks for the tip!
  • user3167101
    user3167101 over 8 years
    @MichaelHärtl Updated to cover that case.
  • user3167101
    user3167101 over 8 years
    Pretty inefficient to create a regex for each number, it would be better to match all numbers and then replace on if the value was found in the array, perhaps?
  • user3167101
    user3167101 over 8 years
    That's great, unless you want to do something besides log it to the console.
  • Raphael C
    Raphael C over 7 years
  • Hyyan Abo Fakher
    Hyyan Abo Fakher about 7 years
    Awesome, simple solution, I use it with different pattern /[[\w+]]/g
  • IonicBurger
    IonicBurger over 6 years
    But since you can do it in es6 that is probably a bit overkill?
  • user3167101
    user3167101 over 6 years
    If you have an object with placeholders like the OP, how does string interpolation help that?
  • Larphoid
    Larphoid over 5 years
    + very nice ... and for $.each you could make String.prototype.format=function(p){var s=this,r=function(v,i){s=s.replace(new RegExp("\\{"+i+"\\}","g"),v);};p.forEach(r);return s;} so you don't have to include jquery just for that one ;)
  • BAERUS
    BAERUS over 5 years
    Works like a charm! Most pragmatic solution to my placeholding problems, perfect.
  • baku
    baku almost 5 years
    i made a variation on your function without prototypes formatMessage(message: string, values: string[]) { let i = 0; return message.replace(/%\w+%/g, (match, idx) => { return values[i++]; }); } this takes message to format and array of replace values and looks for %SOME_VALUE%
  • Johan Persson
    Johan Persson over 4 years
    This lets you store the string in one place in one format and then at a later time replace the corresponding items using only one function. That is to my knowledge not possible to do with es6 template literals. A usage for this would e.g. be translation strings where you will consume the string elsewhere and inject the desired values there.
  • AMS777
    AMS777 about 4 years
    With arrow function: str.replace(/%\w+%/g, placeholder => replacements[placeholder] || placeholder);
  • Thierry J.
    Thierry J. almost 4 years
    This solution doesn't work for runtime replacements
  • Carlos P
    Carlos P over 3 years
    Thanks for this, it's great to have a generic solution for a placeholder string and an object to merge into it like this
  • AMS777
    AMS777 over 3 years
    @CarlosP, that's the idea, to have a generic solution. But I think it should be something native to the language, as it's a common use case.
  • Kade
    Kade over 3 years
    This can be simplified a bit more by using regex groups like this: stringWithPlaceholders.replace(/{(\w+)}/g, (fullMatch, group1) => replacements[group1] || fullMatch )
  • Jesper
    Jesper over 3 years
    All these solutions suffer from the bug of using "||", since the empty string is also falsy in Javascript. This means that if a replacement string is empty, the placeholder will not be replaced. And it's absolutely valid for a replacement string to be empty. Instead of e.g. replacements[group1] || fullMatch, use replacements.hasOwnProperty(group1) ? replacements[group1] : fullMatch.
  • Wyck
    Wyck about 3 years
    Holy injection attack vulnerability, Batman! Requires sanitizing str for backticks.
  • Amio.io
    Amio.io almost 3 years
    Man, can you pls include a reference link in your answer? 🙏
  • Eric
    Eric almost 3 years
  • Amio.io
    Amio.io over 2 years
    My bad, I thought it's from an official specification. Thx for the reference!
  • nima
    nima over 2 years
    While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value. You can find more information on how to write good answers in the help center: stackoverflow.com/help/how-to-answer . Good luck 🙂
  • Double_A
    Double_A over 2 years
    What if I don't want to just log the string, but keep it as value to do something else with it?
  • Jason C
    Jason C almost 2 years
    Node.js util.format supports %f.