JavaScript equivalent to printf/String.Format

前端 未结 30 2399
囚心锁ツ
囚心锁ツ 2020-11-21 04:27

I\'m looking for a good JavaScript equivalent of the C/PHP printf() or for C#/Java programmers, String.Format() (IFormatProvider for .

相关标签:
30条回答
  • 2020-11-21 05:01

    From ES6 on you could use template strings:

    let soMany = 10;
    console.log(`This is ${soMany} times easier!`);
    // "This is 10 times easier!
    

    See Kim's answer below for details.


    Otherwise:

    Try sprintf() for JavaScript.


    If you really want to do a simple format method on your own, don’t do the replacements successively but do them simultaneously.

    Because most of the other proposals that are mentioned fail when a replace string of previous replacement does also contain a format sequence like this:

    "{0}{1}".format("{1}", "{0}")
    

    Normally you would expect the output to be {1}{0} but the actual output is {1}{1}. So do a simultaneously replacement instead like in fearphage’s suggestion.

    0 讨论(0)
  • 2020-11-21 05:01

    Building on the previously suggested solutions:

    // First, checks if it isn't implemented yet.
    if (!String.prototype.format) {
      String.prototype.format = function() {
        var args = arguments;
        return this.replace(/{(\d+)}/g, function(match, number) { 
          return typeof args[number] != 'undefined'
            ? args[number]
            : match
          ;
        });
      };
    }
    

    "{0} is dead, but {1} is alive! {0} {2}".format("ASP", "ASP.NET")

    outputs

    ASP is dead, but ASP.NET is alive! ASP {2}


    If you prefer not to modify String's prototype:

    if (!String.format) {
      String.format = function(format) {
        var args = Array.prototype.slice.call(arguments, 1);
        return format.replace(/{(\d+)}/g, function(match, number) { 
          return typeof args[number] != 'undefined'
            ? args[number] 
            : match
          ;
        });
      };
    }
    

    Gives you the much more familiar:

    String.format('{0} is dead, but {1} is alive! {0} {2}', 'ASP', 'ASP.NET');

    with the same result:

    ASP is dead, but ASP.NET is alive! ASP {2}

    0 讨论(0)
  • 2020-11-21 05:02

    For basic formatting:

    var template = jQuery.validator.format("{0} is not a valid value");
    var result = template("abc");
    
    0 讨论(0)
  • 2020-11-21 05:02

    For those who like Node.JS and its util.format feature, I've just extracted it out into its vanilla JavaScript form (with only functions that util.format uses):

    exports = {};
    
    function isString(arg) {
        return typeof arg === 'string';
    }
    function isNull(arg) {
        return arg === null;
    }
    function isObject(arg) {
        return typeof arg === 'object' && arg !== null;
    }
    function isBoolean(arg) {
        return typeof arg === 'boolean';
    }
    function isUndefined(arg) {
        return arg === void 0;
    }
    function stylizeNoColor(str, styleType) {
        return str;
    }
    function stylizeWithColor(str, styleType) {
        var style = inspect.styles[styleType];
    
        if (style) {
            return '\u001b[' + inspect.colors[style][0] + 'm' + str +
                '\u001b[' + inspect.colors[style][3] + 'm';
        } else {
            return str;
        }
    }
    function isFunction(arg) {
        return typeof arg === 'function';
    }
    function isNumber(arg) {
        return typeof arg === 'number';
    }
    function isSymbol(arg) {
        return typeof arg === 'symbol';
    }
    function formatPrimitive(ctx, value) {
        if (isUndefined(value))
            return ctx.stylize('undefined', 'undefined');
        if (isString(value)) {
            var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
                    .replace(/'/g, "\\'")
                    .replace(/\\"/g, '"') + '\'';
            return ctx.stylize(simple, 'string');
        }
        if (isNumber(value)) {
            // Format -0 as '-0'. Strict equality won't distinguish 0 from -0,
            // so instead we use the fact that 1 / -0 < 0 whereas 1 / 0 > 0 .
            if (value === 0 && 1 / value < 0)
                return ctx.stylize('-0', 'number');
            return ctx.stylize('' + value, 'number');
        }
        if (isBoolean(value))
            return ctx.stylize('' + value, 'boolean');
        // For some reason typeof null is "object", so special case here.
        if (isNull(value))
            return ctx.stylize('null', 'null');
        // es6 symbol primitive
        if (isSymbol(value))
            return ctx.stylize(value.toString(), 'symbol');
    }
    function arrayToHash(array) {
        var hash = {};
    
        array.forEach(function (val, idx) {
            hash[val] = true;
        });
    
        return hash;
    }
    function objectToString(o) {
        return Object.prototype.toString.call(o);
    }
    function isDate(d) {
        return isObject(d) && objectToString(d) === '[object Date]';
    }
    function isError(e) {
        return isObject(e) &&
            (objectToString(e) === '[object Error]' || e instanceof Error);
    }
    function isRegExp(re) {
        return isObject(re) && objectToString(re) === '[object RegExp]';
    }
    function formatError(value) {
        return '[' + Error.prototype.toString.call(value) + ']';
    }
    function formatPrimitiveNoColor(ctx, value) {
        var stylize = ctx.stylize;
        ctx.stylize = stylizeNoColor;
        var str = formatPrimitive(ctx, value);
        ctx.stylize = stylize;
        return str;
    }
    function isArray(ar) {
        return Array.isArray(ar);
    }
    function hasOwnProperty(obj, prop) {
        return Object.prototype.hasOwnProperty.call(obj, prop);
    }
    function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
        var name, str, desc;
        desc = Object.getOwnPropertyDescriptor(value, key) || {value: value[key]};
        if (desc.get) {
            if (desc.set) {
                str = ctx.stylize('[Getter/Setter]', 'special');
            } else {
                str = ctx.stylize('[Getter]', 'special');
            }
        } else {
            if (desc.set) {
                str = ctx.stylize('[Setter]', 'special');
            }
        }
        if (!hasOwnProperty(visibleKeys, key)) {
            name = '[' + key + ']';
        }
        if (!str) {
            if (ctx.seen.indexOf(desc.value) < 0) {
                if (isNull(recurseTimes)) {
                    str = formatValue(ctx, desc.value, null);
                } else {
                    str = formatValue(ctx, desc.value, recurseTimes - 1);
                }
                if (str.indexOf('\n') > -1) {
                    if (array) {
                        str = str.split('\n').map(function (line) {
                            return '  ' + line;
                        }).join('\n').substr(2);
                    } else {
                        str = '\n' + str.split('\n').map(function (line) {
                            return '   ' + line;
                        }).join('\n');
                    }
                }
            } else {
                str = ctx.stylize('[Circular]', 'special');
            }
        }
        if (isUndefined(name)) {
            if (array && key.match(/^\d+$/)) {
                return str;
            }
            name = JSON.stringify('' + key);
            if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
                name = name.substr(1, name.length - 2);
                name = ctx.stylize(name, 'name');
            } else {
                name = name.replace(/'/g, "\\'")
                    .replace(/\\"/g, '"')
                    .replace(/(^"|"$)/g, "'")
                    .replace(/\\\\/g, '\\');
                name = ctx.stylize(name, 'string');
            }
        }
    
        return name + ': ' + str;
    }
    function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
        var output = [];
        for (var i = 0, l = value.length; i < l; ++i) {
            if (hasOwnProperty(value, String(i))) {
                output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
                    String(i), true));
            } else {
                output.push('');
            }
        }
        keys.forEach(function (key) {
            if (!key.match(/^\d+$/)) {
                output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
                    key, true));
            }
        });
        return output;
    }
    function reduceToSingleString(output, base, braces) {
        var length = output.reduce(function (prev, cur) {
            return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
        }, 0);
    
        if (length > 60) {
            return braces[0] +
                (base === '' ? '' : base + '\n ') +
                ' ' +
                output.join(',\n  ') +
                ' ' +
                braces[1];
        }
    
        return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
    }
    function formatValue(ctx, value, recurseTimes) {
        // Provide a hook for user-specified inspect functions.
        // Check that value is an object with an inspect function on it
        if (ctx.customInspect &&
            value &&
            isFunction(value.inspect) &&
                // Filter out the util module, it's inspect function is special
            value.inspect !== exports.inspect &&
                // Also filter out any prototype objects using the circular check.
            !(value.constructor && value.constructor.prototype === value)) {
            var ret = value.inspect(recurseTimes, ctx);
            if (!isString(ret)) {
                ret = formatValue(ctx, ret, recurseTimes);
            }
            return ret;
        }
    
        // Primitive types cannot have properties
        var primitive = formatPrimitive(ctx, value);
        if (primitive) {
            return primitive;
        }
    
        // Look up the keys of the object.
        var keys = Object.keys(value);
        var visibleKeys = arrayToHash(keys);
    
        if (ctx.showHidden) {
            keys = Object.getOwnPropertyNames(value);
        }
    
        // This could be a boxed primitive (new String(), etc.), check valueOf()
        // NOTE: Avoid calling `valueOf` on `Date` instance because it will return
        // a number which, when object has some additional user-stored `keys`,
        // will be printed out.
        var formatted;
        var raw = value;
        try {
            // the .valueOf() call can fail for a multitude of reasons
            if (!isDate(value))
                raw = value.valueOf();
        } catch (e) {
            // ignore...
        }
    
        if (isString(raw)) {
            // for boxed Strings, we have to remove the 0-n indexed entries,
            // since they just noisey up the output and are redundant
            keys = keys.filter(function (key) {
                return !(key >= 0 && key < raw.length);
            });
        }
    
        // Some type of object without properties can be shortcutted.
        if (keys.length === 0) {
            if (isFunction(value)) {
                var name = value.name ? ': ' + value.name : '';
                return ctx.stylize('[Function' + name + ']', 'special');
            }
            if (isRegExp(value)) {
                return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
            }
            if (isDate(value)) {
                return ctx.stylize(Date.prototype.toString.call(value), 'date');
            }
            if (isError(value)) {
                return formatError(value);
            }
            // now check the `raw` value to handle boxed primitives
            if (isString(raw)) {
                formatted = formatPrimitiveNoColor(ctx, raw);
                return ctx.stylize('[String: ' + formatted + ']', 'string');
            }
            if (isNumber(raw)) {
                formatted = formatPrimitiveNoColor(ctx, raw);
                return ctx.stylize('[Number: ' + formatted + ']', 'number');
            }
            if (isBoolean(raw)) {
                formatted = formatPrimitiveNoColor(ctx, raw);
                return ctx.stylize('[Boolean: ' + formatted + ']', 'boolean');
            }
        }
    
        var base = '', array = false, braces = ['{', '}'];
    
        // Make Array say that they are Array
        if (isArray(value)) {
            array = true;
            braces = ['[', ']'];
        }
    
        // Make functions say that they are functions
        if (isFunction(value)) {
            var n = value.name ? ': ' + value.name : '';
            base = ' [Function' + n + ']';
        }
    
        // Make RegExps say that they are RegExps
        if (isRegExp(value)) {
            base = ' ' + RegExp.prototype.toString.call(value);
        }
    
        // Make dates with properties first say the date
        if (isDate(value)) {
            base = ' ' + Date.prototype.toUTCString.call(value);
        }
    
        // Make error with message first say the error
        if (isError(value)) {
            base = ' ' + formatError(value);
        }
    
        // Make boxed primitive Strings look like such
        if (isString(raw)) {
            formatted = formatPrimitiveNoColor(ctx, raw);
            base = ' ' + '[String: ' + formatted + ']';
        }
    
        // Make boxed primitive Numbers look like such
        if (isNumber(raw)) {
            formatted = formatPrimitiveNoColor(ctx, raw);
            base = ' ' + '[Number: ' + formatted + ']';
        }
    
        // Make boxed primitive Booleans look like such
        if (isBoolean(raw)) {
            formatted = formatPrimitiveNoColor(ctx, raw);
            base = ' ' + '[Boolean: ' + formatted + ']';
        }
    
        if (keys.length === 0 && (!array || value.length === 0)) {
            return braces[0] + base + braces[1];
        }
    
        if (recurseTimes < 0) {
            if (isRegExp(value)) {
                return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
            } else {
                return ctx.stylize('[Object]', 'special');
            }
        }
    
        ctx.seen.push(value);
    
        var output;
        if (array) {
            output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
        } else {
            output = keys.map(function (key) {
                return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
            });
        }
    
        ctx.seen.pop();
    
        return reduceToSingleString(output, base, braces);
    }
    function inspect(obj, opts) {
        // default options
        var ctx = {
            seen: [],
            stylize: stylizeNoColor
        };
        // legacy...
        if (arguments.length >= 3) ctx.depth = arguments[2];
        if (arguments.length >= 4) ctx.colors = arguments[3];
        if (isBoolean(opts)) {
            // legacy...
            ctx.showHidden = opts;
        } else if (opts) {
            // got an "options" object
            exports._extend(ctx, opts);
        }
        // set default options
        if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
        if (isUndefined(ctx.depth)) ctx.depth = 2;
        if (isUndefined(ctx.colors)) ctx.colors = false;
        if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
        if (ctx.colors) ctx.stylize = stylizeWithColor;
        return formatValue(ctx, obj, ctx.depth);
    }
    exports.inspect = inspect;
    
    
    // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
    inspect.colors = {
        'bold': [1, 22],
        'italic': [3, 23],
        'underline': [4, 24],
        'inverse': [7, 27],
        'white': [37, 39],
        'grey': [90, 39],
        'black': [30, 39],
        'blue': [34, 39],
        'cyan': [36, 39],
        'green': [32, 39],
        'magenta': [35, 39],
        'red': [31, 39],
        'yellow': [33, 39]
    };
    
    // Don't use 'blue' not visible on cmd.exe
    inspect.styles = {
        'special': 'cyan',
        'number': 'yellow',
        'boolean': 'yellow',
        'undefined': 'grey',
        'null': 'bold',
        'string': 'green',
        'symbol': 'green',
        'date': 'magenta',
        // "name": intentionally not styling
        'regexp': 'red'
    };
    
    
    var formatRegExp = /%[sdj%]/g;
    exports.format = function (f) {
        if (!isString(f)) {
            var objects = [];
            for (var j = 0; j < arguments.length; j++) {
                objects.push(inspect(arguments[j]));
            }
            return objects.join(' ');
        }
    
        var i = 1;
        var args = arguments;
        var len = args.length;
        var str = String(f).replace(formatRegExp, function (x) {
            if (x === '%%') return '%';
            if (i >= len) return x;
            switch (x) {
                case '%s':
                    return String(args[i++]);
                case '%d':
                    return Number(args[i++]);
                case '%j':
                    try {
                        return JSON.stringify(args[i++]);
                    } catch (_) {
                        return '[Circular]';
                    }
                default:
                    return x;
            }
        });
        for (var x = args[i]; i < len; x = args[++i]) {
            if (isNull(x) || !isObject(x)) {
                str += ' ' + x;
            } else {
                str += ' ' + inspect(x);
            }
        }
        return str;
    };
    

    Harvested from: https://github.com/joyent/node/blob/master/lib/util.js

    0 讨论(0)
  • 2020-11-21 05:03

    Here's a minimal implementation of sprintf in JavaScript: it only does "%s" and "%d", but I have left space for it to be extended. It is useless to the OP, but other people who stumble across this thread coming from Google might benefit from it.

    function sprintf() {
        var args = arguments,
        string = args[0],
        i = 1;
        return string.replace(/%((%)|s|d)/g, function (m) {
            // m is the matched format, e.g. %s, %d
            var val = null;
            if (m[2]) {
                val = m[2];
            } else {
                val = args[i];
                // A switch statement so that the formatter can be extended. Default is %s
                switch (m) {
                    case '%d':
                        val = parseFloat(val);
                        if (isNaN(val)) {
                            val = 0;
                        }
                        break;
                }
                i++;
            }
            return val;
        });
    }
    

    Example:

    alert(sprintf('Latitude: %s, Longitude: %s, Count: %d', 41.847, -87.661, 'two'));
    // Expected output: Latitude: 41.847, Longitude: -87.661, Count: 0
    

    In contrast with similar solutions in previous replies, this one does all substitutions in one go, so it will not replace parts of previously replaced values.

    0 讨论(0)
  • 2020-11-21 05:03

    Adding to zippoxer's answer, I use this function:

    String.prototype.format = function () {
        var a = this, b;
        for (b in arguments) {
            a = a.replace(/%[a-z]/, arguments[b]);
        }
        return a; // Make chainable
    };
    
    var s = 'Hello %s The magic number is %d.';
    s.format('world!', 12); // Hello World! The magic number is 12.
    

    I also have a non-prototype version which I use more often for its Java-like syntax:

    function format() {
        var a, b, c;
        a = arguments[0];
        b = [];
        for(c = 1; c < arguments.length; c++){
            b.push(arguments[c]);
        }
        for (c in b) {
            a = a.replace(/%[a-z]/, b[c]);
        }
        return a;
    }
    format('%d ducks, 55 %s', 12, 'cats'); // 12 ducks, 55 cats
    

    ES 2015 update

    All the cool new stuff in ES 2015 makes this a lot easier:

    function format(fmt, ...args){
        return fmt
            .split("%%")
            .reduce((aggregate, chunk, i) =>
                aggregate + chunk + (args[i] || ""), "");
    }
    
    format("Hello %%! I ate %% apples today.", "World", 44);
    // "Hello World, I ate 44 apples today."
    

    I figured that since this, like the older ones, doesn't actually parse the letters, it might as well just use a single token %%. This has the benefit of being obvious and not making it difficult to use a single %. However, if you need %% for some reason, you would need to replace it with itself:

    format("I love percentage signs! %%", "%%");
    // "I love percentage signs! %%"
    
    0 讨论(0)
提交回复
热议问题