JavaScriptでsprintfとかprintfとか

CoverFlow作ってる最中ですが、Firefox以外のブラウザでうまく動かせていない(canvasの互換性が…)ので、気晴らしに30分ほどでsprintfを作ってみました。

ここまで読んで、「はいはい、JavaScriptでsprintfが使えるとたぶん便利だけど、車輪の再開発だよねぇ」と思うのが普通の技術者の反応でしょうね。

ここで公開するsprintfの実装は、国際化(i18n)時に必要となる「プログラム(引数の順番)を変えずにフォーマット文字列だけを差し替える機能」を持ってるので、そういうケースでは特に活躍するかもしれません。

引数の番号付けと交換機能については、PHPのsprintf()関数の仕様を参考にしています。
一応速度のことも気にしているので、あまり重要ではない機能(0や空白のパディング,align)は削りました。

あとは…、"JavaScript sprintf"でググると上位20以内に登場する、

これらのsprintf実装よりも、1.2〜3倍ほど高速に動作するようです。

もっとお手軽で高速な文字列の変換手段がほしい方には、以下のような String::replace() による実装をお勧めします。

"The %d monkeys".replace("%d", 5);

使い方

基本的なフォーマットは、"%[サイン][最小幅][.精度]型" です。
// [と]でくくってるところは"JavaScriptのArray"の意味ではなく、BNFの"省略可能"という意味ですよ。念のため。

引数の番号付けと交換をする場合は、"%[引数の番号$][サイン][最小幅][.精度]型" といったフォーマットで指定します。
これは、1つの引数を何度も参照したり(実行結果の一番下の例がそれです)、見た目の引数の順番に依存したくない場合に使用します。

サインには、# が指定可能です。
#をつけると、%o, %x, %X で、"0", "0x", "0X" が先頭に付加されます。

  • "%#5x".sprintf(32) → " 0x20"

最小幅には、0〜9 までの数値を指定します。

精度は、ドット(".")に続けて 0〜9 の数値を指定します。%fなら小数点以下の桁数の指定になり、%sなら文字列の最大長の指定になります。

型には、d(符号付き10進), u(符号無し10進), o(符号無し8進), x(符号無し16進[小文字]),X(符号無し16進[大文字]), f(符号付きfloat), c(数値の文字化), s(文字列) が指定します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>JavaScript::String.sprintf() test</title>
<style>
body { background-color: black; color: white; }
</style>
</head>
<body> 
<textarea id="txt" rows="35" cols="80"></textarea>

<script type="text/javascript">

window.onload = function boot() {
  var e = document.getElementById("txt");
  e.value = ""
    + "\n\"[%d],[%d],[%d]\".sprintf(-123, 123, \"hoge\")       -> " + "[%d],[%d],[%d]".sprintf(-123, 123, "hoge")
    + "\n\"[%u],[%u],[%u]\".sprintf(-123, 123, \"hoge\")       -> " + "[%u],[%u],[%u]".sprintf(-123, 123, "hoge")
    + "\n\"[0%o],[0%o],[%o]\".sprintf(-123, 123, \"hoge\")     -> " + "[0%o],[0%o],[%o]".sprintf(-123, 123, "hoge")
    + "\n\"[0x%x],[0x%x],[%x]\".sprintf(-123, 123, \"hoge\")   -> " + "[0x%x],[0x%x],[%x]".sprintf(-123, 123, "hoge")
    + "\n\"[0X%X],[0X%X],[%X]\".sprintf(-123, 123, \"hoge\")   -> " + "[0X%X],[0X%X],[%X]".sprintf(-123, 123, "hoge")
    + "\n\"[%f],[%f],[%f]\".sprintf(-123.45, 123.45, \"hoge\") -> " + "[%f],[%f],[%f]".sprintf(-123.45, 123.45, "hoge")
    + "\n\"[%.1f],[%.2f],[%.0f]\".sprintf(-123.45,123.45,123.45)->" + "[%.1f],[%.2f],[%.0f]".sprintf(-123.45, 123.45, 123.45)
    + "\n\"[%c],[%c],[%c]\".sprintf(-123, 123, \"hoge\")       -> " + "[%c],[%c],[%c]".sprintf(-123, 123, "hoge")
    + "\n\"[%s],[%s]\".sprintf(\"hoge\", \"piyo\")               -> " + "[%s],[%s]".sprintf("hoge", "piyo")
    + "\n\"[%%],[%-],[%]\".sprintf()                         -> " + "[%%],[%-],[%]".sprintf()
    + "\n-width and precision------------1234567890----"
    + "\n\"[%#5d]\".sprintf(-123)      -> " + "[%#5d]".sprintf(-123)
    + "\n\"[%#5u]\".sprintf(-123)      -> " + "[%#5u]".sprintf(-123)
    + "\n\"[%#5o]\".sprintf(-123)      -> " + "[%#5o]".sprintf(-123)
    + "\n\"[%#5x]\".sprintf(-123)      -> " + "[%#5x]".sprintf(-123)
    + "\n\"[%#5X]\".sprintf(-123)      -> " + "[%#5X]".sprintf(-123)
    + "\n\"[%#5X]\".sprintf(\"hoge\")    -> " + "[%#5X]".sprintf("hoge")
    + "\n\"[%#5f]\".sprintf(-123.45)   -> " + "[%#5f]".sprintf(-123.45)
    + "\n\"[%#5.1f]\".sprintf(-123.45) -> " + "[%#5.1f]".sprintf(-123.45)
    + "\n\"[%#5c]\".sprintf(-123)      -> " + "[%#5c]".sprintf(-123)
    + "\n\"[%#5s]\".sprintf(-123)      -> " + "[%#5s]".sprintf(-123)
    + "\n\"[%#5.2s]\".sprintf(-123)    -> " + "[%#5.2s]".sprintf(-123)

    + "\n------argument numbering/swapping----"
    + "\n\"The %2$s contains [%1$#5x] monkeys\".sprintf(5, \"tree\")"
    + "\nV"
    + "\nThe %2$s contains [%1$#5x] monkeys".sprintf(5, "tree")

    + "\n\n\"The %2$s contains %1$d monkeys.\n That\'s a nice %2$s full of %1$d monkeys.\".sprintf(5, \"tree\")"
    + "\nV"
    + "\nThe %2$s contains %1$d monkeys.\n That\'s a nice %2$s full of %1$d monkeys.".sprintf(5, "tree");
}
</script>

<script>
/** tiny sprintf
 * format:
 *    "%"[arg-index-specifier"$"][sign-specifier][width-specifier][precision-specifier]type-specifier
 *
 * sign-specifier:
 *    "#": add "0", "0x", "0X" mark
 *       : typeが"o"なら先頭に"0"を追加します。
 *       : typeが"x"なら先頭に"0x"を追加します。
 *       : typeが"X"なら先頭に"0X"を追加します。
 *
 * width-specifier:
 *     n: minimize field width(0 to 9)
 *      : 最低何桁表示するかを指定します。指定可能な値は0〜9です。0で非表示になります。
 *
 * precision-specifier:
 *    "."n: floating-point limit width(0 to 9) for "f". string limit width(0 to 9) for "s"
 *        : ドットと数値を指定することで小数点以下の桁数や文字列の長さを指定できます。指定可能な値は0〜9です。
 *        : typeが"f"なら、小数点以下の桁数を指定します。浮動小数点値が丸められることがあります。0で小数点以下が非表示になります。
 *        : typeが"s"なら、文字列の長さを指定します。指定した長さ以上の文字は切り捨てられます。0で非表示になります。
 *
 * type-specifier:
 *    "d": signed decimal number
 *    "u": unsigned decimal number
 *    "o": unsigned octet number
 *    "x": unsigned hex number(lower case)
 *    "X": unsigned hex number(upper case)
 *    "f": floating-point number
 *    "c": the character with that ASCII value
 *    "s": string
 *    "%": "%"
 *
 * arg-index-specifier:
 *     n : arguments index
 *       : 引数のインデックスを指定します。引数の再利用と、引数の順序を指定することによりi18n化をサポートします。
 *
 */
if (!String.prototype.sprintf) {
  String.prototype.sprintf = function(args___) {
    var rv = [], i = 0, v, width, precision, sign, idx, argv = arguments, next = 0;
    var s = (this + "     ").split(""); // add dummy 5 chars.
    var unsign = function(val) { return (val >= 0) ? val : val % 0x100000000 + 0x100000000; };
    var getArg = function() { return argv[idx ? idx - 1 : next++]; };

    for (; i < s.length - 5; ++i) {
      if (s[i] !== "%") { rv.push(s[i]); continue; }

      ++i, idx = 0, precision = undefined;

      // arg-index-specifier
      if (!isNaN(parseInt(s[i])) && s[i + 1] === "$") { idx = parseInt(s[i]); i += 2; }
      // sign-specifier
      sign = (s[i] !== "#") ? false : ++i, true;
      // width-specifier
      width = (isNaN(parseInt(s[i]))) ? 0 : parseInt(s[i++]);
      // precision-specifier
      if (s[i] === "." && !isNaN(parseInt(s[i + 1]))) { precision = parseInt(s[i + 1]); i += 2; }

      switch (s[i]) {
      case "d": v = parseInt(getArg()).toString(); break;
      case "u": v = parseInt(getArg()); if (!isNaN(v)) { v = unsign(v).toString(); } break;
      case "o": v = parseInt(getArg()); if (!isNaN(v)) { v = (sign ? "0"  : "") + unsign(v).toString(8); } break;
      case "x": v = parseInt(getArg()); if (!isNaN(v)) { v = (sign ? "0x" : "") + unsign(v).toString(16); } break;
      case "X": v = parseInt(getArg()); if (!isNaN(v)) { v = (sign ? "0X" : "") + unsign(v).toString(16).toUpperCase(); } break;
      case "f": v = parseFloat(getArg()).toFixed(precision); break;
      case "c": width = 0; v = getArg(); v = (typeof v === "number") ? String.fromCharCode(v) : NaN; break;
      case "s": width = 0; v = getArg().toString(); if (precision) { v = v.substring(0, precision); } break;
      case "%": width = 0; v = s[i]; break; 
      default:  width = 0; v = "%" + ((width) ? width.toString() : "") + s[i].toString(); break;
      }
      if (isNaN(v)) { v = v.toString(); }
      (v.length < width) ? rv.push(" ".repeat(width - v.length), v) : rv.push(v);
    }
    return rv.join("");
  };
}
if (!String.prototype.repeat) {
  String.prototype.repeat = function(n) {
    var rv = [], i = 0, sz = n || 1, s = this.toString();
    for (; i < sz; ++i) { rv.push(s); }
    return rv.join("");
  };
}
</script>

</body>
</html>

実行するとこうなります。

"[%d],[%d],[%d]".sprintf(-123, 123, "hoge")       -> [-123],[123],[NaN]
"[%u],[%u],[%u]".sprintf(-123, 123, "hoge")       -> [4294967173],[123],[NaN]
"[0%o],[0%o],[%o]".sprintf(-123, 123, "hoge")     -> [037777777605],[0173],[NaN]
"[0x%x],[0x%x],[%x]".sprintf(-123, 123, "hoge")   -> [0xffffff85],[0x7b],[NaN]
"[0X%X],[0X%X],[%X]".sprintf(-123, 123, "hoge")   -> [0XFFFFFF85],[0X7B],[NaN]
"[%f],[%f],[%f]".sprintf(-123.45, 123.45, "hoge") -> [-123],[123],[NaN]
"[%.1f],[%.2f],[%.0f]".sprintf(-123.45,123.45,123.45)->[-123.5],[123.45],[123]
"[%c],[%c],[%c]".sprintf(-123, 123, "hoge")       -> [ナ],[{],[NaN]
"[%s],[%s]".sprintf("hoge", "piyo")               -> [hoge],[piyo]
"[%%],[%-],[%]".sprintf()                         -> [%],[%-],[%]
-width and precision------------1234567890----
"[%#5d]".sprintf(-123)      -> [ -123]
"[%#5u]".sprintf(-123)      -> [4294967173]
"[%#5o]".sprintf(-123)      -> [037777777605]
"[%#5x]".sprintf(-123)      -> [0xffffff85]
"[%#5X]".sprintf(-123)      -> [0XFFFFFF85]
"[%#5X]".sprintf("hoge")    -> [  NaN]
"[%#5f]".sprintf(-123.45)   -> [ -123]
"[%#5.1f]".sprintf(-123.45) -> [-123.5]
"[%#5c]".sprintf(-123)      -> [ナ]
"[%#5s]".sprintf(-123)      -> [-123]
"[%#5.2s]".sprintf(-123)    -> [-1]
------argument numbering/swapping----
"The %2$s contains [%1$#5x] monkeys".sprintf(5, "tree")
V
The tree contains [  0x5] monkeys

"The %2$s contains %1$d monkeys.
 That's a nice %2$s full of %1$d monkeys.".sprintf(5, "tree")
V
The tree contains 5 monkeys.
 That's a nice tree full of 5 monkeys.

一番下のあたりで、引数の入れ替えと、1つの引数を複数回参照しています。