LoginSignup
11
10

More than 3 years have passed since last update.

agh.sprintf.js - JavaScript で sprintf を実装する

Last updated at Posted at 2015-05-29

概要 JavaScript で実装された sprintf はたくさんありますが、C で定義されている機能を正確に実装しているものは殆どありません。ここでは C, POSIX の機能をフルに実装した agh.sprintf.js を紹介します!

1 agh.sprintf.js

agh.sprintf.js @ GitHub:
JavaScript で書かれた sprintf%g, %a, %n%3$d などにも対応しています!

JavaScript で sprintf を実装した例は探すとたくさん見つかりますが、満足に使えるものがなくて自分で実装したものを、公開してみることにします。中身についても少し説明して、既存の他実装の機能とも較べてみます。この実装では C, POSIX の規格にできるだけ従って真面目に (= できるだけ厳密に) 実装したつもりです。実は、実際に書いたのは1,2年前(記事投稿時から起算)なのですが、今でもまともに使える実装は他に見当たらないようなので、公開する意義はあると思って投稿しました。

1.1 使い方

HTML

$ git https://github.com/akinomyoga/agh.sprintf.js.git
$ cd agh.sprintf.js
<script type="text/javascript" charset="utf-8" src="agh.sprintf.min.js"></script>
<script type="text/javascript">
var result1 = sprintf("書式指定文字列", ...args);
var result2 = vsprintf("書式指定文字列", [...args]);
</script>

Node

$ npm install agh.sprintf
const agh = require('agh.sprintf');
var result1 = agh.sprintf("書式指定文字列", ...args);
var result2 = agh.vsprintf("書式指定文字列", [...args]);

1.2 対応している書式指定

書式 := % 引数? フラグ? 幅? 精度? サイズ? 変換

  • 変換 := diuoxXfFeEgGaAcCsSnp% から1文字
  • := 整数 | * | * 整数 $
  • 精度 := . 整数 | .* | .* 整数 $
  • サイズ := hh | h | l | ll | t | z | I | I32 | I64 | q | j | L | w
  • フラグ := ( '' | - | + | # | 0 | ' ) +
  • 位置 := 整数 $

詳細は https://github.com/akinomyoga/agh.sprintf.js/blob/master/README.md にて。

1.3 特徴

  • C, POSIX の機能を網羅
  • 特に %g conversion (短い表記、有効数字による精度指定) に対応
  • 浮動小数点数の正しい丸め
  • %#x の(値が 0 のときの)正しい出力、%#f, %#g の正しい出力、etc.

「この機能は実装しているけれど、あの機能は実装していない」というような状態だと、使う時に一々対応している機能を確認しなければならず面倒なので、C, POSIX にある機能は全て対応しています。特に %g も実装しています。%g は printf の標準の機能で自分は頻繁に使うのですが、他に実装しているものは少ないようです(1つしか見付かりませんでした…)。

また、浮動小数点数の変換も「num.toString().slice(...)」的ないいかげんなものではなく、ちゃんと実装しています。例えば、最後の桁の所で四捨五入しています。また、倍精度以上の桁数を出力するときも安易に 0 を出力するということはしていません (瑣末かもしれませんが)。

他に、他実装でいいかげんになっている %#x, %#f, %#g なども丁寧に実装しています。

1.3.1 余談: %g について

%g は浮動小数点数を出力する形式の一種であり、特におすすめです! 使っている人が少ないのは残念です。基本的に一番文字数が少なくなる様な表現で出力してくれるので、人間にとって見やすい形になってくれます。また、出力桁数は有効数字で計算してくれます (既定は6桁)。出力を %f と比較すると以下のようになります:

数字 %g %f
1.23 1.23 1.230000
3.14159265 3.14159 3.141593
1020 1e20 100000000000000000000.000000

2 実装の方法(中身について少し)

特に面白いことをしているというわけではないのですが、実装の概略を紹介しておきます。基本的には、正規表現を用いて % で始まる書式指定を置換するという構造にしています。雰囲気は下の様な感じです。

function vsprintf(fmt,args){
  var iarg=0;
  return fmt.replace(/%書式指定/g, function($0, $1, $2, ...){
    return some_conversion(args[iarg++], $0, $1, $2, ...);
  });
}

some_conversion の部分は大体以下の様な流れになっています。

  1. 書式指定の解析 → フラグ・精度・幅などの具体的な値を取得
  2. 引数を読み取って変換子に対応する値 (整数、小数、文字列など) を取得
  3. 符号("-", "+", " " など)・接頭辞("0x", "0" など)を生成
  4. 値から内容の文字列を生成(2015 → "7DF" など)
  5. 左右のパディングを生成
  6. 3.-5. を繋げる

各stepで変換子(diuxXfegなど)ごとに処理が少しずつ異なります。以下のような変換子の辞書を作って違いに対応しています(※つまり、この辞書に追加するだけで簡単に変換子を追加できる)

var conversions={
  d:{getv:getIntegerValue, integral:true, signed:true, prefix:null, conv:convertDecimal},
  i:{getv:getIntegerValue, integral:true, signed:true, prefix:null, conv:convertDecimal},
  u:{getv:getUnsignedValue, integral:true, signed:false, prefix:null, conv:convertDecimal},
  o:{getv:getUnsignedValue, integral:true, signed:false, prefix:prefixOctal, conv:convertOctal},
  x:{getv:getUnsignedValue, integral:true, signed:false, prefix:prefixLHex, conv:convertLowerHex},
  X:{getv:getUnsignedValue, integral:true, signed:false, prefix:prefixUHex, conv:convertUpperHex},
  e:{getv:getFloatValue, integral:false, signed:true, prefix:null, conv:convertScientific},
  E:{getv:getFloatValue, integral:false, signed:true, prefix:null, conv:convertScientificU},
  f:{getv:getFloatValue, integral:false, signed:true, prefix:null, conv:convertFloating},
  F:{getv:getFloatValue, integral:false, signed:true, prefix:null, conv:convertFloatingU},
  g:{getv:getFloatValue, integral:false, signed:true, prefix:null, conv:convertCompact},
  G:{getv:getFloatValue, integral:false, signed:true, prefix:null, conv:convertCompactU},
  a:{getv:getFloatValue, integral:false, signed:true, prefix:prefixFloatLHex, conv:convertScientificHex},
  A:{getv:getFloatValue, integral:false, signed:true, prefix:prefixFloatUHex , conv:convertScientificHexU},
  c:{getv:getCharValue, integral:false, signed:false, prefix:null, conv:convertChar},
  C:{getv:getCharValue, integral:false, signed:false, prefix:null, conv:convertChar},
  s:{getv:null, integral:false, signed:false, prefix:null, conv:convertString},
  S:{getv:null, integral:false, signed:false, prefix:null, conv:convertString},
  p:{getv:getUnsignedValue, integral:false, signed:false, prefix:prefixPointerHex, conv:convertUpperHex},
  n:{getv:null, integral:false, signed:false, prefix:null, conv:convertOutputLength},
  '%':{noValue:true, integral:false, signed:false, prefix:null, conv:convertEscaped}
};

変換子の実装で特に中心となるのが内容の文字列を生成する部分です。上の辞書項目の conv メンバに設定された関数がそれに当たります。整数のための convertDecimal, convertOctal, convertLowerHex, convertUpperHex、小数のための convertScientific, convertCompact, convertFloating, etc.、文字・文字列のための convertChar, convertString で内容の文字列を決定します。

例として、浮動小数点数の内容の計算で何をやっているかについて触れてみます。浮動小数点数は、%f/%g/%e に応じて精度を決定してから仮数部を生成します。その後で、小数点・桁区切・指数部などの挿入を行います。仮数部は value.toString(base) は使わずに、剰余と掛け算で生成しています。大体以下のような感じの関数を convertHoge から呼び出して使います:

function generateFloatingSequence(value, precision, base) {
  var ret = "";
  while(--precision>0)
    ret += ""+ (0 | value = value * base % base);

  // 最後の桁の四捨五入の処理 (略)
  ret = ...;
}

(より詳しい実装については agh.sprintf.js を直接御覧ください)

3 既存の実装と比較

3.1 俯瞰

検索してでてきた既存の実装についてまとめました。個人的によくできていると思うものから順に紹介します。

[1] JavaScript sprintf function - php.js

対応機能: 変換 scboxXuideEfFgG%, フラグ /[-+ 0#]|'./, 精度, *

機能としては十分といって良いと思います。他の実装に較べて足りないのは、位置パラメータ n$ ぐらいですが、これは使う人も多くはなさそうなので余り問題ではないように思います。検索して引っかかった中で、殆ど唯一のまともな実装です。

他、細かいことをいうとすれば、変換 a, A, n, p 辺りがありません。見た感じ浮動小数点数の # フラグには対応していません。' フラグで桁区切 (1,000,000 の様な3桁毎の区切り) を出力する機能もない。が、これらはそれほど重要ではありません。

[2] alexei sprintf.js, node-sprintf, JavaScript sprintf, a, b, c

対応機能: 変換 bcideufosxX%j, フラグ /[-+]|'./, 精度, *, n$

色々な所に派生物があるようですが、これらは基本的に同一の実装です。
node-sprintf としても提供されています。一番使われている実装ではないかと思われます。しかし、残念なことに %g がありません (個人的にこれは痛いです)。また、フラグも中途半端にしか対応していません。
ただ、独自機能の named arguments (%(memberName)d)は注目に値します。

[3] Train of Thoughts POSIX sprintf(3)

対応機能: 変換 %diouxXfFcs, フラグ /[-+#0' ]/, 精度, *, n$
珍しく桁区切に対応しています。やはり %g がないが、他は比較的よく実装されています。

[4] Jan Moesen Jan! » JavaScript » sprintf() and printf()

対応機能: 変換 bcdufosxX%, フラグ /-|'./
例によって %g がない。また %e もない。フラグも少ない。精度の指定もない。

[5] Jakob Westhoff sprintf.js - An almost feature complete sprintf implementation

対応機能: 変換 bcdfosxX, フラグ /[-+0 ]|'./, 精度
almost complete という割に機能はそんなにない。Jan! の機能を再実装しただけのように見えます。

[6] uupaa JavaScriptでsprintfとかprintfとか

対応機能: 変換 duoxXfcs%, フラグ #, 精度, n$
例によって %g %e がない。フラグも少ない。精度や位置パラメータは指定できる。

[7] AYA sprintf

対応機能: 変換 dfeE, フラグ /[-+ 0]/
浮動小数点数だけの対応。

[8] 高度な JavaScript 技集

対応機能: 変換 luUoOxXsc, フラグ /[-0]/, 精度, サイズ, *
一番需要のある(面倒な)浮動小数点数に全く対応していません。

[9] 他?

(他にも sprintf の JavaScript 実装があったら教えて下さい!)

3.2 比較

C, POSIX で定義されている printf 系の機能の実装に関しては、agh.sprintf.js は他実装には負けていないつもりです。

一方で非標準機能については積極的には実装していません。他実装に多く見られる非標準機能としては以下の様なものがありますが、これらについては未実装です:

  • %b で整数を2進表示で出力する。
  • フラグ '? でパディング文字を指定する。但し、これは SUSv2 の3桁区切フラグ "'" と衝突することに注意する。もしパディング文字指定を POSIX 的に実装するとしたら strfmon(3)=? を採用するのが自然だろうか。
  • Named arguments (上記[2]の独自機能) sprintf("%(users[0].name)s", {users:[{name:"hello"}]}) 的なもの。これは便利そうである。但し、strfmon(3) および java.io.PrintStream.printf のフラグ "("「負の数を括弧で囲む」と衝突する。
  • 変換 j ([2]独自機能) JSON として文字列に変換し出力する。これも興味深い。但し、C99 の intmax_t サイズ指定子と衝突する。

他に比較項目として考えるべきものに動作速度があります。しかし、agh.sprintf.js は特に速度について意識して実装したものではないですし、実測するのが面倒なので省略します。。。

編集: Train of Thoughts POSIX sprintf(3) を追加
編集: 中身の説明を追記、タイトルを多少分かりやすく
編集 (2019-08-31): 文章を敬体に変更

他の sprintf 実装 (未確認)

11
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10