速くてコンパクトな JavaScript 用の sprintf の実装

[javascript][sprintf] で検索してたどり着く方が多いようなので、uupaa-0.7.js から切り出して張ってみます。

/*!{id:"uupaa.js",ver:0.7,license:"MIT",author:"uupaa.js@gmail.com"}*/
window.sprintf || (function() {
var _BITS = { i: 0x8011, d: 0x8011, u: 0x8021, o: 0x8161, x: 0x8261,
              X: 0x9261, f: 0x92, c: 0x2800, s: 0x84 },
    _PARSE = /%(?:(\d+)\$)?(#|0)?(\d+)?(?:\.(\d+))?(l)?([%iduoxXfcs])/g;

window.sprintf = _sprintf;

function _sprintf(format) {
  function _fmt(m, argidx, flag, width, prec, size, types) {
    if (types === "%") { return "%"; }
    var v = "", w = _BITS[types], overflow, pad;

    idx = argidx ? parseInt(argidx) : next++;

    w & 0x400 || (v = (av[idx] === void 0) ? "" : av[idx]);
    w & 3 && (v = (w & 1) ? parseInt(v) : parseFloat(v), v = isNaN(v) ? "": v);
    w & 4 && (v = ((types === "s" ? v : types) || "").toString());
    w & 0x20  && (v = (v >= 0) ? v : v % 0x100000000 + 0x100000000);
    w & 0x300 && (v = v.toString(w & 0x100 ? 8 : 16));
    w & 0x40  && (flag === "#") && (v = ((w & 0x100) ? "0" : "0x") + v);
    w & 0x80  && prec && (v = (w & 2) ? v.toFixed(prec) : v.slice(0, prec));
    w & 0x6000 && (overflow = (typeof v !== "number" || v < 0));
    w & 0x2000 && (v = overflow ? "" : String.fromCharCode(v));
    w & 0x8000 && (flag = (flag === "0") ? "" : flag);
    v = w & 0x1000 ? v.toString().toUpperCase() : v.toString();

    if (!(w & 0x800 || width === void 0 || v.length >= width)) {
      pad = Array(width - v.length + 1).join(!flag ? " " : flag === "#" ? " " : flag);
      v = ((w & 0x10 && flag === "0") && !v.indexOf("-"))
        ? ("-" + pad + v.slice(1)) : (pad + v);
    }
    return v;
  }
  var next = 1, idx = 0, av = arguments;

  return format.replace(_PARSE, _fmt);
}

})();

PHP の sprintf の仕様をベースにしています。http://jp2.php.net/manual/ja/function.sprintf.php
また、uupaa-0.7.js の uu.fmt() の機能から、"%A" (PRINTABLE), "%j" (JSON), "%r" (RGB), "%R" (RGBA), "%h" (#000000) を削っています。

ざっくりとした説明

 format: %[arg-index$][flag][width][.precision][size]type

 arg-index: 引数の呼び出しと再利用,
      数値: 数値とダラー("$")により引数を0から始まる番号で呼び出すことができます。

 flag: 出力方法を指定します。
    以下をサポートします。
      "#": typeがo,x,Xなら文字列の先頭に"0","0x","0X"を追加します。
    以下は非サポートです。
      "-": 左詰で出力します。(非サポート)
      "+": 数値の前に符号を追加します。(非サポート)
      空白: 数値が負なら"-"を、それ以外なら空白を出力します。(非サポート)

 width: 出力する最小文字数か数値の最低桁数を指定します。
    以下をサポートします。
      数値: 数値により最低限表示する桁数を指定できます。0で非表示になります。
            数値や文字列の桁あわせに使用します。
    以下は非サポートです。
      "*": 引数で幅を指定します。(非サポート)

 precision: 出力する最大文字数か小数点以下の桁数を指定します。
    以下をサポートします。
      数値: 数値で、小数点以下の桁数や文字列の長さを指定できます。
            precisionの前にはドット(".")が必要です。
            typeがfなら小数点以下の桁数を指定します。
              浮動小数点値は丸められる場合があります。0で小数点以下は非表示になります。
            typeがsなら文字列の長さを指定します。
              指定した長さ以上の文字は切り捨てられます。0で文字列全体が非表示になります。
    以下は非サポートです。
      "*": 引数で精度を指定します。(非サポート)

 size: デフォルト引数のサイズを指定します。
    以下は非サポートです。
      "l": long型に変更します。(非サポート)

 type: 変数の型を指定します。
    以下をサポートします。
      "i": 符号付き8進数値(signed octet number)
      "d": 符号付き10進数値(signed decimal number)
      "u": 符号無し10進数値(unsigned decimal number)
      "o": 符号無し8進数値(unsigned octet number)
      "x": 符号無し16進数値[小文字](unsigned hex number[lower case])
      "X": 符号無し16進数値[大文字](unsigned hex number[upper case])
      "f": 浮動小数点([-]dddd.dddd)(floating-point number)
      "c": 文字の数値表現(the character with that ASCII value)
      "s": 文字列(string)
      "%": パーセント記号("%")そのものを出力
    以下は非サポートです。"egEG"は"f"で代用してください。
      "e": 浮動小数点([-]d.dddde[+/-]dddd)
      "g": 浮動小数点("f","e"の結果でより短い方を出力する)
      "E": 浮動小数点([-]d.ddddE[+/-]dddd)
      "G": 浮動小数点("f","E"の結果でより短い方を出力する)
      "n": 出力済みの文字数
      "p": ポインタ

速度的な

<!doctype html><html><head><title></title>
<script src="xxx_sprintf.js"></script>
<script>
    function perf(loop) {
      for (var i = 0; i < loop; ++i) {
        var n =  43951789;
        var u = -43951789;
        var c = 65;
        var d = 123.45678901234567890123456789;

        sprintf("%%c = '%c'<br />", c);
        sprintf("%%d = '%d'<br />", u);
        sprintf("%%u = '%u'<br />", n);
        sprintf("%%u = '%u'<br />", u);
        sprintf("%%f = '%010.10f'<br />", d);
        sprintf("%%o = '%o'<br />", n);
        sprintf("%%s = '%100.10s'<br />", "Ala-bala-portocala");
        sprintf("%%x = '%x'<br />", n);
        sprintf("%%X = '%X'<br />", n);
        sprintf("<br />%4$s, %3$s, %1$s, %2$s", 'c', 'd', 'b', 'a');
      }
    }
    var begin = +new Date;
    perf(1000);
    alert((+new Date - begin));
</script>
</head><body></body></html>

[javascript][sprintf]でぐぐって、上位10以内のライブラリと速度を比較してみました。

A B C
IE6 657 1516 1671
IE8 562 1312 1203
Opera10 625 922 1141
Firefox3.5.3 381 597 390
Google Chrome4dev 171 196 119

(単位ms)

見ろ、我軍は圧倒的でわ… あれ? Chrome4 で負けてる

反省会

  • 実は、まだまだ速度的/コード量的に改善の余地があります。
    • でも、あんまりやりすぎると黒魔術になってコードが読めなくなるので、これぐらいで。
  • B の %u がバグってた
  • C が Chrome で速いのは、正規表現を一切使わずに、文字列操作だけでやってるからのような気がする。
    • Chrome は文字列操作がめっちゃ速いんだろうか