Not only is the Internet dead, it's starting to smell really bad.:2021年04月12日分

2021/04/12(Mon)

[プログラミング] gdtoaを使う(その3)

前回の続き、いつものことだが飽きた。

@ここは私の夢日記

旅館の宴会場で大学の先輩がギター弾いてたらどこからともなくゆるふわギャルが集まってきてライブやる流れになったので、俺も参加すべくギターを旅館の主に借りにいったのだがこれがサイバーパンクなマッドサイエンティストで、弦は無く指板上にプッシュボタンが実装されてるし *1ネックはボディにボールジョイント接続で360度グネグネ自由に曲がる *2トンデモギター。

おまけにトーンノブが20個くらいあってそれぞれ☢や☣️といった物騒なマークと㏃やら㏖といった謎の国際単位系が刻まれていて使い方がさっぱり判らないしちょっと回すだけで謎の警告音が鳴り響く。 そしてそれを繋ぐのがなぜか骨董品級のTEISCOのアンプで壊れててまったく音が出ず、お前何しに出てきたのという冷ややかな視線。 小一時間格闘してようやく弾けたのがストーンズのサティスファクションのリフ、場が完全に凍りついて目が覚めたおはようございます。

どうですかフロイト先生夢診断をお願いします。そうじゃねギターというのは男根の象徴だしバンドやろうぜなんて肛門期固着の強迫性障害の現れじゃよ。

どう考えても理系コンプレックスでみた夢ですね本当にありがとうございました、わからない俺は経済学部で数式の代わりに音楽史を学んでいた。 ご丁寧にピックがペラッペラでガッツのあるリフ弾くのに不利なやつしか手元に無いとか無駄にリアリティあったりスゴイね人間の脳がみせる幻覚、というかこの記事も夢の中の夢かもしれないZZZ…

@dtoaでprintf(3)の"%e"書式指定子をエミュってみる

前回のgdtoaでprintf %f変換するサンプルコードはまとめてBitBucket Snippetsに 貼っておいた。もうちょっとコード簡潔にできると思うけどまぁこんなもんで、あとは勝手に最適化しといて。

では今回はprintf %e変換をgdtoaで実装でっきるっかなでっきるっかなじゃねえよやるんだよ。

@ゆ…指数表記

書式指定子「%f」はふだん我々がよく目にする小数点数の表記法である

(?<符号>[+\-])?(?<整数部>[[:digit:]]+)((?<小数点>\.)(?<小数部>[[:digit:]]+))?

形式へ変換する。

いっぽう書式指定子「%e」は指数表記法への変換を行う、か…数学などの分野では非常に{大きな,小さな}数を表すのに指数というものを使う。

m × Re

で表現する

  • m … 仮数(Mantissa)
  • R … 基数(Radix)
  • e … 指数(Exponent)

ね、あいたたたたた頭痛が…昨日から腹の調子も…あっ天井のシミが睨んでる…

ところが電卓の7セグメントディスプレイやコンピューターのキャラクタ端末ではこの指数の上付き文字(Superscript)を表現できないので、代替表示としてE表記(E notation)というものが生まれたんでしたっけ?歴史とか知らんがな。

このE電じゃなくてE表記は以下のような形式となる。

(?<符号>[+\-])?(?<仮数部>(?<整数部>[[:digit:]])((?<小数点>\.)(?<小数部>[[:digit:]]+))?)(?<e表記>[eE])(?<指数部>(?<指数符号>[+\-])(?<指数>[[:digit:]]{2,}))

ん?どこにも基数が無いけど10進表記なので基数は10よって省略されております。

そんで指数表示では%fなら同じ1.0であっても

0.01e+02
0.1e+01
1e+00
10e-01
100e-02

と異なる表記ができてしまうのだけれど、printf(3)の%eによる出力では仮数部の整数部は1桁に加藤球よろしく統一してしまえばよろしい *3

@gdtoaに渡すパラメーター

これも過去回で触れたコメント文より。

                2 ==> max(1,ndigits) significant digits.  This gives a
                        return value similar to that of ecvt, except
                        that trailing zeros are suppressed.

やはり家出のドリッピーよろしく超訳すると(ワイは朝からゲリッピーです)

	2 ==>	最大(1,指定した数)が有効桁数だよ、これは%e変換とよく似た結果になる。
		ただし末尾の連続するゼロは取り除かれるよ。

と書かれているので2を指定する。

そして有効桁数だけど、Cでは小数点以下の有効桁数は指定の無い場合6(%fと同じやね)なんだけど、6を指定してはならない(戒め)。 さっきのコメント文であたかも「max(1,ndigits)」とあるので整数桁数1に小数点以下桁数ngigit返してくれるんやろうなぁと勘違いしてしまうのだけどさにあらずや。 なんと整数部の桁数は常に0として変換を行う、もしかして作者は英語がにがてか…!?

よってかならず+1することを忘れずに。

#include "gdtoa.h"

#define PRECISION	6

int
main(int argc, char *argv[])
{
	double dvalue;
	char *head, *tail;
	int prec = PRECISION, exp, neg, len;

	head = dtoa(dvalue, 2, prec + 1, &exp, &neg, &tail);
	len = tail - head;
	...
}

@実際のコード例

前回の%f変換ではdtoaが負の有効桁数を返すケースがあったのでA~Eのケース分類してそれぞれサンプルとコード例を書いたけど、E表記は必ず整数部一桁と単純化されているのでこ仮数部までの表示は至極簡単である。

まずは仮数部の出力。

#include <stdio.h>
#include <stdlib.h>

#include "gdtoa.h"

#define PRECISION	6

int
main(int argc, char *argv[])
{
	double dvalue = ...
	char *head, *tail;
	int prec = PRECISION, exp, neg, len, zeros;
	FILE *fp = stdout;

	head = dtoa(dvalue, 2, prec + 1, &exp, &neg, &tail);
	len = tail - head;
	if (neg && putc('-', fp) == EOF)
		abort();
        if (exp == 9999) {
                if (fwrite(head, 1, len, fp) != len)
			abort();
	} else {
		if (putc(*head, fp) == EOF)
			abort();
		if (prec > 0) {
			if (putc('.', fp) == EOF)
				abort();
			if (--len > 0 && fwrite(&head[1], 1, len, fp) != len)
				abort();
			for (zeros = len; zeros < prec; ++zeros) {
				if (putc('0', fp) == EOF)
					abort();
			}
		}
	}
	freedtoa(head);
	exit(EXIT_SUCCESS);
}

はい、なんも頭使うとこ無いですね。

仮数部についても戻り値のexpの値をみれば一目瞭然なので

#include <stdio.h>
#include <stdlib.h>

#include "gdtoa.h"

#define PRECISION	6

int
main(int argc, char *argv[])
{
	double dvalue = ...
	char *head, *tail;
	int prec = PRECISION, exp, neg, len, zeros;
	FILE *fp = stdout;

	head = dtoa(dvalue, 2, prec + 1, &exp, &neg, &tail);
	len = tail - head;
	if (neg && putc('-', fp) == EOF)
		abort();
        if (exp == 9999) {
                if (fwrite(head, 1, len, fp) != len)
			abort();
	} else {
		if (putc(*head, fp) == EOF)
			abort();
		if (prec > 0) {
			if (putc('.', fp) == EOF)
				abort();
			if (--len > 0 && fwrite(&head[1], 1, len, fp) != len)
				abort();
			for (zeros = len; zeros < prec; ++zeros) {
				if (putc('0', fp) == EOF)
					abort();
			}
		}
		if (fprintf(fp, "e%+.2d", exp - 1) < 0)
			abort();
	}
	freedtoa(head);
	exit(EXIT_SUCCESS);
}

注意が必要なのは

  • precを+1してるので戻り値のexpから-1するのを忘れないこと
  • 指数は最低2桁表示で1桁ならゼロ埋めが必要

ってとこくらいすかね。

…はいちょっと最後指数の表示に

		if (fprintf(fp, "e%+.2d", exp - 1) < 0)

とかズルぶっこいてますね、コードが煩雑になるので判りやすいようワザとなのだが使わないコードにすると

#include <assert.h>
#include <float.h>
#include <stdio.h>
#include <stdlib.h>
#include "gdtoa.h"

#define PRECISION	6
#define MAXEXPSIZ	4

int
main(int argc, char *argv[])
{
	double dvalue = ...
	char *head, *tail, expstr[MAXEXPSIZ + 2];
	int prec = PRECISION, exp, neg, len, zeros, expsig;
	FILE *fp = stdout;

	head = dtoa(dvalue, 2, prec + 1, &exp, &neg, &tail);
	len = tail - head;
	if (neg & putc('-', fp) == EOF)
		abort();
        if (exp == 9999) {
                if (fwrite(head, 1, len, fp) != len)
			abort();
	} else {
		if (putc(*head, fp) == EOF)
			abort();
		if (prec > 0) {
			if (putc('.', fp) == EOF)
				abort();
			if (--len > 0 && fwrite(&head[1], 1, len, fp) != len)
				abort();
			for (zeros = len; zeros < prec; ++zeros) {
				if (putc('0', fp) == EOF)
					abort();
			}
		}
		if (--exp >= 0) {
			assert(exp <= DBL_MAX_EXP);
			expsig = '+';
		} else {
			assert(exp >= DBL_MIN_EXP);
			expsig = '-';
			exp = -exp;
		}
		tail = &expstr[sizeof(expstr)];
		if (exp > 9) {
			do {
				*--tail = '0' + (exp % 10);
				exp /= 10;
			} while (exp > 9);
			*--tail = '0' + exp;
		} else {
			*--tail = '0' + exp;
			*--tail = '0';
		}
		*--tail = expsig;
		*--tail = 'e';
		len = &expstr[sizeof(expstr)] - tail;
		if (fwrite(tail, 1, len, fp) != len)
			abort();
	}
	freedtoa(head);
	exit(EXIT_SUCCESS);
}

長げぇよ!まぁそこ略すよりサンプルなんだしエラー処理はしょれよ!という話もあるけど、コピペプログラマーがペタペタァッwww!した時に大災害が起きないようなるべくエラー処理もサンプルに含めるのが俺のジャスティス。 なら変数の使い回しもやめろやという話もある(ぉ

ちなみにMAXEXPSIZ = 4という正体不明の定数だけど、これはC99の5.2.4.1にてISO/IEC 60559(IEEE 754のISO名)を満たす要件として

DBL_MIN_EXP	-1021
DBL_MAX_EXP	+1024

と指数の最少最大の推奨値が書いてあり、どっちも4桁で収まるからの数字ちゅうことね。 もちろん異なる値の実装も存在する可能性あるのでもっと真面目にサイズ計算すべきかもしれんが飽きてきたのでめんどくせえ、よって宿題としておく。

なおfloat.hの定義はコンパイラの__DBL_MIN_EXP__/__DBL_MAX_EXP__という事前定義マクロの値を参照する実装が多いと思うので、以下で実際の値は確認してくれ。

$ echo __DBL_MIN_EXP__ | gcc -E -
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "<stdin>"
(-1021)
$ echo __DBL_MAX_EXP__ | gcc -E -
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "<stdin>"
1024

@次回予告

次回は"%g"について、そして可能であれば"%a"の実装に今度はhdtoaを使うって話も書ければいいですね…まぁまた変な夢見たらこの連載は中止ってことで。

*1:これはガチで存在する、 YAHAMA EZ-EGが有名やね。
*2:さすがにこれは見たことが無い、ただ世界はネタギターで溢れているので存在しても不思議ではない。
*3:もちろんscanf(3)の%eそして同等の変換をすることになってるstrtod(3)はいかなる形式でもdoubleに変換できなければならない。