2021/04/17(Sat)
○[プログラミング] gdtoaでレッツprintf(3)実装(その6)
前回のa変換についてもコード整理したものを ここに貼っといた、幅指定によるスペース埋めそしてゼロ埋め(0)そして左寄せ(-)書式指定子の実装もつっこんだので若干読みづらいコードになったけど(それでもNのvfwprintf.c読むよかマシだと思う)より実践的なコードになったと思う。
あわせて他のe/f/g変換も対応してあるので、こんなもんに興味のある奇人は ここ見てね。
なんかディアゴスティーニの週刊printf(3)、毎週送られてくるコードを組み合わせるとオレオレprintf(3)が完成みたいなノリになってきたけど、初巻900円以降続刊は1800円で数年後の100巻目でprintf(3)がようやく完成とか誰が買うのそんな雑誌。
つーか今回でgdtoaの使い方についてはおしまいなのだけど、本当にprintf(3)作る気ならlong doubleサポートのためにldtoa/hldtoa周りも書かねばならんのだ、ほんとprintf(3)ってそびクソだなぁ…
今回のコードで追加した部分、幅指定の実装の説明だけしとくか、dtoa/hdtoaの返す結果から最終的に組み上がる文字列の長さの計算ルーチン。
f変換の場合は以下の通り。
static inline int
cvt_fsize(int sign, int len, int prec, int exp, int flags)
{
int s, i, f, d;
s = (sign) ? 1 : 0;
i = (exp <= 0) ? 1 : exp;
f = (exp > len) ? 0 : len - exp;
if (f < prec)
f = prec;
d = (f > 0 || flags & SHARP) ? 1 : 0;
return s + i + f + d;
}
s(ign part)が符号、i(nteger part)が整数部、f(ractional part)が小数部、d(ecimal point)が小数点、それぞれの長さの計算方法ね。 その2で表まで組んでexpの値について説明してあるから理解してれば判るとは思うけど、ざっくりまとめると
- exp(dtoaの返す文字列の先頭が小数点からいくつ離れてるか)が0より大きければ整数部の長さはexp
- expが0以下なら整数部の長さは常に1(0.ではじまるから)
- expがlenより大きければ小数部は出現しない
- expがlen以下なら小数部の長さはlen(dtoaの返す文字列の長さ) - expになる(expが負の値であっても)
- ↑よりprec(精度)の方が大きければゼロ埋めが発生するのでそっち優先
- 小数点が現れるのは小数部が出現する場合だけではない、#書式指定子があったら強制表示
ってとこですかね、ああこれ典型的なソースのコメントに「iに1を足す」って書くやつだ…
もちろんLC_NUMERIC対応を実装すればさらにここに桁区切り文字の長さも加わってくるのだけど、今回はここまで。
e変換の場合はこちら。
static inline int
cvt_esize(int sign, int len, int prec, int exp, int flags)
{
int s, i, f, d, e;
s = (sign) ? 1 : 0;
i = 1;
f = len - 1;
if (f < prec)
f = prec;
d = (f > 0 || flags & SHARP) ? 1 : 0;
if (exp < 0)
exp = -exp;
#if 0
e = 2;
if (exp > 9) {
do {
++e;
exp /= 9;
} while (exp > 9);
++e;
} else {
e += 2;
}
#else
/* assume DBL_MIN_EXP(-1021) DBL_MAX_EXP(1024) */
if (exp < 100)
e = 4;
else if (exp < 1000)
e = 5;
else
e = 6;
#endif
return s + i + f + d + e;
}
e(notation)が指数部の計算、こちらもざっとまとめると
- 仮数の整数部の長さは常に1
- 仮数の小数部はlenから↑の1を引いた長さ
- ↑よりprec(精度)の方が大きければゼロ埋めが発生するのでその長さ
- 小数点が現れるのは小数部が出現する場合だけではない、#書式指定子があったら強制表示
- 指数部の数値部分は最低2ケタ
最後にa変換。
static inline int
cvt_asize(int sign, int len, int prec, int exp, int flags)
{
int x, s, i, f, d, p;
x = 2;
s = (sign) ? 1 : 0;
i = 1;
f = len - 1;
if (f < prec)
f = prec;
d = (f > 0 || flags & SHARP) ? 1 : 0;
if (exp < 0)
exp = -exp;
#if 0
p = 2;
while (exp > 9) {
++p;
exp /= 9;
}
++p;
#else
/* assume DBL_MIN_10_EXP(-307) DBL_MAX_10_EXP(308) */
if (exp < 10)
p = 3;
else if (exp < 100)
p = 4;
else
p = 5;
#endif
return x + s + i + f + d + p;
}
xは最初の0xの長さ、p(notation)が指数部の計算、e変換とほとんど一緒だけど
- 指数部の数値部分はe変換とは異なり最低1ケタ
ちゅーとこに注意。
んでrpad/lpad関数でフラグが立ってれば左右どちらかをスペースあるいはゼロで埋めるって寸法よ。
#define PADSIZ 20
static const char zeros[PADSIZ] = "00000000000000000000";
static const char spaces[PADSIZ] = " ";
static inline int
pad(const char *filler, int len, FILE *fp)
{
while (len > PADSIZ) {
if (fwrite(filler, 1, PADSIZ, fp) != PADSIZ)
return 1;
len -= PADSIZ;
}
if (fwrite(filler, 1, len, fp) != len)
return 1;
return 0;
}
static inline int
lpad(int sign, int width, int siz, int flags, FILE *fp)
{
if (width > siz && (flags & (MINUS|ZERO)) == 0 &&
pad(spaces, width - siz, fp))
return 1;
if (sign && putc(sign, fp) == EOF)
return 1;
if ((flags & HEX) && fwrite("0x", 1, 2, fp) != 2)
return 1;
if (width > siz && (flags & (MINUS|ZERO)) == ZERO &&
pad(zeros, width - siz, fp))
return 1;
return 0;
}
static inline int
rpad(int width, int siz, int flags, FILE *fp)
{
if (width > siz && (flags & MINUS) &&
pad(spaces, width - siz, fp))
return 1;
return 0;
}
static inline int
cvt_afmt(char *head, int len, int width, int prec, int exp, int flags, FILE *fp)
{
int sign, size;
sign = cvt_sign(flags);
size = cvt_asize(sign, len, prec, exp, flags);
if (lpad(sign, width, size, flags|HEX, fp))
return 1;
...
if (rpad(width, size, flags, fp))
return 1;
return 0;
}
左埋めの場合スペースとゼロで符号および0xの出現位置が変わるので、lpad関数の中で出力してしまう。
コードをシンプルにすべく条件分岐が若干非効率になっとるけど、そもそもprintf(3)自体が不経済なシロモノなので些細な性能問題とかは無視する。 最適化なんぞコンパイラがやってくれんだろ(鼻ホジ)。
以前に古いNのprintf(3)実装のプロファイルをとった時、整数型のlやll書式指定子を実装するのにいちいちint/long/long longでコード別にしたくないからintmax_tで統一とかやらかしとって、そこで性能ガタ落ちしてた記憶があるな。 今はすべて別関数になっとるけど。
話は変わるが、今のglibcにはI書式指定子という拡張があって整数型(i/d/u)の場合0-9をlocaleの代替文字列で出力するなんて機能あるんだけど、これLC_NUMERICにはそんなデータ持ってるはずないんだけどもlocaledef拡張したんかな。 まさかLC_TIMEのALT_DIGITを使うわけにはいかないだろうし。
まぁLinuxインストールして確認すりゃいいんだろけど最近はVMwareですら億劫なのでやる気にならん、もう何もかもがめんどくさいしWSL2とかもっとアレ。