JavaScript 實現sprintf 功能程式碼

pengfoo發表於2014-10-14
(function(window) {
    var re = {
        not_string: /[^s]/,
        number: /[dief]/,
        text: /^[^\x25]+/,
        modulo: /^\x25{2}/,
        placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fiosuxX])/,
        key: /^([a-z_][a-z_\d]*)/i,
        key_access: /^\.([a-z_][a-z_\d]*)/i,
        index_access: /^\[(\d+)\]/,
        sign: /^[\+\-]/
    }

    function sprintf() {
        var key = arguments[0], cache = sprintf.cache
        if (!(cache[key] && cache.hasOwnProperty(key))) {
            cache[key] = sprintf.parse(key)
        }
        return sprintf.format.call(null, cache[key], arguments)
    }

    sprintf.format = function(parse_tree, argv) {
        var cursor = 1, tree_length = parse_tree.length, node_type = "", arg, output = [], i, k, match, pad, pad_character, pad_length, is_positive = true, sign = ""
        for (i = 0; i < tree_length; i++) {
            node_type = get_type(parse_tree[i])
            if (node_type === "string") {
                output[output.length] = parse_tree[i]
            }
            else if (node_type === "array") {
                match = parse_tree[i] // convenience purposes only
                if (match[2]) { // keyword argument
                    arg = argv[cursor]
                    for (k = 0; k < match[2].length; k++) {
                        if (!arg.hasOwnProperty(match[2][k])) {
                            throw new Error(sprintf("[sprintf] property '%s' does not exist", match[2][k]))
                        }
                        arg = arg[match[2][k]]
                    }
                }
                else if (match[1]) { // positional argument (explicit)
                    arg = argv[match[1]]
                }
                else { // positional argument (implicit)
                    arg = argv[cursor++]
                }

                if (get_type(arg) == "function") {
                    arg = arg()
                }

                if (re.not_string.test(match[8]) && (get_type(arg) != "number" && isNaN(arg))) {
                    throw new TypeError(sprintf("[sprintf] expecting number but found %s", get_type(arg)))
                }

                if (re.number.test(match[8])) {
                    is_positive = arg >= 0
                }

                switch (match[8]) {
                    case "b":
                        arg = arg.toString(2)
                    break
                    case "c":
                        arg = String.fromCharCode(arg)
                    break
                    case "d":
                    case "i":
                        arg = parseInt(arg, 10)
                    break
                    case "e":
                        arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential()
                    break
                    case "f":
                        arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg)
                    break
                    case "o":
                        arg = arg.toString(8)
                    break
                    case "s":
                        arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg)
                    break
                    case "u":
                        arg = arg >>> 0
                    break
                    case "x":
                        arg = arg.toString(16)
                    break
                    case "X":
                        arg = arg.toString(16).toUpperCase()
                    break
                }
                if (re.number.test(match[8]) && (!is_positive || match[3])) {
                    sign = is_positive ? "+" : "-"
                    arg = arg.toString().replace(re.sign, "")
                }
                else {
                    sign = ""
                }
                pad_character = match[4] ? match[4] === "0" ? "0" : match[4].charAt(1) : " "
                pad_length = match[6] - (sign + arg).length
                pad = match[6] ? (pad_length > 0 ? str_repeat(pad_character, pad_length) : "") : ""
                output[output.length] = match[5] ? sign + arg + pad : (pad_character === "0" ? sign + pad + arg : pad + sign + arg)
            }
        }
        return output.join("")
    }

    sprintf.cache = {}

    sprintf.parse = function(fmt) {
        var _fmt = fmt, match = [], parse_tree = [], arg_names = 0
        while (_fmt) {
            if ((match = re.text.exec(_fmt)) !== null) {
                parse_tree[parse_tree.length] = match[0]
            }
            else if ((match = re.modulo.exec(_fmt)) !== null) {
                parse_tree[parse_tree.length] = "%"
            }
            else if ((match = re.placeholder.exec(_fmt)) !== null) {
                if (match[2]) {
                    arg_names |= 1
                    var field_list = [], replacement_field = match[2], field_match = []
                    if ((field_match = re.key.exec(replacement_field)) !== null) {
                        field_list[field_list.length] = field_match[1]
                        while ((replacement_field = replacement_field.substring(field_match[0].length)) !== "") {
                            if ((field_match = re.key_access.exec(replacement_field)) !== null) {
                                field_list[field_list.length] = field_match[1]
                            }
                            else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
                                field_list[field_list.length] = field_match[1]
                            }
                            else {
                                throw new SyntaxError("[sprintf] failed to parse named argument key")
                            }
                        }
                    }
                    else {
                        throw new SyntaxError("[sprintf] failed to parse named argument key")
                    }
                    match[2] = field_list
                }
                else {
                    arg_names |= 2
                }
                if (arg_names === 3) {
                    throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported")
                }
                parse_tree[parse_tree.length] = match
            }
            else {
                throw new SyntaxError("[sprintf] unexpected placeholder")
            }
            _fmt = _fmt.substring(match[0].length)
        }
        return parse_tree
    }

    var vsprintf = function(fmt, argv, _argv) {
        _argv = (argv || []).slice(0)
        _argv.splice(0, 0, fmt)
        return sprintf.apply(null, _argv)
    }

    /**
     * helpers
     */
    function get_type(variable) {
        return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase()
    }

    function str_repeat(input, multiplier) {
        return Array(multiplier + 1).join(input)
    }

    /**
     * export to either browser or node.js
     */
    if (typeof exports !== "undefined") {
        exports.sprintf = sprintf
        exports.vsprintf = vsprintf
    }
    else {
        window.sprintf = sprintf
        window.vsprintf = vsprintf

        if (typeof define === "function" && define.amd) {
            define(function() {
                return {
                    sprintf: sprintf,
                    vsprintf: vsprintf
                }
            })
        }
    }
})(typeof window === "undefined" ? this : window);
var assert = require("assert"),
    sprintfjs = require("../src/sprintf.js"),
    sprintf = sprintfjs.sprintf,
    vsprintf = sprintfjs.vsprintf

describe("sprintfjs", function() {
    it("should return formated strings for simple placeholders", function() {
        assert.equal("%", sprintf("%%"))
        assert.equal("10", sprintf("%b", 2))
        assert.equal("A", sprintf("%c", 65))
        assert.equal("2", sprintf("%d", 2))
        assert.equal("2", sprintf("%i", 2))
        assert.equal("2", sprintf("%d", "2"))
        assert.equal("2", sprintf("%i", "2"))
        assert.equal("2e+0", sprintf("%e", 2))
        assert.equal("2", sprintf("%u", 2))
        assert.equal("4294967294", sprintf("%u", -2))
        assert.equal("2.2", sprintf("%f", 2.2))
        assert.equal("10", sprintf("%o", 8))
        assert.equal("%s", sprintf("%s", "%s"))
        assert.equal("ff", sprintf("%x", 255))
        assert.equal("FF", sprintf("%X", 255))
        assert.equal("Polly wants a cracker", sprintf("%2$s %3$s a %1$s", "cracker", "Polly", "wants"))
        assert.equal("Hello world!", sprintf("Hello %(who)s!", {"who": "world"}))
    })

    it("should return formated strings for complex placeholders", function() {
        // sign
        assert.equal("2", sprintf("%d", 2))
        assert.equal("-2", sprintf("%d", -2))
        assert.equal("+2", sprintf("%+d", 2))
        assert.equal("-2", sprintf("%+d", -2))
        assert.equal("2", sprintf("%i", 2))
        assert.equal("-2", sprintf("%i", -2))
        assert.equal("+2", sprintf("%+i", 2))
        assert.equal("-2", sprintf("%+i", -2))
        assert.equal("2.2", sprintf("%f", 2.2))
        assert.equal("-2.2", sprintf("%f", -2.2))
        assert.equal("+2.2", sprintf("%+f", 2.2))
        assert.equal("-2.2", sprintf("%+f", -2.2))
        assert.equal("-2.3", sprintf("%+.1f", -2.34))
        assert.equal("-0.0", sprintf("%+.1f", -0.01))
        assert.equal("-000000123", sprintf("%+010d", -123))
        assert.equal("______-123", sprintf("%+'_10d", -123))
        assert.equal("-234.34 123.2", sprintf("%f %f", -234.34, 123.2))

        // padding
        assert.equal("-0002", sprintf("%05d", -2))
        assert.equal("-0002", sprintf("%05i", -2))
        assert.equal("    <", sprintf("%5s", "<"))
        assert.equal("0000<", sprintf("%05s", "<"))
        assert.equal("____<", sprintf("%'_5s", "<"))
        assert.equal(">    ", sprintf("%-5s", ">"))
        assert.equal(">0000", sprintf("%0-5s", ">"))
        assert.equal(">____", sprintf("%'_-5s", ">"))
        assert.equal("xxxxxx", sprintf("%5s", "xxxxxx"))
        assert.equal("1234", sprintf("%02u", 1234))
        assert.equal(" -10.235", sprintf("%8.3f", -10.23456))
        assert.equal("-12.34 xxx", sprintf("%f %s", -12.34, "xxx"))

        // precision
        assert.equal("2.3", sprintf("%.1f", 2.345))
        assert.equal("xxxxx", sprintf("%5.5s", "xxxxxx"))
        assert.equal("    x", sprintf("%5.1s", "xxxxxx"))

    })

    it("should return formated strings for callbacks", function() {
        assert.equal("foobar", sprintf("%s", function() { return "foobar" }))
        assert.equal(Date.now(), sprintf("%s", Date.now)) // should pass...
    })
})


相關文章