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

2021/04/02(Fri)

[プログラミング] ISBN Where To

NなんとかBSDにはユーザーを増やすためのHow Toは存在しないがムカつくdeveloperを都市ごと吹き飛ばすためのICBM Where Toがあるってしょーもないネタページ、さすがに悪趣味過ぎたか今はページ消えたのね。 まぁ更新なにそれ旨いののJPの和訳ページにはまだ 残ってるけどな!

いやまあ今日はICBMでもSSBNでもなくISBNの話なんですけどね…

書籍のISBNって、ワイの貧しい脳味噌にかすかに残った10桁番号時代の記憶では ここにある表の通り、出版社記号桁数に応じてブロック分けされてるから単純なロジックでハイフネーションできたと記憶してたのだが、 13桁化する前あたりに苦し紛れな枯渇対策で空き領域を無理矢理転用したりしたようで、酷いセグメンテーション起こしててもはや通用しないのだね。

なもんで ここにあるRangeMessage.xmlというデータベースを使って照合しないとダメっちゅーことを本日学びました、多分数日で忘れると思うけど。

ちなみにPythonだと isbn_hyphenate というモジュールがあって上記のRangeMessage.xmlを元にハイフネーションしてくれるのだが、いっこバグがあってISBN13は問題ないのだがISBN10を食わせると、13桁への変換に必要な末尾のチェックディジット再計算を忘れてるので、不正なISBN13が生成されるねこれ。 ダメじゃん。

ちなみにチェックディジットの再計算のロジックを今さっきWikiPedia読みながら書いてみたけど、こんな感じのコード組み込めばいいはず。

import re

def isbn10to13(isbn):
    if not re.match('^\d{9}(\d|X)$', isbn):
        raise Exception
    checkdigit = isbn[-1]
    isbn = isbn[:-1]
    digit = 0
    for i in range(0, 9):
        digit += int(isbn[i]) * (10 - i)
    digit = 11 - digit % 11
    if '0123456789X0'[digit] != checkdigit:
        raise Exception
    isbn = '978' + isbn
    digit = 0
    for i in range(0, 12):
        digit += int(isbn[i]) * [1, 3][i % 2]
    digit = 10 - digit % 10
    isbn += '01234567890'[digit]
    return isbn

なおワイは全人生でPython歴15分くらいの「ぜんぜんわからない俺は雰囲気でPythonを書いてる」人なので、修正及びプルリクは誰かに任せた。

2021/04/04(Sun)

[Windows] Windows10のメモ帳が文字化けする

やはりあのスターの球団って、大輔という名前の監督は鬼門なんじゃないかな(挨拶)、毛髪の有無は無関係なもよう。 そろそろ「 三浦大輔監督とともに苦難を乗り越えてゆくスレ」が建ってたりするんだろうかね…

      _______ 
     /         \ 
   /      81  __ノ 
 / /\/\___ノ\ 
 | /    \ , , /  \ 
  |:::::::: (●)    (●) | 
  |::::::::   \___/   |
  ヽ:::::::::::::::  \/    ノ
「今日は負けないヨ・ロ・シ・ク!!」

   . . .... ..: : :: :: ::: :::::: :::::::::::: : :::::::::::::::::::::::::::::::::::::::::::::
        Λ_Λ . . . .: : : ::: : :: ::::::::: :::::::::::::::::::::::::::::
       /:彡ミ゛ヽ;)ー、 . . .: : : :::::: :::::::::::::::::::::::::::::::::
      / :::/:: ヽ、ヽ、 ::i . .:: :.: ::: . :::::::::::::::::::::::::::::::::::::::
      / :::/;;:   ヽ ヽ ::l . :. :. .:: : :: :: :::::::: : ::::::::::::::::::
 ̄ ̄ ̄(_,ノ  ̄ ̄ ̄ヽ、_ノ ̄
「試合ねぇだろ…」

さて本題、昔はUTF-8で保存すると本来あってはならないBOMを先頭にいれて保存しやがるタチの悪い振る舞いをしたWindowsの「メモ帳」アプリだけども、最近はふつーにBOM無しで保存してBOMつきで保存するには文字コード選択ドロップダウンで「UTF-8(BOM付き)」を選ばんとならんのだな。

ところで21世紀になるかならないかの頃に一部の人がBOMありが正しい!BOMなしのUTF-8はUTF-8Nと呼ぶべき!みたいな主張をしたせいで未だに数多くの珍記事が検索にヒットするわけだが、チェストん前名前訊くんは女々か? まぁインターネッツがいくら誤情報だらけになろうがもはや気にしたって時間の無駄である、書籍に還るよワイは。

そんなことよりもだね、デフォルトがBOM無しになったことでShift_JIS(Windows-31J)とUTF-8(BOMなし)の自動判定がアレになって文字が化けよることがママあるのよな。

うーんこんな実装だと、 UTF-8N復権派が「なぜなら俺は始祖マークを信じてる!」「うおおおおおおおおおおおおおおおおおおおおおおおおお!」して歴史捏造しだしそうなのでどうにかせーや。

というか こっちの記事だと「なおBOM無しはUTF-8Nと呼ばれることがある」なんて書いてあってまだまだ活動中の模様である。

この始祖マークってのは元IBMで現GoogleのMark Davisの事、なお進撃ネタでなくマジモンで始祖(Unicode Consortiumの創始者のひとり)である。

彼がIBM時代に書いた このページ(消失してるのでwebarchiveより)の中段くらいの「Table 2: UTF serializations」ってのがあってそこに

って書かれてたもんで、これをソースにしてたんだよなUTF-8N派は。

それでは始祖ご本人による弁明を お読みください、いやUTF-8Nはイタリック体だからセーフってさ、あなたUTF-8の項にBOMありと仕様相違なこと書いてたよね…これはお前が始めた物語だろ。

つーことで始祖も否定したUTF-8Nなんだが、復権派って輩ってのはまだほとんど解読できてない古語でも手前勝手に解釈してそれを真実と信じこむタイプだからな厄介なのよね。

そもそもUTF-8のメリットってのはUS-ASCII互換な事なのだけど、BOMありだと

という問題があってそっちの方がより重大な問題なのですわ。 文字コード自動判定なんてできたら便利ですね程度のシロモノのために、スクリプトがUTF-8で書けなくなるとかアホでしょ。

まぁプログラムがどうやって動くかなんてまともに知らん人でも口つっこめるのが文字コードの話であって以下略、なんか自分に盛大にブーメランが返ってくるのだが気にしない、イタタタタタ。

だいぶ脱線(いつも)したので話を戻そう、このメモ帳の文字化け問題でおもしろいのは同じファイルであっても

って謎の挙動を示すことなんだよな、なもんで大半の人はダブルクリックでしか開かんだろうからまずこの文字化けにはお目にかからないのであまり問題になって無さげ。

なぜこういう挙動を示すのかを考えてみると、もしかしてダブルクリックした時の文字コードの自動判定ってメモ帳ではなくシェル(エクスプローラー)側でやってんのかなぁと。 メモ帳は昔から変わらずおバカのままで、BOMあったらUTF-8無ければShift_JISと解釈してるのでは疑惑がある。

もしかしたらNTFSの拡張属性に文字コード名保存してるのかもと思ったけど、SysinternalのStreamsコマンド(もしかすると同等の機能がPowerShellにあるかもしれないが調べるのめどい)で確認したけど無さげ。 まぁシェルでなくメモ帳がやってるとしても、ダブルクリックとファイルで違う自動判定ロジックが動いてんだからまぁアレだわな。

いやまぁ自分はAAの編集以外はメモ帳でなくいまだCygwin上のvimであれこれ書くのでこれ以上深追いする気はないんだけどね。

いつまでCygwin使ってんのかといっても、もはやWSL(Windows Subsystem for Linux)という名前すら忘れてて今この記事に「みんなとっくにSFUに乗り換えたんだろうけど」とか書きそうになってしまったゾ。 おいおいSFU(Subsystem For Unix)っていつの時代だ、SUA(Subsystem for Unix-based Application)ですらねえ!

まぁCygwinがいまだにB21の頃のちょっとでもなにかしようとするとトラブルにぶち当たるアレだったなら速攻で乗り換えたんだろうが、もう何年もCygwinならしゃーない的な問題にぶち当たったことが無いのですわ。 いや単純に馴れきってしまっただけなんだろうか。

そいや文字コード自動判定がらみの話でついでだけど、perl5のEncode::Guessってけっこう優柔不断でうーんどっちか判んない!とばかりに候補複数返すの、古のjcode.pl使ってる歴史的コードを修正するときに案外使いづらいなって昔感じたことを思い出した。

まぁそもそも文字コー自動判定そのものが悪というのはワイは何年も前から言い続けてるわけですが、まぁ蝉の鳴き声に耳傾ける人なんてのはいないってことで。

2021/04/05(Mon)

[プログラミング] 車輪の再再再発明してみた

久しぶりにプログラミング言語Cなんぞ書くとほんと忘れておるな、ちょっと前はPowerShellそして最近はPerlに回帰したたので変数宣言しようとすると無意識で$つけてしまうわ。 あとやべーことにprintf(3)の書式すら忘れておった、printf(3)とか自分でワイド文字拡張の実装書いたりもして、かなり念入りに仕様読み込んだはずなんだけどな…

@何を再発明したのよ?

それを実装するのはあなたで100万人目です

な車輪の再再再発明、JSON parser for Cを書いたよ。 まぁ書いたといってもかなり以前にCitrusの内部で使う用に書いて微妙な気分になって放置したやつをひっぱりだして整理しただけなんだがな。

置き場所はいつものBitBucketのリポジトリ、 こちらとなっております。

まだ<sys/rbtree.h>とかN依存コードが若干残ってるので、世界で3人くらいしか動かせないと思う。

@しょーもないもん作って暇なん?

そうっすね、かの文豪であらせられる太宰治も

蝉は、やがて死ぬる午後に気づいた。ああ、私たち、もっと幸せになってよかったのだ。

と書き残しておられる、そんな心境です。

つーか俺だってこんなもん書いてる時間あったらカメラにフィルム詰めて花見に行きたかったですわ(全ギレ)!何が新しい生活様式ですかあああぁ!

書いた当時のモチベを説明すると、Citrusの各ctype/stdenc moduleはlocale/iconvデータのVARIABLEセクションにあるプロパティを読み込んで初期化するのだが、ここの文法がてんでバラバラなのだ。 そんなこんなで自分で書いたmoduleではコード共用すべくcitrus_prop.cというコードを書いたのだが、あまりにもやっつけ仕事でワイに見せるだけで顔真っ赤にしてヤダヤダするくらいの出来栄えなので書き直したかったのだ。

そんでどうせなら流行ってるしJSON形式いいよねというとこでこうなった。

ちなみにNには元々その手のプロパティを扱うライブラリにproplib(3)というものがあるのだけど、libc内でなくlibpropとして独立してる上に少々扱いづらいのだ(個人の感想です、誹謗中傷にあらず)。

@名前は?

えっなにそのAVの導入部みたいな質問…

名前なぁ、libJSONとかにするとjson-cとかの先達らとバッティングして、名前空間汚染野郎など命名されて今や腐臭を放っているインターネッツで誹謗中傷されそうなのでちょい捻って「libJamerSON」とさせていただいた、ワイの尊敬するベーシストのジェームス・ジェマーソンをリスペクト *1

ホラホラこんなクソ記事読んでないで聴け、世紀の名演を。

いやほんと5:05あたりのWhat's Happening Brotherにメドレー繋がるところのベースライン鳥肌ものですわぁ(恍惚)。

まぁ中見てこんなクソコードじゃぁジェマーソンじゃなくて「(検索の)邪魔ー(読んで)損」じゃねーかと言われたら申し訳ないが故人だしまぁいいだろう。

なおワイは命名というプログラマに必須の能力を欠いているので、デスクトップにはtest.cとかunko.plとかxxx/とかあsdfghjk.txtが転がってるのだが、さすがにお外に流すライブラリにそんな名前つけるのはどうかとでつけた名前なので、センスの事はとやかくいうな。

なおAPI名がjson_*でなくjamerson_*になることでタイプ量が4文字増えてめんどいのだけど、Cだしプリプロセッサでよきにはからえ(事故の元)。

@何で今更?

何年も埃かぶったコードをなんで今になってひっぱり出してきたかというと、先日書いた 記事のISBN自動ハイフネーションツールをお遊びがてらCで再実装しようと思った時、RangeMessage.xmlをCの構造体に変換するってーと、例えばC99の不完全配列型を使って

struct range {
	unsigned int min, max, length;
};
struct lengths_map {
	const char *prefix;
	struct range ranges[];
};

みたいな定義にしても、こいつは静的に初期化すること許されざるという制限がある。

struct lengths_map groups_length[] = {
  {
    "978",
    {
      {       0, 5999999,  1 },
      { 6000000, 6499999,  3 },
      ...
      {       0,       0, -1 }, /* end marker */
    },
  },
  {
    "979",
    {
      {       0,  999999,  1 },
      { 1000000, 1299999,  2 },
      ...
      {       0,       0, -1 }, /* end marker */
    }
  },
  {
    NULL
  } /* end marker */
};
とにかく初期化は認めん、ISO-Cのブランドに傷がつくからな…

と怒られるのでな。

なにがISO-Cブランドですかぁああ!! こんなジジババしか使わない言語に権威なんてありませぇええん!

ともあれ、こういうケースでは

struct range {
    unsigned int min, max, length;
};
struct lengths_map {
    const char *prefix;
    struct range *ranges;
};
struct range range_978[] = {
  {       0, 5999999,  1 },
  { 6000000, 6499999,  3 },
  ...
  {       0,       0, -1 }, /* end marker */
};
struct range range_979[] = {
  {       0,  999999,  1 },
  { 1000000, 1299999,  2 },
  ...
  {       0,       0, -1 }, /* end marker */
};
const struct lengths_map groups_length[] = {
  {
    "978",
    range_978
  },
  {
    "979",
    range_979
  }
  ...
  {
    NULL,
    NULL
  } /* end marker */
};

とかすりゃいいだけではあるんだが、ネストが深かったり大量データだったり複雑になってくると正直管理しきれなくなってくるからな(まぁ適当なスクリプト言語で自動生成しちまやいいだけではある)。

それにそもそもの問題として、Cソースに変換してしまうとRangeMessage.xmlが更新されるたびにバイナリを再コンパイルにしなきゃならんのでな。 更新頻度的が頻繁だったりするとわりとおつらいことになる。

なもんでRangeMessage.xmlを読んで動的に構築するのがベターかと思ったのだが、令和の新しい生活様式の時代にまだXMLなのかよ…という気分になるしわざわざlibxml2とか入れたくねえしexpatだと貧弱過ぎるしな *2、そういえば何十年振りだろうDTDが書かれてるXMLみたの読み方完全に忘れてたぞ…

つーことで(その1)XMLはやめてPerlのようなものでサクッとJSONに変換して読み込むことにしたんだけど、json-c入れようとしたらビルドにCMake要求されるのだが、そいつはC++で書かれとるせいでオレオレN6でビルドしたC++11対応のlibstdc++はlibmに不足する関数があるせいで<cmath>使えんのですわ、よってビルドエラーになるのだ *3。 あーlibmのマージ作業もやらんとならねぇな…

つーことで(その2)json-c使うのは諦めた(判断が早い)、そいや昔こんなの書いたねと思い出しひっぱり出してきたというしょーもないお話、どーでもいい話長すぎるわクソが。

@ これ使う意味ある(直球)?

無いねー全く無いねー、json-cとかがGPLなら、GPLを憎むあまりにどこの馬の骨ともしれんワイのクソコードだって2-clause BSDLならforkするぜ!って変人も現れるんだろうけどだいたいこの手のやつMIT Licenseだしな、まぁ要はbitbucketの肥やしになるだけです。

元はlibcに組込む目的なのでなるべくシンプルに余計なもの乗っけないように書いたのだけど、コード量的にもバイナリサイズ的にも他の実装と比べてそう小さいわけでもなく、微妙な気持ちになるよね。

@判った!GitHubにアップしたコードで適正年収診断!ってやつを試す気だね?

やんねーよあんなもん!某メガバンのソースコード流出事件かよ!あんな根拠の無いモン試すの「うわっ…私の年収、低すぎ…?」広告をクリックするタイプの人間だよ!あんなの コンプレックス産業の一形態だよ!

そもそも俺はGitHub使ってねーよ!いつだってマイナーな方を選ぶからBitBucket使ってるよ!逆神だよ!NikonとOLYMPUSだけは俺のせいじゃねーよ!

そういやBitBucketが去年8月でMercurialのサポート止めたけどリリースちゃんと読んでなかったからリポジトリが消されるとは思ってなかったよ! その点Google Codeは消す消すいっていつまでも残ってるよなと思ったけど今確認したら消えてたよ! 介護やらで忙殺されてた時期だったから気づいたらjqPlotってチャートライブラリのをforkしていくつかバグ潰したコードが消えてたよ! まぁ二度と使うことないだろうからいいけど! あんなわりと重大なバグのプルリク送ってもへんじがないしかばね *4なんて知らねーよ! 今だったらChart.js使うよ! 当時まだそっち存在しなかったんだよ!

@使い方は?

ドキュメントなんてあるわけないでしょおおお!私は書いたコードをすぐ放流したいし、すぐ忘れたいんですぅうう!
なのにOSS凶徒陣は何個ドキュメント書いたのかとか継続的インテグレーションがどうだのゴチャゴチャと!
私のコードは花粉症患者のチリ紙なんです!頭の固い先生方に認められなくて結構!!!

なんだとぉ…

@アマミヤ先生あんた正気か!?

ごめんねぇえええええええ!!

@せめて使い勝手がいいとかなんかいいとこないの?

まぁCだからおのずと限界はあるし、APIって好みも人それぞれだから自分の書いたコードの良し悪しとかさっぱりわからん、以下の使用例みて思うところがあれば森の中にわけいってそこに生えてる葦に向かって叫んでください、いつか葦による嘲笑が私のロバの耳にも届くかもしれません。

中身は前述のISBN13の自動ハイフン化ロジックすな。

/*-
 * Copyright (c) 2021 Takehiko NOZAKI,
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

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

#include "jamerson.h"

static jamerson_value_t value;
static jamerson_object_t lengths_map;

__attribute__((constructor))
static void constructor()
{
	FILE *fp;

	fp = fopen("isbn.json", "r");
	if (fp == NULL)
		abort();
	value = jamerson_load_file(fp);
	if (value == NULL)
		abort();
	lengths_map = jamerson_value_is_object(value);
	if (lengths_map == NULL)
		abort();
}

__attribute__((destructor))
static void destructor()
{
	/* you are paranoia, let's join malloc/free bikesched */
	if (value != NULL)
		jamerson_value_delete(value);
}

static inline int
prefixlen(const char *key, const char *prefix, unsigned int first7, size_t *ret)
{
	jamerson_object_t lengths, range;
	jamerson_array_t ranges;
	size_t siz, i, len;
	jamerson_number_t num;
	unsigned int min, max;

	lengths = jamerson_value_is_object(jamerson_object_get(lengths_map, key));
	if (lengths == NULL)
		return 1;
	ranges = jamerson_value_is_array(jamerson_object_get(lengths, prefix));
	if (ranges == NULL)
		return 1;
	siz = jamerson_array_size(ranges);
	for (i = 0; i < siz; ++i) {
		range = jamerson_value_is_object(jamerson_array_get(ranges, i));
		if (range == NULL)
			return 1;
		num = jamerson_value_is_number(jamerson_object_get(range, "min"));
		if (num == NULL)
			return 1;
		min = (unsigned int)jamerson_number_double_value(num);
		if (min > 9999999)
			return 1;
		num = jamerson_value_is_number(jamerson_object_get(range, "max"));
		if (num == NULL)
			return 1;
		max = (unsigned int)jamerson_number_double_value(num);
		if (max > 9999999)
			return 1;
		if (first7 >= min && first7 <= max) {
			num = jamerson_value_is_number(jamerson_object_get(range, "length"));
			if (num == NULL)
				return 1;
			len = (size_t)jamerson_number_double_value(num);
			if (len > 7)
				return 1;
			*ret = len;
			return 0;
		}
	}
	return 1;
}

static inline unsigned int
first7(const char *s)
{
	char buf[8];
	unsigned long l;

	strlcpy(buf, s, sizeof(buf));
	strlcat(buf, "0000000", sizeof(buf));
	l = strtoul(buf, NULL, 10);
	assert(l <= 9999999);
	return (unsigned int)l;
}

int
isbn13_hyphenate(char *dst, size_t dstsiz, const char *src)
{
	static const char * const prefixes[] = {
	    "groups_length", "publisher_length", NULL
	};
	const char * const *prefix;
	const char *with_hyphen = (const char *)dst;
	size_t srcsiz, len;
	int ret;

	if (dstsiz < 18)
		return 1;
	srcsiz = strlen(src);
	if (srcsiz != 13)
		return 1;

	dstsiz -= 3, srcsiz -= 3;
	memcpy(dst, src, 3);
	dst += 3, src += 3;
	*dst = '\0';

	for (prefix = prefixes; *prefix != NULL; ++prefix) {
		ret = prefixlen(*prefix, with_hyphen, first7(src), &len);
		if (ret)
			return ret;
		if (dstsiz < len + 1 || srcsiz < len)
			return 1;
		dstsiz -= len + 1, srcsiz -= len;
		*dst++ = '-';
		memcpy(dst, src, len);
		dst += len, src += len;
		*dst = '\0';
	}

	if (srcsiz < 2)
		return 1;
	--srcsiz;
	if (dstsiz < srcsiz + 4)
		return 1;
	*dst++ = '-';
	memcpy(dst, src, srcsiz);
	src += srcsiz, dst += srcsiz;
	dst[0] = '-';
	dst[1] = *src;
	dst[2] = '\0';

	return 0;
}

int
main(int argc, char *argv[])
{
	const char *without_hyphen = "9784870999237";
	char with_hyphen[18];

	if (isbn13_hyphenate(with_hyphen, sizeof(with_hyphen), without_hyphen))
		abort();
	printf("%s -> %s\n", without_hyphen, with_hyphen);

	exit(EXIT_SUCCESS);
}

こいつも書き殴っただけのレベルなのでコード整理したら、ISBN10 <-> 13桁変換ロジックなんかと一緒にライブラリとして放流するかもしれない。

読み込むisbn.jsonは以下のperl5のようなもので生成するなどした、Cとかperl5とかそんな加齢臭漂う言語ばかりで大丈夫なんですかね…

#!/usr/pkg/bin/perl

=head1 COPYRIGHT & LICENSE

 Copyright (c) 2021 Takehiko NOZAKI,
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions
 are met:
 1. Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

 THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGE.

=head1 NAME

 isbn_xml2any.pl - parse ISBN RangeMessage.xml and generate code

=head1 SYNOPSIS

 isbn_xml2any.pl [options] [file]

=head1 OPTIONS

=over 8

=item B<--format>

Specify output format, json(default) or c.

=item B<--help>

Print this help message.

=back
 
=cut

use strict;
use warnings;
use Getopt::Long;
use JSON;
use Pod::Simple::Text;
use Pod::Usage;
use XML::XPath;
use XML::XPath::XMLParser;
use Template;

my $meta_name = {
    'Source' => '/ISBNRangeMessage/MessageSource/text()',
    'SerialNumber' => '/ISBNRangeMessage/MessageSerialNumber/text()',
    'Date' => '/ISBNRangeMessage/MessageDate/text()'
};
my $lengths_map_name = {
    'groups_length'    => '/ISBNRangeMessage/EAN.UCCPrefixes/EAN.UCC',
    'publisher_length' => '/ISBNRangeMessage/RegistrationGroups/Group'
};

sub parse_copyright($$)
{
    my ($filename, $copyright) = @_;

    my $ps = new Pod::Simple::Text;
    my $out;
    $ps->output_string(\$out);
    $ps->parse_file($filename);
    my @lines = split(/\n/, $out);
    my $in_copyright = 0;
    foreach my $line (@lines) {
        if ($line =~ m/^COPYRIGHT/) {
            $in_copyright = 1;
        } elsif ($line =~ m/^[[:upper:]]+/) {
            $in_copyright = 0;
        } elsif ($in_copyright) {
            $line =~ s/^[[:space:]]+//;
            push(@{$copyright}, $line);
        }
    }
    shift(@{$copyright});
    pop(@{$copyright});
}

sub parse_lengths_map($$$$)
{
    my ($xpath, $name, $path, $isbn) = @_;

    $isbn->{lengths_map} = {};
    $isbn->{lengths_map}->{$name} = {};
    foreach my $lengths_map ($xpath->find($path)->get_nodelist) {
        my $prefix = $xpath->find('Prefix/text()', $lengths_map)->string_value;
        $isbn->{lengths_map}->{$name}->{$prefix} = [];
        foreach my $rule ($xpath->find('Rules/Rule',
          $lengths_map)->get_nodelist) {
            my $range = $xpath->find('Range/text()', $rule)->string_value;
            die unless ($range =~ m/^([[:digit:]]+)-([[:digit:]]+)$/);
            my ($min, $max) = ($1, $2);
            my $length = $xpath->find('Length/text()', $rule)->string_value;
            die unless ($length =~ m/^([[:digit:]])$/);
            push(@{$isbn->{lengths_map}->{$name}->{$prefix}}, {
                min => int($min),
                max => int($max),
                length => int($length)
            });
        }
    }
}

my $format = 'json';
my $help = 0;
GetOptions(
    'format=s' => \$format,
    'help|?' => \$help
) || die;
pod2usage(1) if $help || $#ARGV > 0;

my $isbn = {};
my $copyright = [];
parse_copyright($0, $copyright);
my $xpath = ($#ARGV < 0)
     ? XML::XPath->new(ioref => \*STDIN)
     : XML::XPath->new(filename => $ARGV[0]);
while (my ($name, $path) = each(%{$meta_name})) {
    $isbn->{meta}->{$name} = $xpath->find($path)->string_value;
}
while (my ($name, $path) = each(%{$lengths_map_name})) {
    parse_lengths_map($xpath, $name, $path, $isbn);
}
my $out;
if ($format eq 'c') {
    my $tt = new Template();
    $tt->process(\*DATA, {
	copyright => $copyright,
	isbn => $isbn
    }, \$out) || die $tt->error;
} elsif ($format eq 'json') {
    my $json = JSON->new->canonical->pretty;
    $out = $json->encode($isbn);
} else {
    die;
}
print $out, "\n";
__END__
__DATA__
[% USE format -%]
[% rfmt = format('%7d') -%]
/*-
[% FOREACH line IN copyright -%]
 * [% line %]
[% END -%]
 */

#ifndef ISBN_TAB_H_
#define ISBN_TAB_H_

/*
 * THIS FILE AUTOMATICALLY GENERATED.  DO NOT EDIT.
 *
 * generated from:
 * RangeMessage.xml(https://www.isbn-international.org/range_file_generation)
[% FOREACH name IN isbn.meta.keys.sort -%]
 * [[% name %]: [% isbn.meta.$name %]]
[% END -%]
 */

struct range {
	unsigned int min, max;
	size_t length;
};

struct lengths_map {
	const char *prefix;
	const struct range *ranges;
};

[% FOREACH name IN isbn.lengths_map.keys.sort -%]
[% lengths = isbn.lengths_map.$name -%]
[% FOREACH prefix IN lengths.keys.sort -%]
[% ranges = lengths.$prefix -%]
static const struct range [% name %]_[% prefix.replace('-', '_') %][] = {
[% FOREACH range IN ranges -%]
	{ [%- rfmt(range.min) -%], [%- rfmt(range.max) -%],  [%- range.length -%] },
[% END -%]
	{ [% rfmt(0) %], [% rfmt(0) %], -1 }
};

[% END -%]
[% END -%]
[% FOREACH name IN isbn.lengths_map.keys.sort -%]
[% lengths = isbn.lengths_map.$name -%]
static const struct lengths_map [% name %][] = {
[% FOREACH prefix IN lengths.keys.sort -%]
	{
		"[% prefix %]",
		[% name %]_[% prefix.replace('-', '_') %]
	},
[% END -%]
	{
		NULL,
		NULL
	}
};

[% END -%]
#endif /* ISBN_TAB_H_ */

@なんでいつも脱線してばかりの読み辛い文章書くの?

自分で読み返しても支離滅裂な文章で埋まってるこのチラシの裏だけど、これは若かりし頃にボブ・ディランの「追憶のハイウェイ61」というアルバムの裏ジャケにある「覚え書き」という散文を読んで感銘を受け、以来まともな文章がひとつも書けなくなったからである。 なので時代が変わればノーベル文学賞もワンチャンあると思っている。

*1:ワイもうベース何年も弾いとらんのでWarwick Streamer君はすっかり錆び切っておられるので、ジェマーソンの死んだベースの弦の音を好みずっと張り替えなかった伝説が再現できそうである。
*2:そいやXML parserもまだ新人の頃に仕事でしかもDelphiで書いたな、まだSOAP/XML-RPCなんて言葉もない時代でDelphiもXML parserなんて無いのにC/S間の通信フォーマットにXML使えって上にいわれ、それなら将軍Delphi用のXML parserをこの屏風から出してくださいといったら、Delphiのプロとやらを高給で連れてきて書かせる流れになったのだけど、出来上がったシロモノは動きませんでした。 ワイの提案したもうexpatをDelphiでラップするでいいじゃんってのもなぜかオープンソースは信用ならんとかいわれ、泣く泣くその残骸を修正といいながらスクラッチで書き直す羽目になったギョーカイ悲惨体験、週に2日だけ来てゴミ書いて単金ピー百万のなんとかさぁあああああん! なおこの経験で文字コードすこしわかるようになった、今ではさっぱりわからん。
*3:まぁCMakeで使ってるのstd::lroundくらいでこいつはlibmには存在してるのでちょっとkludge入れりゃいいだけではあるんだが重厚長大なビルドツール自体がワイ嫌いなのだ、やはりimakeはすべてを解決する。
*4:オリジナルの作者が飽きたんだか心が折れたんだかで無反応になり、しばらくしてから別のGitHub上のforkの方が本家になり、ワイの数個のプルリクは引き継がれなかったもよう。 分散型リポジトリたってプルリクや課題管理みたいなコラボレーション周りは中央集権で意味ねえよな、そりゃGitHub落ちたらみんなSNSでお茶挽きますわね。

2021/04/06(Tue)

[プログラミング厳語C] intwidest_t

kbkさんのしばらく前に復活した 雑記帳より、ワイの 先日の記事と同時発生的にUTF-8のBOMネタがフェイク技術情報発生源ことQiiiiiiiiiiiiiiitaの記事を発端にhateブや火ウイッヒ-で発生してたことを知るなど。

ワイはQiiiiiiiiiiiiiiitaもhateブも火ウイッヒ-も基本視界からシャットアウトしてるので空飛ぶスパゲッティモンスターに誓って完全に偶然なのだけども、怖いもの見たさで検索かけたらいまだにBOMは有益的な論説があってガチで進撃の復権派の「うおおおおおおおおおお!」じゃねーかと思いましたまる。

前回UTF-8Nに触れた記事は 2006年あたりまで遡るのだが、もはや復権派が何やらかしたのか覚えてねえな…

その副作用により某方面のアレな四月莫迦commitネタを知り、さぞかし寒い反応だろうなとsource-changes(某の意味がない)を何年かぶりに開いたら、滝沢先生でもないのにlint(1)の修正をしているdeveloperの方がいて、こっちの方が四月莫迦commitじゃないのかとすら思った(おい)。

以前 debugging lint(1)という記事でも書いたように、gccやclangなどのコンパイラがlint(1)より懇切丁寧に文法チェックをしてくれる時代で、LINTコマンドとコンパイラの属性情報(__attribute__とか)の両方を併記するのもめんどいわけで、OもFもとっくの昔にビルドプロセスからlint(1)による文法チェック(ただしコメントアウトだらけ)にはご退場いただいてるのになといいう感想なのだが、まぁC90から30年進歩が止まってるlint(1)がもうじき登場するであろうC2Xにも対応して

#if defined(__lint__)
...
#end

という世にもおぞましいkludgeが消えてくれるならまぁいいか(たぶんその日は来ない)。

ところでワイがかろうじて動向を追ってたC11(まだC1Xと呼ばれてた)の段階(えぇ…もう10年前なの…)で、FALLTHROUGHやNORETURNあたりに相当する機能を言語仕様に入れよう的な話があったと記憶してるのだが(ペーパー番号までは失念)、C2Xどうなってんだろうな。 ちょっと前にのぞいた時はintmax_tのABI互換性とかいう、libc開発者なら20年前には気づいてないとならない今更そこ問題にするの莫迦なのという話題で延々と無限ループしてたようだが。だいたいお前らABIに関知する場じゃねえだろと。

そんでワイはもうintN_t増やすごとにlong long long long long long long long long intとかintmaxmaxmaxmaxmaxmaxmaxmaxmaxmaxmax_tとか増やせとジョークをいってたのだが、マジモンの提案でintwidest_t導入しようぜ( N2465)とかいってて俺は限界だと思った。

そもそもintmax_tで騒ぐなら32it time_tの2038年問題対応の時に騒いでおけ、intmax_tなんてtime_tよかよっぽどインパクト小さいはずやで。

基本的にlibcのsonameを変更しろって話だし、それを回避したいならstrtoimax/strtoumaxあたりをELF symverなりNなら__RENAMEマクロで変名処理すりゃいいだけだし、あとは古いintmax_tを使うライブラリと新しいintmax_tを使うライブラリをチャンポンで使わなきゃ問題ないはずなのだが。

いやたぶんチャンポンで使おうと虫のいいこと考えてるからこの騒動なんだろうなぁ、libcのsoname変更なんてO方面みたいに毎日やってもいい文化もあるというのに。

2021/04/09(Fri)

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

(追記) 第二引数についてとんでもない勘違いしてたのと、それ以降の引数の番号がズレてたので修正しました。

数学は高二で脱落した「さんすうが苦手か…!?」な人だしCPUとFPUとGPUそしてxPUの違いもぜんぜんわからない俺は雰囲気でコードを書いてるマンなので、浮動小数数数数点点点点数数の扱いでIEEEEEEEE754の仕様を読もうとするだけで猛烈な頭痛に襲われ寝込むので、そもそもプログラマ名乗ってたこと自体間違いなのである。

なおワイのように不動小数と書いたりIEEEのEの数がいくつだったも数えられないのでいつも大目に書いてる莫迦であっても、倍賞美津子度浮動小数点数計算ハコテン操作できるライブラリが表題のgdtoaである、それロン!(脱衣シーン鑑賞中)

ライセンスがおおよそ2-clause BSDLと同等で緩いから利用に問題は無いし、それに自分でIEEE754の実装して楽しいと思える人類は世界で3人くらいなので、世のほとんどのlibc実装やLL言語もこのgdtoa使って実装されてるのだが、ソースがOpenSSLすら読みやすく思えてくる古代のインタフェースも洗練されてないので、いつか捨ててやりてえと思ってるが多分死ぬまで無理そう。

このgdtoaってのは元々dtoa.cというdoubleを文字列に変換するCソース単体で配布されてたので、まずはそいつの使い方。

#include <stddef.h>
#include <stdio.h>
#include "gdtoa.h"
int
main(void)
{
	double dvalue = 1.123456789;
	char *head, *tail;
	int exp, neg;

	head = dtoa(dvalue, 2, 8, &exp, &neg, &tail);
	if (head == NULL)
		abort();
	if (exp == 9999)
		...
	...
	freedtoa(head);
}

このdtoa()という関数を呼ぶだけで文字列に変換してくれる。

まず二番目で指定するのは変換モードですな。

        mode:
                0 ==> shortest string that yields d when read in
                        and rounded to nearest.
                1 ==> like 0, but with Steele & White stopping rule;
                        e.g. with IEEE P754 arithmetic , mode 0 gives
                        1e23 whereas mode 1 gives 9.999999999999999e22.
                2 ==> max(1,ndigits) significant digits.  This gives a
                        return value similar to that of ecvt, except
                        that trailing zeros are suppressed.
                3 ==> through ndigits past the decimal point.  This
                        gives a return value similar to that from fcvt,
                        except that trailing zeros are suppressed, and
                        ndigits can be negative.
                4,5 ==> similar to 2 and 3, respectively, but (in
                        round-nearest mode) with the tests of mode 0 to
                        possibly return a shorter string that rounds to d.
                        With IEEE arithmetic and compilation with
                        -DHonor_FLT_ROUNDS, modes 4 and 5 behave the same
                        as modes 2 and 3 when FLT_ROUNDS != 1.
                6-9 ==> Debugging modes similar to mode - 4:  don't try
                        fast floating-point estimate (if applicable).

                Values of mode other than 0-9 are treated as mode 0.

                Sufficient space is allocated to the return value
                to hold the suppressed trailing zeros.

ざっと簡単に説明すると

ということで普通に2と3の使い方だけ知ってればほぼ事足りると思われる(おい。

誤差の丸め方についてはヘッダファイルに定義があるけど

enum {  /* FPI.rounding values: same as FLT_ROUNDS */
        FPI_Round_zero = 0,
        FPI_Round_near = 1,
        FPI_Round_up = 2,
        FPI_Round_down = 3
        };

の4つ、これはIEEE754-1985で定義されてるもので

いま日本語訳として適切なもの探そうとして、普通に「マルコメ」って検索欄に打ち込んじゃって味噌汁出てきたし、出てきた用語も最近セッとか性の無限大と空目するし、ついには「丸め」が「攻め」に見えだしてもうダメだ。 なんせ∞という記号をみるだけで脳内でヨコハチ無限大と読んでしまうし三浅一深とか秘技卍ハーケンクロスとか以下略。

ちなみにIEEE754-2008で追加された

は未対応なはず、なんせ最終更新が1998年だからな…どっかに対応版とかあったら教えて。

そんで三番目の引数は有効桁数の指定、printf(3)なんかだと

$ cat >unko.c
#include <stdio.h>
int
main(void)
{
	printf("%.8f\n", 1.123456789);
}
^D

などと精度として指定するものと思えばよろし、ただしこれを実行すると

$ make unko
$ ./unko
1.12345679

と、有効桁数以降を丸めた結果が表示されるのだけど、dtoaが返す文字列はこのままでは無いことに注意(後述)。

四番目の引数は 山椒渡しポインタ渡しになってるけど、これはdtoaからの 返り血戻り値で、exponentつまり指数が入ってくる。 ワイは3本早く人間になりたい~詳しいことはこれも後述。 ちなみに9999が返された場合はdoubleの値がINF(Infinity)あるいはNAN(Not A Number)だったという意味を持ち、変換結果は

のどっちかになる。なおmath.hにあるisinf(3)は主にVAXとかVAXとかVAX方面がサポートしておらず移植性を欠くので未だに現役の某OSで困るのだが、gdtoaではどうなってるかというと

#if defined(IEEE_Arith) + defined(VAX)
#ifdef IEEE_Arith
        if ((word0(&d) & Exp_mask) == Exp_mask)
#else
        if (word0(&d)  == 0x8000)
#endif
                {
                /* Infinity or NaN */
                *decpt = 9999;
#ifdef IEEE_Arith
                if (!word1(&d) && !(word0(&d) & 0xfffff))
                        return nrv_alloc("Infinity", rve, 8);
#endif
                return nrv_alloc("NaN", rve, 3);
                }
#endif

この「#if defined(IEEE_Arith) + defined(VAX)」という謎のifdefで非常に混乱したのだがVAXとIEEE_Arithが同時にdefinedになるはずが無いのでやっぱりInfinityはサポートしてないちゅーこと。

ここではたと思い出したのだけど、昔N/i386のstrtold(gdtoaのstrtopx.c)が正しくINFを変換してくれないという記事を 書いて、まぁ俺の専門分野じゃねーからとsend-prだけしといたんだが、もしかしてgdtoaが正しくてN/i386のsrc/lib/libc/arch/i386/gen/infinityl.cの方が間違ってるような気がしてきたゾ。 とはいえ放置プレイ喰らってそのまま埋もれてるし、俺もいまさらi386なんて使ってないからどうでもいいのだけどね…

よく考えたらx86_64も同じだったわ、ローカルパッチ外したら

$ cat >unko.c
#include <math.h>
#include <stdlib.h>
#include <assert.h>

int
main(void)
{
        assert(isinf(strtold("INF", NULL)));
}
^D
$ make unko
$ ./unko
assertion "isinf(strtold("INF", NULL))" failed: file "unko.c", line 8, function "main"
Abort (core dumped)

とまぁ盛大に死んでくれよる。

オレオレN6ではさっきの記事の差分をstrtopx.cに適用してるのだけど、もしかしてNのINF定義(src/lib/libc/arch/*/gen/infinityl.c)

[i386]
const union __long_double_u __infinityl =
        { { 0, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f, 0, 0 } };

[x86_64]
const union __long_double_u __infinityl =
        { { 0, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f, 0, 0, 0, 0, 0, 0 } };

の方が間違ってるとしたらアレでアレ…あとでまじめに仕様調べて正しい修正を考えようか…

話を戻して五番目の引数、こいつも察しのとおり戻り値で、doubleの値が

が返される、よく負のオーラ滲み出てるといわれます天井のシミの顔がいつも攻撃してくるんです。

六番目の引数以下同文、dtoaの戻り値である文字列の末尾を指すポインタが返ってくる。 この仕様だけ見ると

的な事を想像してしまうのだけど、コード読む限りではちゃんとヌルヌルターミネーターされてるんだよな。

        if (s == s0) {                  /* don't return empty string */
                *s++ = '0';
                k = 0;
        }
        *s = 0;
        *decpt = k + 1;
        if (rve)
                *rve = s;
        return s0;
        }

なので使わなくてもいい、まぁケツの方から処理したい時に使えばいいんじゃないでしょうか、剃刀とか。

このdtoaの戻り値の文字列は必ずfree(3)ではなくfreedtoaで開放する必要がある、というのもgdtoa元々はdtoa_resultというグローバル変数を使い回すのでスレッドアンセーフだったのだ。

#ifndef MULTIPLE_THREADS
 char *dtoa_result;
#endif

 char *
#ifdef KR_headers
rv_alloc(i) size_t i;
#else
rv_alloc(size_t i)
#endif
{
        size_t j;
        int k, *r;

        j = sizeof(ULong);
        for(k = 0;
                sizeof(Bigint) - sizeof(ULong) - sizeof(int) + j <= i;
                j <<= 1)
                        k++;
        r = (int*)(void*)Balloc(k);
        if (r == NULL)
                return NULL;
        *r = k;
        return
#ifndef MULTIPLE_THREADS
        dtoa_result =
#endif
                (char *)(void *)(r+1);
        }

なもんで

という対応を入れよったからなのだ、うーんこの。ところでfree(3)のラッパーって流しのMCかなんかか?

よって安全側に倒すために

	size_t len;
	char *head, *tail, *tmp;
	int exp, neg;

	head = dtoa(dvalue, 2, 8, &exp, &neg, &tail);
	if (head == NULL)
		abort();
	len = tail - head;
	tmp = strdup(head);
	if (tmp == NULL)
		abort();
	freedtoa(head);
	head = tmp;
	tail = tmp + len;

とでもコピーしちまった方がいいかもね、こういうところがパラノイアっていわれるねん。

@次回予告

以前printf(3)のウン国際化対応でさんざんgdtoa呼び出してるスパゲッティーを超えた鬱コードを触ってもう二度と触りたくないと思ってたのだが、暇潰しがてらのJSON parser for Cの実装でprintf("%g")をサポートしない環境を考えたら結局またgdtoa触る羽目になってしまった。 次は戻り値の文字列がどのような形式で返されるのか、そして指数と正負をつかってどのように普段目にする%fとか%eそして%gまたまた%aの形式に変換するのかを説明する予定、いつになるかは知らん。

[プログラミング] gdtoaのstrtold(strtopx.c)のバグはすでに本家で修正済であった

さっきの記事、最新版のgdtoaみたらワイの当初の修正で正しかったようでgdtoa側に同じ修正が入ってる事が判明、というか更新があったとかしらそん。そんでNもマージ済なのでN7以降では直ってるというちゅことか。 それでもワイのsend-prから5年くらいは放置されたわけでまぁ世の中isinf(3)が動かなくてもいかに何の問題も無いかということだ、というかgdtoaの作者さんgithubで管理してくだち!県北の土手でプルリク盛りあおうぜ。

2021/04/10(Sat)

[バージョン管理] そろそろgit subtree捨てたい

せっかくオレオレN6は本家と違ってgitで管理してるので、3rd Partyでgitで管理されてるものは可能な限りgit subtree使って更新を楽にしようと計画してるのだが、現在のとこopensslとtzdataしか終わってなくてなかなか道は険しい。

そもそもN独自で当たってるローカルな修正の監査が大変なのである、opensslとか差分全部 リストアップして全部妥当かどうか検証するのさすがに辛かったし世界でユーザー1名様のみのOSにかける手間じゃねぇよな…

そしてtzdataもデータファイルとzic/zdumpコマンドのマージは終わったけど、libc/time以下にあるCソースの更新がめんどくさいくて手が止まっている。 なんせtzdataが ソース互換失ってる上にprivate.hとNのクロスビルド用のtoolchainが用意してるnbtool_config.hであっちこっち定義が衝突して宇宙ヤバいのだ。 あとstrftime/strptimeのERA対応実装をファイルサーバの未整理の中から発掘してそれもマージしねぇとならんしISO 14652 LC_TIME extensionsとかの作業以下略

それでも本家への追従は楽になって、先日リリースされたtzdataの2021aへの追従は

$ git subtree pull --prefix=external/public-domain/tz/dist --squash tz 2021a

と叩くだけでCONFLICT出ない限りサクッと作業が終わる、書いたdeveloper本人しか中身理解してないようなhoge2n**bsd.shなんてスクリプト群はゴミ箱ポイーで。

ただgit submoduleでなくgit subtreeを使ったのはちょっと早計だったなぁと後悔している、以前 git subtreeの落とし穴という記事を書いたけど、subtreeが存在する状態でgit rebaseを実行しようとすると意味不明な挙動を示すのでな…

結局過去記事にリンク張ってあるAltassianのブログにもあるように、基本subtreeの含まれるリポジトリのrebaseをするのなら

という苦行が必要になるのだよな。

まぁrebaseは禁止でCVSやSubversionみたいにコミット積み重ねてきゃいいという運用で回避でもいいんだが、gitの開発者ライナス中指ファックおじさん的にはrebaseこそがかつてのLinux kernelの管理手法であるtarballとpatchの上位互換であるわけだから、おいそれと禁止するわけにもいかない。 実際private branch切って長期間作業してると、masterの変更取り込みをrebaseでなくmergeでやると履歴グチャグチャになって何やってたかよくわからんようになるからな。

それにこの問題はrebaseだけでなくfilter-branchでも発生するのよな、よって

なんて時に、歴史を改編して葬り去りたいなんて迅速に行わなければならない作業がなかなか終わらないという地獄絵図になる。

なんでgit submoduleを選ばなかったかというと、submodule側に変更を加えないのであればorigin変えずに済むんだけど、加えた瞬間にそれはforkと同義になるのでリポジトリ用意してそっちにorigin変更せにゃならんから、とにかく管理がめんどくさいのだ。 まだ使いはじめた頃はBitBucketもリポジトリ数上限とかあってsubmodule毎にリポジトリ作ってたらオーバーしかねないのもネックだった記憶、それにcommitがsubmodule側と親リポジトリ側の2回コミットが必要になるのもダルい。

その点git subtreeだとただ一つのリポジトリ(ただしoriginは複数)と一回のコミットで履歴も--squashで圧縮できるから飛びついたんだけど、上記のような仕様なのかバグなのかよくわからん挙動でいちいち作業止まるのでなぁ。

ところでしばらく前からbaseでもcontribでもないんだけど、 git subrepoというつくれぽみたいな第三の選択肢が登場して、submoduleとsubtreeのいいとこどりなんて謳い文句なんだが、どんなもんなんだろうね。 中開いたらBashで書かれてたのでシバン含めて3行以上のシェルスクリプトは信用しないしないオレルールに抵触したので試してないけど。 というかIssueのopen数みるとまだ安定して無さげなので、見に回ってるのだが。

やはり分散型バージョン管理は SVKがオレの理想形でかなり期待してたんだけどな、OpenSolarisがほんの初期に使っててすぐMercurialに乗り換えたくらいか。 まだCPANには残ってるが開発は長期間止まってるね。

ファイルバックアップも Subversion+DeltaVで実現できると信じてた昔々のお話ですな、まだ1.0になる前の死ぬほど重いBerkeley DBバックエンド時代のSubversionとか2003年くらいから使ってたの今考えるとアホだったなーと。

2021/04/11(Sun)

[プログラミング] gdtoaの使い方(その2)

前回の続き、あくまで使い方であってgdtoa(のソースコードの)歩き方ではない…はず…

本当は先に用語の説明を書こうと思ってたのだが、文章力がボブ・ディラン並みなので後回しにしてコード例を先に置いておく。

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

まずはgdtoaのパラメーターである

  • 第二引数の変換モード
  • 第三引数の有効桁数

に何をセットするかなんだけど、モードについては *1前回も引用したコメント

                3 ==> through ndigits past the decimal point.  This
                        gives a return value similar to that from fcvt,
                        except that trailing zeros are suppressed, and
                        ndigits can be negative.

いつもの超訳だと

	3 ==>	小数点以降を有効桁数とする、これは%f変換とよく似た結果となる。
		ただし末尾の連続するゼロは取り除かれるし、有効桁数は負の値になることもある。

と書かれているので3を使うことにする。有効桁数は負の値ってのが意味不明かもだが後で説明でして?そいつはコトだ。

そんでCの仕様だと精度で明示的に指定しなかった場合は小数点以下の有効桁数は6となる

If the precision is missing, it shall be taken as 6;

よって第三引数はそれで。

#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, 3, prec, &exp, &neg, &tail);
	len = tail - head;
	...
}

@dtoaが返す文字列(head)のフォーマット、そしてexpで返される値とは

ここ読んでるすうがくやってる理系のコンピーターおじいちゃんたち(なお想定読者層はシティポップ好きゆるふわカメラ女子)は有効数字(Significant Digit)知ってると思うけど、dtoaはそっからさらに右側のゼロを削った形式を返す。 なんか他にこれ呼び名あるんだっけ、数学は理系クラス男ばっかりだから高2で投げ出したからな。 まぁ要は左や右の旦那さま(政治)とゼロは全部消しちまったぜってことです。

そんで削られた左側のゼロおよび小数点はexpから復元、そして右側のゼロはlenおよびprecから復元できるという寸法。

たとえば「0.00123456789000の有効数字」というと、左側のゼロ及び小数点を無視し「1234567890000」の12桁となるけど、gdtoaではさらに右側も無視して「123456789」の9桁になる。 さらに今回のサンプルコードでは「有効桁数は小数点以下6桁」としたので、7桁目で切上げを行うので最終的には「1235」が返ってくる文字列となる。

そんで小数点の位置から数えて文字列の先頭は

0
.
0 <= [ 0]
0 <= [-1]
1 <= [-2]
2
3
5

と表現できるけどこれがexpとして返される値となる。マイナスになるってのがさっきのコメントにあった「有効桁数が負の値」の意味だと思われる、作者はえいごがにがてか…!?

じゃあ実際に右や左のゼロと小数点を復元して普段目にする形式に変換するロジックを書いていこうか。

@A. expの値が9999となるケース

これは前回も軽く触れたけど、値がINFあるいはNANだった場合のケース。

dvalue len exp neg head
INF 8 9999 0 "Infinity"
NAN 3 9999 0 "NaN"
-INF 8 9999 1 "Infinity"
-NAN 3 9999 1 "NaN"

このケースはそのままheadを出力すればいい。

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(HAVE_MATH_H)
#include <math.h>
#endif

#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

struct dtoatestcase {
	double dvalue;
	const char *svalue;
};

#define DTOATEST(value) { .dvalue = value, .svalue = #value }
#define debug	printf

#define PRECISION	6

int
main(int argc, char *argv[])
{
	static const struct dtoatestcase t[] = {
#if defined(HAVE_MATH_H)
		DTOATEST(INF),
		DTOATEST(NAN),
		DTOATEST(-INF),
		DTOATEST(-NAN),
#else
		DTOATEST(1.0/0.0),
		DTOATEST(0.0/0.0),
		DTOATEST(-1.0/0.0),
		DTOATEST(-0.0/0.0),
#endif
	};
	size_t i;
	char *head, *tail;
	int prec, exp, neg, len;
	FILE *fp = stdout;

	for (i = 0; i < arraycount(t); ++i) {
		for (prec = 0; prec < PRECISION; ++prec) {
			debug("dvalue:[%s], prec:[%d], expected:[%.*f], result:[",
			    t[i].svalue, prec, prec, t[i].dvalue);
			head = dtoa(t[i].dvalue, 3, prec, &exp, &neg, &tail);
			if (head == NULL)
				abort();
			len = tail - head;
			if (neg) {
				if (putc('-', fp) == EOF)
					abort();
			}
			if (exp == 9999) {
				if (fwrite(head, 1, len fp) != 3)
					abort();
			}
			freedtoa(head);
			debug("]\n");
		}
	}
}

もしmath.hが存在しない環境であれば 過去記事にも書いたように

  • INF … (1.0/0.0)
  • NAN … (0.0/0.0)

で作れる(お前がVAXでも使ってない限りは)、そいやVisual C/C++だとこれ定数で書けない問題って直ったのだろうか。

なお仕様ではINFおよびNANは

The F conversion specifier produces "INF", "INFINITY", or "NAN" instead of "inf", "infinity", or "nan", respectively.

と変換しロッテ書かれてるので、upper/lower混在となるgdtoaの出力は正しくないことに注意。

あと今Nのprintf(3)が負のNANを正として出力するバグを発見したような気がするが気にしない気にしない。

@B. len == expとなるケース

判りやすいように例を挙げると以下のケース。

dvalue len exp neg head
0.0 1 1 0 "0"
1.0 1 1 0 "1"
12.0 2 2 0 "12"
123.0 3 3 0 "123"
-0.0 1 1 1 "0"
-1.0 1 1 1 "1"
-12.0 2 2 1 "12"
-123.0 3 3 1 "123"

最後のやつを例にとると、小数点の位置から数えて文字列の先頭は

-
1 <= [3]
2 <= [2]
3 <= [1]
.
0

と表現されるのでexp = 3が返ってくるわけすわ。

このケースでは

  • 小数点以下は全てゼロ(フレドリック・ブラウン著)
  • よってINF/NAN同様にそのままheadは全部出力してしまってよろしい
  • あとは小数点以下を有効桁数までゼロで埋めればいい

というコードになる。

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

#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

struct dtoatestcase {
	double dvalue;
	const char *svalue;
};

#define DTOATEST(value) { .dvalue = value, .svalue = #value }
#define debug	printf

#define PRECISION	6

int
main(int argc, char *argv[])
{
	static const struct dtoatestcase t[] = {
		DTOATEST(0.0),
		DTOATEST(1.0),
		DTOATEST(12.0),
		DTOATEST(123.0),
		DTOATEST(-0.0),
		DTOATEST(-1.0),
		DTOATEST(-12.0),
		DTOATEST(-123.0),
	};
	size_t i;
	char *head, *tail;
	int prec, exp, neg, len, zeros;
	FILE *fp = stdout;

	for (i = 0; i < arraycount(t); ++i) {
		for (prec = 0; prec <= PRECISION; ++prec) {
			debug("dvalue:[%s], prec:[%d], expected:[%.*f], result:[",
			    t[i].svalue, prec, prec, t[i].dvalue);
			head = dtoa(t[i].dvalue, 3, prec, &exp, &neg, &tail);
			if (head == NULL)
				abort();
			len = tail - head;
			if (neg) {
				if (putc('-', fp) == EOF)
					abort();
			}
			if (len == exp) {
				if (fwrite(head, 1, len, fp) != len)
					abort();
				if (prec > 0) {
					if (putc('.', fp) == EOF)
						abort();
					for (zeros = 0; zeros < prec; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
				}
			}
			freedtoa(head);
			debug("]\n");
		}
	}
}

上のコード例のとおり、IEEE 754には負のゼロ(-0.0)があるのだけど、よくある間違いとして

	double dvalue = -0;

と書いたりすると負のゼロが存在しえないintのリテラルからのキャストとなるのでお前は書いたはずの符号を失って死ぬ。

そして結果をみてもうお気づきでしょうが、123.0と-123.0でdtoaの戻り値が違うのはnegが0か1かだけであとは全く一緒。

というのもdoubleの内部表現は

  • 符号部(sign) … 1bit
  • 仮数部(fraction) … 52bit
  • 指数部(exponent) … 11bit

であるので、この1bitの違いでしかないので当然っちゃー当然、なので以降のケースでは負の値は省略させてもらう。

そりゃ正か負かなんて符号があるかないだけだもんな、もし銀行預金額がdoubleで記録されてたら記憶媒体の1bitが反転しただけで借金が貯金に反転するなんて夢あるよね…やはりECCメモリの採用は悪…化けろもっと化けろ…

まぁ金融系はふつー10進数使ってて2進数なんぞ使わんはずなので以下略、これは別に1bit化けるだけ云々という話ではなく誤差の問題でだぞ。

@C. len < expとなるケース

こいつも例を挙げると

dvalue len exp neg head
10.0 1 2 0 "1"
100.0 1 3 0 "1"
1000.0 1 4 0 "1"

最後のやつを例にとると、小数点の位置から数えて文字列の先頭は

1 <= [4]
0 <= [3]
0 <= [2]
0 <= [1]
.
0

と表現されるのでexp = 4が返る。

このケースでは

  • 小数点の後だけでなく前にも補完が必要なゼロが存在する
  • よってheadをすべて出力した後0の位まで(つまりexp - len個の)ゼロ埋めする必要がある
  • 小数点以下を有効桁数precまでゼロで埋めるのは一緒

ってのがlen == expのケースとの違いですな。

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

#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

struct dtoatestcase {
	double dvalue;
	const char *svalue;
};

#define DTOATEST(value) { .dvalue = value, .svalue = #value }
#define debug	printf

#define PRECISION	6

int
main(int argc, char *argv[])
{
	static const struct dtoatestcase t[] = {
		DTOATEST(10.0),
		DTOATEST(100.0),
		DTOATEST(1000.0),
	};
	size_t i, len;
	char *head, *tail;
	int prec, exp, neg, zeros;
	FILE *fp = stdout;

	for (i = 0; i < arraycount(t); ++i) {
		for (prec = 0; prec <= PRECISION; ++prec) {
			debug("dvalue:[%s], prec:[%d], expected:[%.*f], result:[",
			    t[i].svalue, prec, prec, t[i].dvalue);
			head = dtoa(t[i].dvalue, 3, prec, &exp, &neg, &tail);
			if (head == NULL)
				abort();
			len = tail - head;
			if (neg) {
				if (putc('-', fp) == EOF)
					abort();
			}
			if (len < exp) {
				if (fwrite(head, 1, len, fp) != len)
					abort();
				for (zeros = len; zeros < exp; ++zeros) {
					if (putc('0', fp) == EOF)
						abort();
				}
				if (prec > 0) {
					if (putc('.', fp) == EOF)
						abort();
					for (zeros = 0; zeros < prec; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
				}
			}
			freedtoa(head);
			debug("]\n");
		}
	}
}

ここで勘のいい人は気づいただろうけど

				for (zeros = len; zeros < exp; ++zeros) {
					if (putc('0', fp) == EOF)
						abort();
				}

このループはlen == expの場合には回らない、よってlen == expとlen < expのコードはほぼ同じなのだ。

だからコードを短くしたければ

			if (len == exp) {
				block B
			} else if (len < exp) {
				block C
			}

の条件分岐は

			if (len <= exp) {
				block C
			}

としておまとめてしまってよろしいと思います。

@D. len > exp かつ exp > 0のケース

はいはい、サンプルね。

dvalue len exp neg head
1.1 2 1 0 "11"
1.12 3 1 0 "112"
1.123 4 1 0 "1123"
12.1 3 2 0 "121"
12.12 4 2 0 "1212"
12.123 4 2 0 "12123"
123.1 4 3 0 "1231"
123.12 5 3 0 "12312"
123.123 6 3 0 "123123"

最後のやつを例にとると、小数点の位置から数えて文字列の先頭は

1 <= [3]
2 <= [2]
3 <= [1]
.
1
2
3

と表現されるのでexp = 3が返る、もう覚えたね?簡単だね!

このケースでは

  • headを小数点の前つまり長さexpだけ出力する
  • headの残りlen - exp分は小数点出力後に
  • precまでゼロ埋め

というロジックでおk。

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

#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

struct dtoatestcase {
	double dvalue;
	const char *svalue;
};

#define DTOATEST(value) { .dvalue = value, .svalue = #value }
#define debug	printf

#define PRECISION	6

int
main(int argc, char *argv[])
{
	static const struct dtoatestcase t[] = {
                DTOATEST(1.1),
                DTOATEST(1.12),
                DTOATEST(1.123),
                DTOATEST(12.1),
                DTOATEST(12.12),
                DTOATEST(12.123),
                DTOATEST(123.1),
                DTOATEST(123.12),
                DTOATEST(123.123),
	};
	size_t i;
	char *head, *tail;
	int prec, exp, neg, len, zeros;
	FILE *fp = stdout;

	for (i = 0; i < arraycount(t); ++i) {
		for (prec = 0; prec <= PRECISION; ++prec) {
			debug("dvalue:[%s], prec:[%d], expected:[%.*f], result:[",
			    t[i].svalue, prec, prec, t[i].dvalue);
			head = dtoa(t[i].dvalue, 3, prec, &exp, &neg, &tail);
			if (head == NULL)
				abort();
			len = tail - head;
			if (neg) {
				if (putc('-', fp) == EOF)
					abort();
			}
			if (len > exp && exp > 0) {
				if (fwrite(head, 1, exp, fp) != exp)
					abort();
				if (prec > 0) {
	 				if (putc('.', fp) == EOF)
						abort();
					len -= exp;
                        		if (fwrite(&head[exp], 1, len, fp) != len)
                                		abort();
					for (zeros = len; zeros < prec; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
				}
			}
			freedtoa(head);
			debug("]\n");
		}
	}
}

@E. len > expかつexpが0以下のケース

dvalue len exp neg head
0.1 1 0 0 "1"
0.01 1 -1 0 "1"
0.001 1 -2 0 "1"

最後のやつを例にとると、小数点の位置から数えて文字列の先頭は

0
.
0 <= [ 0]
0 <= [-1]
1 <= [-2]

と表現されるのでexp = -2が返る、もう完璧!

よってロジックは

  • 先頭のゼロと小数点を出力
  • -exp個ゼロ埋めをする
  • headを全て出力する
  • precまで残りゼロ埋め
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

struct dtoatestcase {
	double dvalue;
	const char *svalue;
};

#define DTOATEST(value) { .dvalue = value, .svalue = #value }
#define debug	printf

#define PRECISION	6

int
main(int argc, char *argv[])
{
	static const struct dtoatestcase t[] = {
                DTOATEST(0.1),
                DTOATEST(0.01),
                DTOATEST(0.002),
	};
	size_t i;
	char *head, *tail;
	int prec, exp, neg, len, zeros;
	FILE *fp = stdout;

	for (i = 0; i < arraycount(t); ++i) {
		for (prec = 0; prec <= PRECISION; ++prec) {
			debug("dvalue:[%s], prec:[%d], expected:[%.*f], result:[",
			    t[i].svalue, prec, prec, t[i].dvalue);
			head = dtoa(t[i].dvalue, 3, prec, &exp, &neg, &tail);
			if (head == NULL)
				abort();
			len = tail - head;
			if (neg) {
				if (putc('-', fp) == EOF)
					abort();
			}
			if (len > exp && exp <= 0) {
 				if (putc('0', fp) == EOF)
					abort();
				if (prec > 0) {
 					if (putc('.', fp) == EOF)
						abort();
					for (zeros = exp; zeros < 0; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
                        		if (fwrite(head, 1, len, fp) != len)
						abort();
					for (zeros = len - exp; zeros < prec; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
				}
			}
		}
		freedtoa(head);
		debug("]\n");
	}
}

@次回

はいこれでprintf(3)の%fとほぼ同等の変換がgdtoaで実現できるようになりました、まぁ他にも書式指定子による幅指定とか左寄せゼロorスペース埋めとかも考慮する必要があるのだけど、そこいらへんは割愛します。

次回は%eの変換について説明する予定、いつになるかは知らん。

ただし最もめんどくせえPOSIX localeによる国際化機能すなわちLC_NUMERICにおける

  • decimal_point … 小数点記号
  • thousands_sep … 桁区切り記号
  • grouping … 桁区切り記号の挿入位置のルール

についてもおそらくprintf(3)を自分で実装する機会でもなけりゃまず使い方とか知ることが無いだろうから、これについては機会があれば説明書きたいとは思っている。

ちなみにワイまだ幼児の頃にみた古いIBMのCMがお気に入りだったのだが、そのせいで日本人がアラビア数字と1000毎に桁区切り入れることに非常に違和感があるのだ。

なのでその結果さんすうが苦手にと思われる、おのれIBM!

*1:前回の説明でなぜか第二引数丸めモードの指定と勘違いして記事書いてたわ(訂正済)、わからないやっぱり俺は雰囲気でgdtoaを操作している。

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に変換できなければならない。

2021/04/15(Thu)

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

前回の続き、e変換のコードも整理して ここに貼っておいた。

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

今回の%gというのは

  • %eとして変換を行い、指数が-4より大きくかつ精度で指定した有効桁数以下なら
    • %fと同じddd.ddd形式で表示する
    • それ以外なら%eと同じd.ddde±dd形式で
  • 精度は%eとは異なり小数点以下の有効桁数ではなく整数部の1桁目も含む
    • なので0を指定した場合は1と解釈される
  • 精度はあくまで%eとして変換するときのもので%f形式の有効桁数は以下にも以上にもなる、よって
    • 有効桁数未満でも(#を指定しない限り)ゼロ埋めしない
    • 〃より大きくとも切り詰めたり丸めたりせず、全ての桁を表示する

という変換を行う、なんかややこしそうに感じるけど前々回と前回の内容を理解してればささっと書ける。

ちなみに一時配布元のねっとり部 *1ことnetlib.orgのページにはgdtoa作者によるg_fmt.cというソースコードがあるのでこの通りに書けばいい勘違いしてしまうのだが、実はこいつはちょっとprintf(3)の%gとは挙動が違う。

 char *
g_fmt(register char *b, double x)
{
	...
	s = s0 = dtoa(x, 0, 0, &decpt, &sign, &se);

はい、モードと有効桁数どちらもゼロ指定で変換しとりますやね。

モード0ってのはこれ。

                0 ==> shortest string that yields d when read in
                        and rounded to nearest.

超訳すると

	0 ==> doubleを読み込んで誤差を最近接丸めした値を最短の文字列にする。

ですかね。

最短の文字ってどゆこと?なんだけど、2進数で実装されてるdoubleで10進数を表現しようとすると割り切れず循環小数になる場合がある、例えば0.1は正確に0.1ではなくクッソ適当に精度30とか指定すると

$ cat >unko.c
#include <stdio.h>

int
main(int argc, char *argv[])
{
	printf("%.30f\n", 0.1);
}
^D
$ make unko
$ ./unko
0.100000000000000005551115123126

ちゅーかんじで誤差が存在ってのが判ると思う。

しかしdtoaにモード0を指定すると有効桁数に30とか指定しても

$ cat >unko.c
#include <stdio.h>
#include "gdtoa.h"
int
main(void)
{
	char *head, *tail;
	int exp, neg;

	head = dtoa(0.1, 0, 30, &exp, &neg, &tail);
	printf("head:[%s], exp:[%d\]n", head, exp);
}
^D
$ make unko
$ ./unko
head:[1], exp:[0]

このように無視される、どうやら実装読んでみると常に有効桁数18あたりで最近接丸めした結果になるっぽい(真面目に読む気は無いので以下略)。

以下のようにdoubleの値の仮数部を前後に1bitづつずらした値をモード0で変換した結果を比較すると判りやすいだろうか。

$ cat >unko.c
#include <stdio.h>
#include <string.h>
#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

union double2bytes {
	double dvalue;
	char bvalue[8];
};

int
main(void)
{
	const unsigned char *s[] = {
		"\x99\x99\x99\x99\x99\x99\xb9\x3f",
		"\x9a\x99\x99\x99\x99\x99\xb9\x3f",
		"\x9b\x99\x99\x99\x99\x99\xb9\x3f",
	};
	union double2bytes u;
	size_t i;
 	char *head, *tail;
	int exp, neg;

	for (i = 0; i < arraycount(s); ++i) {
		memcpy(u.bvalue, s[i], sizeof(u.bvalue));
		head = dtoa(u.dvalue, 0, 0, &exp, &neg, &tail);
		printf("dvalue[%g], head:[%s]\n", u.dvalue, head);
	}
}
^D
$ make unko
$ ./unko
dvalue[0.1], head:[9999999999999999]
dvalue[0.1], head:[1]
dvalue[0.1], head:[10000000000000002]

まず%gの場合デフォルトの有効桁数6で切上げ発生するのでどれも0.1として表示されてるんだけど、モード0だとどれも違う値として変換されてる事がお判りいただけるかと *2

よって今回の%gの実装にこのモード0は使わず

  1. %eとおなじモード2を使う
  2. precが0の場合は1として扱う
  3. %eの場合の精度は小数点以下、%gの場合は整数部の1桁目も含むのでprecは-1ずれる
  4. 有効桁数までのゼロ埋めは不要(#まで実装するなら要るけど)

ちゅーかんじで実装するってわけよ。

コードは長くなるけど前々回の%f、前回の%eのコードを理解してればprecの調整とゼロ埋め以外は同じコードだと判るかと思うので、一気に全部貼る。

#include <assert.h>
#include <float.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(HAVE_MATH_H)
#include <math.h>
#endif

#include "gdtoa.h"

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

struct dtoatestcase {
	double dvalue;
	const char *svalue;
};

#define DTOATEST(value) { .dvalue = value, .svalue = #value }
#define debug	printf

#define PRECISION	6
#define MAXEXPSIZ	4

int
main(int argc, char *argv[])
{
	static const struct dtoatestcase t[] = {
#if defined(HAVE_MATH_H)
		DTOATEST(INF),
		DTOATEST(NAN),
#else
		DTOATEST(1.0/0.0),
		DTOATEST(0.0/0.0),
#endif
		DTOATEST(0.0),
		DTOATEST(1.0),
		DTOATEST(12.0),
		DTOATEST(123.0),
		DTOATEST(10.0),
		DTOATEST(100.0),
		DTOATEST(1000.0),
                DTOATEST(1.1),
                DTOATEST(1.12),
                DTOATEST(1.123),
                DTOATEST(12.1),
                DTOATEST(12.12),
                DTOATEST(12.123),
                DTOATEST(123.1),
                DTOATEST(123.12),
                DTOATEST(123.123),
                DTOATEST(0.1),
                DTOATEST(0.01),
                DTOATEST(0.002),
		DTOATEST(1.123456789),
		DTOATEST(11.23456789),
		DTOATEST(112.3456789),
		DTOATEST(1123.456789),
		DTOATEST(11234.56789),
		DTOATEST(112345.6789),
		DTOATEST(1123456.789),
		DTOATEST(11234567.89),
		DTOATEST(112345678.9),
		DTOATEST(1123456789.0),
		DTOATEST(11234567890123.0),
	};
	size_t i;
	char *head, *tail, expstr[MAXEXPSIZ + 2];
	int prec, gprec, exp, neg, len, zeros, expsig;
	FILE *fp = stdout;

	for (i = 0; i < arraycount(t); ++i) {
		for (prec = 0; prec <= PRECISION; ++prec) {
			debug("dvalue:[%1$s], gprec:[%2$d], expected:[%3$.*2$g], result:[",
			    t[i].svalue, prec, t[i].dvalue);
			gprec = prec;
			if (gprec == 0)
				gprec = 1;
			head = dtoa(t[i].dvalue, 2, gprec, &exp, &neg, &tail);
			if (head == NULL)
				abort();
			len = tail - head;
			if (neg) {
				if (putc('-', fp) == EOF)
					abort();
			}
			if (exp == 9999) {
				if (fwrite(head, 1, len, fp) != len)
					abort();
			} else if (exp > -4 && exp <= gprec) {
				if (len <= exp) {
					if (fwrite(head, 1, len, fp) != len)
						abort();
					for (zeros = len; zeros < exp; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
				} else if (exp > 0) {
					if (fwrite(head, 1, exp, fp) != exp)
						abort();
	 				if (putc('.', fp) == EOF)
						abort();
					len -= exp;
                        		if (fwrite(&head[exp], 1, len, fp) != len)
                                		abort();
				} else {
 					if (fwrite("0.1", 1, 2, fp) != 2)
						abort();
					for (zeros = exp; zeros < 0; ++zeros) {
						if (putc('0', fp) == EOF)
							abort();
					}
                        		if (fwrite(head, 1, len, fp) != len)
						abort();
				}
			} else {
				if (putc(*head, fp) == EOF)
					abort();
				if (--len > 0) {
					if (putc('.', fp) == EOF)
						abort();
					if (write(&head[1], 1, len, fp) != len)
						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);
			debug("]\n");
		}
	}
}

もちろんコードを整理すれば%fや%gのコードと共有できるけどそれは各人で勝手にやってどうぞ。

@次回予告

露骨に手抜きになってきたけど、次回はいつになるか知らないけどhdtoaで%aを実装する予定。

*1:元天才塾にありそうな部活である。
*2:doubleの値を直接つくるには共用体を使うとよい、Nの場合にはmachine/ieee.hにieee_double_uそしてglibcでもmath_private.hにieee_double_shape_typeなんてのがご用意されてるはず。 なお今回はめんどくせえのでchar[8]との共用体で書いたのでENDIAN無視したコードなのでそこだけ注意な。

2021/04/16(Fri)

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

前回の続き、g変換をこれまで書いたe/f変換を使うようコードを整理して ここに貼っておいた。

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

これまでe/f/g変換は10進表記だったのだけれど、今回できるかなのa変換は16進表記でありP表記(P notation)に変換する。

んでP表記(P-notation)ってのはこんなん。

(?<符号>[+\-])?(?<仮数部>0[xX](?<整数部>[[:xdigit:]])((?<小数点>\.)(?<小数部>[[:xdigit:]]+))?)(?<p表記>[pP])(?<指数部>(?<指数符号>[+\-])(?<指数>[[:digit:]]+))

はい、E表記と比べると仮数部が0xh.hhhhで指数部はp±d形式になっとります。 そして指数はE表記では2桁以上だったのが1桁以上でゼロ埋め不要になっとります。

そんでP表記でprintf(3)なら"%p"だろ!となりそうなもんだけど、残念ながらとっくの昔に"%p"書式指定子はポインタを表示するものとして先約済なので、かわりに"%a"書式指定子を使うとC99で定められたのですわ。 なぜ数ある中から"%a"が選ばれたのか残念ながらワイ経緯知らんしそれっぽい理由も思いつかないし調べる気力も無いので以下略

前回例にとりあげた0.1(これ自体循環小数だけど)から1bit前後にずらした値は10進だと精度によってはどれも0.1に丸められてしまったけれど

$ cat >unko.c
#include <stdio.h>
#include <string.h>

#define arraycount(array)       (sizeof(array)/sizeof(array[0]))

union double2bytes {
	double dvalue;
	char bvalue[8];
};

int
main(void)
{
	const unsigned char *s[] = {
		"\x99\x99\x99\x99\x99\x99\xb9\x3f",
		"\x9a\x99\x99\x99\x99\x99\xb9\x3f",
		"\x9b\x99\x99\x99\x99\x99\xb9\x3f",
	};
	union double2bytes u;
	size_t i;

	for (i = 0; i < arraycount(s); ++i) {
		memcpy(u.bvalue, s[i], sizeof(u.bvalue));
		printf("dvalue[%g]\n", u.dvalue);
	}
}
^D
$ make unko
$ ./unko
dvalue[0x1.9999999999999p-4]
dvalue[0x1.999999999999ap-4]
dvalue[0x1.999999999999bp-4]

はい、16は2のべき乗なので正確に表現できとりますね、そこの36bitワードマシンとか二進化十進数に一言お持ちのお方、ご静粛にご着席ください。

ちなみにこの仮数部の1bitの差(つまり1と1より仮数部1bitだけ大きい数字の差)をマシンイプシロン(青い装甲騎兵ではない)あるいは計算機イプシロンと呼ぶ(doubleだとほとんどの実装において「0x1p-52」となる)んだけどその差を誤差無く表現できるってわけよ。

どうでもいいけどマシンイプシロンについてはCの規格でfloat.hにDBL_EPSILONという定数を用意しろとある、Nだとfloat.hからインクルードされてるsys/float_ieee754.hで

#if __STDC_VERSION__ >= 199901L
#define DBL_EPSILON     0x1.0p-52
...
#else
#define DBL_EPSILON     2.2204460492503131E-16
...
#endif

となっとりますな、なお環境によってはコンパイラの事前定義マクロ__DBL_EPSILON__を使うのでそっちは以下で確認して。

$ echo __DBL_EPSILON__ | gcc -E -
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "<stdin>"
((double)2.22044604925031308084726333618164062e-16L)

@まさかの時の現代貨幣理論

経済学部で音楽専攻してたワイはギターは絶対にインチネジ派なので度量法をメートルで統一とか絶対に許さないよ…なのだが、貨幣はもう16進にして

  • 0x186a0p+0円記念硬貨
  • 0x2710p+0円札
  • 0x1388p+0円札
  • 0x7d0p+0円札
  • 0x3e8p+0円札
  • 0x1f4p+0円硬貨
  • 0x64p+0円硬貨
  • 0x32p+0円硬貨
  • 0xap+0円硬貨

を発行すればなにかと開発力の低さが社会信用問題になるジャパニーズ金融機関もシステムトラブル発生しなくなるというご提案、いかがですかFinTechとかいう横文字ギョーカイや財務省官僚の方々。

これが実現すれば、右や左の旦那さま恵まれぬ私にどうか0x1.388p+13円お恵みを~と物乞ひすれば、1.4円くらいの小銭と勘違いして銀行口座に気軽に振り込んでくれるだろうしwin-winでなのである。

@hdtoaに渡すパラメーター

話を元に戻して"%a"の実装なんだけど、doubleの内部表現を共用体使って引き摺り出せるならP表記への変換なんて自前で簡単に実装できる気がしてくるけど、精度指定した場合有効桁数未満での丸めはやっぱり必要になるのでgdtoaにお任せした方がいい。

そこでご用意されているのがdtoaの16進バージョンであるhdtoa、今回もこいつに頼ることにする。

char *
hdtoa(double d, const char *xdigs, int ndigits, int *decpt, int *sign,
	    char **rve)
{

dtoaと比べてだいたい引数は一緒なんだけど、差異としては

  • dtoaのような変換モード指定はない(そもそも要らない)
  • dtoaと違って変換結果を大文字と小文字のどちらにするかの指定は必要になるけど、第二引数に以下のいづれかをを渡すことで実現
    • "0123456789ABCDEF"
    • "0123456789abcdef"

ちゅーあたりか。

ここで一点注意、さっきの俺の口座に0x1.388p+13円を振り込むには0x2710p+0円札プラス手数料が必要と気づいた人はもうお判りだろうけど、E表記と同じくP表記は同じ値であっても複数の表現を持つのだ。

0x1.3p3
0x2.6p2
0x4.cp1

これ仕様では

there is one hexadecimal digit (which shall be non-zero if the argument is a normalized floating-point number and is otherwise unspecified) before the decimal-point character

と小数点の前は一桁だよだけしか決まってないので、どの表現になるか決まらないのだ(%eの場合はこれだけで決定するんだが)。よってどの表現がでてきても正しい実装依存ガチャなのだ。

このことはhdtoaのソースにも書いてあって、例もそこから抜粋したのだが

 * Note that the C99 standard does not specify what the leading digit
 * should be for non-zero numbers.  For instance, 0x1.3p3 is the same
 * as 0x2.6p2 is the same as 0x4.cp3.  This implementation chooses the
 * first digit so that subsequent digits are aligned on nibble
 * boundaries (before rounding).

はい0x4.cp「1」を0x4.cp「3」とtypoしとりますな、やはり県北の土手でプルリク盛りあおうや…

よって%aを実装しても正しく変換できたかのテストケースを書こうしても移植性考えるとちょっとめんどくさい、結果をstrtod(3)で再度doubleに戻した後はコンパイラが対応してりゃP表記リテラルで書いた期待値と比較すればいいけど、未対応だと期待値をバイナリで書かないとならん。まぁ今時さすがに無いとは思いたい。

@hdtoaが返す文字列(head)のフォーマット、そしてexpで返される値とは

とりあえず適当な値入れて呼んでみる。

$ cat >unko.c
#include <stdio.h>
#include <stdlib.h>
#include "gdtoa.h"

int
main(void)
{
	static const char *xdigit = "0123456789abcdef";
	char *head, *tail;
	int exp, neg;

	head = hdtoa(0x1p-52, xdigit, 0, &exp, &neg, &tail);
	if (head == NULL)
		abort();
	printf("head:[%s], exp:[%d], neg:[%d]\n", head, exp, neg);
}
^D
$ make unko
$ ./unko
head:[1], exp:[-51], neg:[0]

はい指数が-51で帰ってきてるのでhdtoaが変換した結果は「0x0.1p-51」になるということやね。

では精度を指定すると?

$ cat >unko.c
#include <stdio.h>
#include <stdlib.h>
#include "gdtoa.h"

int
main(void)
{
	static const char *xdigit = "0123456789abcdef";
	char *head, *tail;
	int exp, neg;

	head = hdtoa(0x1p-52, xdigit, 6, &exp, &neg, &tail);
	if (head == NULL)
		abort();
	printf("head:[%s], exp:[%d], neg:[%d]\n", head, exp, neg);
}
^D
$ make unko
$ ./unko
head:[100000], exp:[-51], neg:[0]

ほうdtoaの時とは違ってhdtoaは右側のゼロ出力するやんけ、「0x0.100000p-51」ってことやな。

(追記) ただし0.0を変換した場合は「000000」にはならず「0」になるのでその時だけは自分でゼロ埋め必要、めんどくさ。

ということでCの仕様である

  • 小数点の前に1桁必要
  • デフォルトの精度は6ではなく変換された全ての桁

よって「0x1p-52」あるい「0x1.000000p-52」と表示させるにはprecとexpの値の調整が必要になるわけだ。e変換とおんなじだな。

@いつものクッソ適当な実装例

つーことで前々回のコードをちょいちょい弄ってこうなる。

#include <assert.h>
#include <limits.h>
#include <float.h>
#include <stdio.h>
#include <stdlib.h>

#define PRECISION       0
#define MAXEXPSIZ       3

static const char *xdigit = "0123456789abcdef";

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;

	if (prec >= 0)
		++prec;
	head = hdtoa(dvalue, xdigit, prec, &exp, &neg, &tail);
	len = tail - head;
	if (neg && putc('-', fp) == EOF)
		abort();
	if (exp == INT_MAX) {
		if (fwrite(head, 1, len, fp) != len)
			abort();
	} else {
		if (fwrite("0x", 1, 2, fp) != 2)
			abort();
		if (putc(*head, fp) == EOF)
			abort();
		if (--len > 0) {
			if (putc('.', fp) == EOF)
				abort();
			if (fwrite(&head[1], 1, len, fp) != len)
				abort();
		}
		if (--exp >= 0) {
			assert(exp <= DBL_MAX_10_EXP);
			expsig = '+';
                } else {
			assert(exp >= DBL_MIN_10_EXP);
			expsig = '-';
			exp = -exp;
		}
		tail = &expstr[sizeof(expstr)];
		while (exp > 9) {
			*--tail = '0' + (exp % 10);
			exp /= 10;
		}
		*--tail = '0' + exp;
		*--tail = expsig;
		*--tail = 'p';
		len = &expstr[sizeof(expstr)] - tail;
		if (fwrite(tail, 1, len, fp) != len)
			abort();
	}
	freedtoa(head);
	exit(EXIT_SUCCESS);
}

もう勘所はわかってるのでちょっと考えれば書けますやね。

一点だけ注意、MAXEXPSIZという謎の定数はe変換の時は4でその理由として

DBL_MIN_EXP	-1021
DBL_MAX_EXP	+1024

を格納できうるサイズという説明をした。

今回a変換の場合は3になっとるのだけど、これは同様にC99の5.2.4.1にてIEEE 754を満たしてるなら指数の上限加減が-307~308が最小値となるとあるからの数字。

DBL_MIN_10_EXP	-307
DBL_MAX_10_EXP	308

まぁ念のため実際の値を自分の使ってる環境値を確認しといてください。

(追記) INF/NANの場合dtoaだと9999がexpに返るけど、hdtoaの場合はINT_MAXになるのを忘れててそのままの実装をお出ししてしまったので修正したよ。

@次回予告

とりあえずこれで やる気のない駆け足だけれどもNなんかのprintf(3)のコードを読む際にわけわかんねーとなりがちな浮動小数点数とgdtoaの関係についての解説はおしまい。 なーんだprintf(3)の実装なんて簡単そうと思っていただけたら幸いであるが、お前の書く文章は読みづらいということであればここは俺のチラシの裏なので苦情は/dev/nullへ。

なお次回からはLC_NUMERICによる国際化という地獄につきあってもらう、あと機会があれば"%1$f"みたいなpositional orderもだな…

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の値について説明してあるから理解してれば判るとは思うけど、ざっくりまとめると

ってとこですかね、ああこれ典型的なソースのコメントに「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)が指数部の計算、こちらもざっとまとめると

最後に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変換とほとんど一緒だけど

ちゅーとこに注意。

んで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とかもっとアレ。

2021/04/19(Mon)

[プログラミング] printf(3)の桁区切りはどのように実装されているのか(その1)

今回からは「'」書式指定子による桁区切りの実装について解説するのだけど、前回までのf変換のコードを元に説明するのはクソめんどくさいので、まずシンプルなd変換を実装しそいつで説明していくことにする。

このくらい初心者でもササッと書けるでしょさすがにハハッ

#define _GNU_SOURCE
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define arraycount(array)	(sizeof(array)/sizeof(array[0]))

static inline int
to_char(int digit)
{
	return '0' + digit;
}

int
cvt_dfmt(int value, FILE *fp)
{
	/* assume INT_MIN(-2147483648) - INT_MAX(2147483647) */
	char buf[11], *s;
	unsigned int d;
	int neg;
	size_t len;

	d = (unsigned int)value;
	if ((neg = value < 0) != 0)
		d = -d;
	s = &buf[sizeof(buf)];
	do {
		assert(s > &buf[0]);
		*--s = to_char(d % 10);
		d /= 10;
	} while (d > 0);
	if (neg) {
		assert(s > &buf[0]);
		*--s = '-';
	}
	len = &buf[sizeof(buf)] - s;
	if (fwrite(s, 1, len, fp) != len)
		return 1;
	return 0;
}

int
main(void)
{
	int itest[] = {
		INT_MIN,
		-1000,
		-100,
		-10,
		-1,
		0,
		1,
		10,
		100,
		1000,
		INT_MAX
	};
	size_t i, n;
	FILE *fp;
	char *expect, *result;

	for (i = 0; i < arraycount(itest); ++i) {
		expect = NULL;
		if (asprintf(&expect, "%d", itest[i]) < 0)
			abort();
		result = NULL;
		fp = open_memstream(&result, &n);
		if (fp == NULL)
			abort();
		if (cvt_dfmt(itest[i], fp))
			abort();
		fclose(fp);
		printf("expect:[%s], result[%s]\n",
		    expect, result);
		if (strcmp(result, expect))
			abort();
		free(expect);
		free(result);
	}
	exit(EXIT_SUCCESS);
}

難しいところは何一つ無いと思う *1、符号外す時はunsigned型使うってのと、変換バッファのサイズ(-いれて11桁)をINT_MIN/INT_MAXの値を仮定した決め打ちしてる部分があるくらいか。ちなみにNのvfwprintf.cなんかだとくっそ適当に

/*
 * The size of the buffer we use as scratch space for integer
 * conversions, among other things.  Technically, we would need the
 * most space for base 10 conversions with thousands' grouping
 * characters between each pair of digits.  100 bytes is a
 * conservative overestimate even for a 128-bit uintmax_t.
 */
#define BUF     100

        CHAR_T buf[BUF];        /* buffer with space for digits of uintmax_t */

とか100あればint128_t時代がきても大丈夫でしょとかやっとるし、気にしたら負けである。

別解、あまり当チラシの裏は競技プログラミングガチ勢の方々を読者として想定していないので興奮せず着席して黙ってて頂きたいのだが、符号除いてせいぜい10桁ということで

	if (d < 10) {
		buf[0] = to_char(d);
		len = 1;
	} else if (d < 100) {
		buf[0] = to_char(d / 10);
		buf[1] = to_char(d % 10);
		len = 2;
…
	} else {
		buf[0] = to_char( d / 1000000000);
		buf[1] = to_char((d % 1000000000) / 100000000);
		buf[2] = to_char((d % 100000000) / 10000000);
		buf[3] = to_char((d % 10000000) / 1000000);
		buf[4] = to_char((d % 1000000) / 100000);
		buf[5] = to_char((d % 100000) / 10000);
		buf[6] = to_char((d % 10000) / 1000);
		buf[7] = to_char((d % 1000) / 100);
		buf[8] = to_char((d % 100) / 10);
		buf[9] = to_char( d % 10);
		len = 10;
	}

とループ展開してドヤ顔したくなる時もある、しかしお前は桁区切りの実装めんどくさくなる呪いがかかったわけだがそんなコードで大丈夫か?

そんじゃループ展開しない方のバージョンで3ケタ区切りを実装してみましょかね。

--- dcvt.c.orig	2021-04-18 16:20:00.000000000 +0900
+++ dcvt.c	2021-04-18 16:20:53.000000000 +0900
@@ -10,20 +10,27 @@
 int
 cvt_dfmt(int value, FILE *fp)
 {
-	/* assume INT_MIN(-2147483648) - INT_MAX(2147483647) */
-	char buf[11], *s;
+	/* assume INT_MIN(-2,147,483,648) - INT_MAX(2,147,483,647) */
+	char buf[14], *s;
 	unsigned int d;
-	int neg;
+	int neg, grouping;
 	size_t len;
 
 	d = (unsigned int)value;
 	if ((neg = value < 0) != 0)
 		d = -d;
 	s = &buf[sizeof(buf)];
+	grouping = 3;
 	do {
+		if (grouping == 0) {
+			assert(s > &buf[0]);
+			*--s = ',';
+			grouping = 3;
+		}
 		assert(s > &buf[0]);
 		*--s = to_char(d % 10);
 		d /= 10;
+		--grouping;
 	} while (d > 0);
 	if (neg) {
 		assert(s > &buf[0]);
@@ -68,8 +75,10 @@ main(void)
 		fclose(fp);
 		printf("expect:[%s], result[%s]\n",
 		    expect, result);
+#if 0
 		if (strcmp(result, expect))
 			abort();
+#endif
 		free(expect);
 		free(result);
 	}

こっちも難しくないよね、カウンタgroupingを3にセットして0になる度リセットとカンマを出力すりゃいいだけの話だ。

expect:[-2147483648], result[-2,147,483,648]
expect:[-1000], result[-1,000]
expect:[-100], result[-100]
expect:[-10], result[-10]
expect:[-1], result[-1]
expect:[0], result[0]
expect:[1], result[1]
expect:[10], result[10]
expect:[100], result[100]
expect:[1000], result[1,000]
expect:[2147483647], result[2,147,483,647]

それではこれで閉会!終わり!ジ・エンド!

…とはいかないんですわ、そもそも3桁ごとにカンマで区切るとかどこの田舎者の世界の話だよってな!

@そもそもどこで桁を区切るのか

有名なところではインド の山奥でっの命数法においては

日本語転写 アルファベット代用表記 デーヴァナーガリー ナスタアリーク 指数表記
ハザール hazār हज़ार ہزار 1e+03
ラーク lākh लाख لاکھ 1e+05
コルール karōṛ करोड़ کروڑ 1e+07
アラブ arab अरब ارب 1e+09
カラブ kharab खरब کھرب 1e+11
ニール nīl नील نیل 1e+13
パドマ padma पद्म پدم 1e+15
シャンク śaṅkh शंख شنکھ 1e+17

とハザール(1000)までは千進法の3桁区切りだけど、ラーク(1,00,000)以降は百進法の2桁区切りになるんだよね *2、うん国際化プログラミングにおいては桁区切りが固定位置であるという思い込みをまず捨てようか。

10,000,000(ten million)
1,00,00,000(ēk karōṛ)

ちなみにペルシャ語におけるコルールは十進ですらなく50万であるのだが、これは英米でもハーフミリオン *3とかあるし基数が10でないものは無視してよろしい。

そもそも論として日本も「一十百千」そして「万億兆」となる万進法であり4桁区切り(指数4)の命数法なのよね、アラビア数字の桁区切りは英国式に(one ten handled + million billion trillion quadrillionの)3桁(指数3の倍数)毎にやるルールだけどね。

そしてだな、以前の回で「塵劫記」に登場する日本の命数法を紹介したのを思い出してほしい。

命数 指数表記
1e+04
1e+08
1e+12
1e+16
1e+20
𥝱(秭) 1e+24
1e+28
1e+32
1e+36
1e+40
1e+44
1e+48
恒河沙 1e+56
阿僧祇 1e+64
那由他 1e+72
不可思議 1e+80
無量大数 1e+88

はい、正確には4桁区切りどころか

  • 極までは指数4(万進)
  • それ以降は指数8(万万進)

というインドと同じくめっちゃ変則な桁区切りルールによる命数法なのですわ。

なお江戸時代マンすら不便だと思ったのか、極以降を指数8とするのはごく初期の版(寛永8年版)だけで、のちに指数4に改められた(寛永11年版)ので1無量大数 = 1e+68としとるネットのどうでしたか記事の方が圧倒的に多い *4けどね。

なお塵劫記よりもはるかに巨大な数を扱う命数に華厳経(八十華厳)の「不可説不可説転」がある、1e+1037218383881977644441306597687849648128という指数からして桁あふれしとる巨大数で「悟りに必要な功徳の数」を示すもの。

まぁ今こうやってクソ記事書いてるワイ弥勒菩薩が悟りを開くという56億7千万年後のコンピューターならムーアの法則で負ける気せえへん地元やし *5およそ6億回目の改訂を経たISO-CやIEEE754ではintmax_tやbinary1145148101919…で軽々と扱えてるはずなので、その時は八十華厳のため7桁区切りを実装しないとならんのだ。

@桁区切り文字や小数点のうん国際化

これも日本では桁区切り文字は「,(COMMA U+002C)」が一般的だけど、これは3桁区切りと同じで明治期のお雇いエゲレス人からの借り物でしかない。漢数字だったら中黒つまり「・(U+30FB KATAKANA MIDDLE DOT)」を使うし、アラビア数字であっても縦書きなら中黒とJISも定めておる。

そもそも西欧ですら統一されておらず

桁区切り 小数点 備考
イギリス , (U+002C COMMA) . (U+002E FULL STOP) メシマズ
フランス . (U+002E FULL STOP) , (U+002C COMMA) カエル喰い

と泳いで渡れるドーバー海峡挟んだだけで逆になっとるし、大陸に渡ったメリケン人だと小数点とはすなわち分数の表記法であるとして

3141591145148101919

のように表記したりもしますやね。

@次回予告

次回はこれら国によってまちまちな桁区切りと小数点について、POSIX localeがどのような機能を提供するのかを説明する。

*1:むしろテストコードで使ってるasprintf(3)やopen_memstream(3)って何って言われそうであるが、glibc拡張の前者はともかく後者がPOSIX:2008に入ってもう干支一回りしてるので不勉強悔いてどうぞ。
*2:なお表ではインドの連邦公用語であるヒンディー語かつ代表的な3つの表記法を挙げてあるけど、他にもマラーティー語、ベンガル語、オルドゥ語、タミル語、テルグ語、カンナダ語、マラヤーラム語、ネパール語など、インド憲法で定める22の公用語毎に異なる表記が存在することに注意。
*3:なおカブトボーグの世界ではミリオンは百万でなく1万である、スッゴイカワイソ。
*4:更に後の版では無量大数を 無量 = 1e+68 大数 = 1e+72 としているものもある。
*5:トレイ・ムーア(阪神)が出場した日本シリーズは2003年であって、当発言のあった 2005年の日本シリーズ(33-4)は無関係である、いいね?

2021/04/20(Tue)

[プログラミング] printf(3)の桁区切り(位取り)はどのように実装されているのか(その2)

過去回で「%eは指数を最低2桁表示する」って話したけど、なんでかなーと思ったけどこれ電卓の指数表示が2桁だった時代の名残ちゅうことか、おのれヒューレットとパッカード! *1

はい今回は予告通りprintf(3)から少し離れてPOSIX localeとは何ぞやを説明するよ。

@チンPOSIX localeとはなんぞや

なんか収まりが悪いと直したくなりますよね定位置を *2、右か左かなんてチンポジの話だけにして欲しいよね…

まぁそれはおいといて(ゴソゴソ)、POSIX localeってのはボブ・ディラン級の言語能力を駆使して簡潔にまとめると

  • ソフトウェアの国際化(インターナショナライゼーション略してi18n)のためのフレームワーク
  • ひとたびPOSIX localeで定めるルールに従ってプログラムを書けば、とある地域・言語向けに地域化(ローカライゼーション略してl10n)するために
    • プログラムをいちいち修正して再コンパイルすることなく
    • 地域・言語ごとに別のプログラムとすることなく(シングルバイナリという)
    • 環境変数LANGあるいはLC_*で地域・言語を指定するだけでローカライズ済のアプリケーションが起動する

という魔法のこと、もちろん魔法というのは人類に扱いきれないのが世の常なのでJavaのWrite Once, Run Anywareくらい信用ならん自滅系の詠唱かもしれない。

なお通例POSIX localeと呼ばれるのでUnix方面だけのものと思われがちだけど、これはれっきとしたISO-Cの機能でありC90の頃から基本機能は存在し、C95(C90AMD1)/C99で大幅に機能が強化され実用になり、POSIX:2008及びC11でまるで冗談のような筋の悪い拡張がされてエンガチョと化した、だいたい元RedhatのUrlich Drepperのせいだし一番悪いのはC++がstd::localeでろくでもねえ拡張入れたらその尻ぬぐいなんだけど。

@まずは基本的な使い方

このPOSIX localeというものを使うには、<locale.h>に存在するsetlocale(3)を呼び出すことからはじめる。

#include <locale.h>
int
main(int argc, char *argv[])
{
	setlocale(LC_ALL, "");
}

引数を2つとるのだが、第一引数はLC_ALL、第二引数は""でいい余計な事は何も考えるな、戻り値のエラーチェックもせんでいい。

より高度なフレームワークをお使いであればそいつ自身が独自の国際化機能を持ち、内部でsetlocale(3)を勝手に呼んどいてくれるケースもあるのでその場合は明示的に呼ばんでもいい。 例えばX ToolkitのXtSetLanguageProc()とかがそれ。

int
main(int argc, char *argv[])
{
	Widget toplevel;

	XtSetLanguageProc(NULL, (XtLanguageProc)NULL, NULL);

	toplevel = XtOpenApplication(...);
}

他にも *3GTK+とかgtk_init()が内部的に呼んでたよなと記憶してたが、いつの間にやら明示的にsetlocale(3)呼べに変わってたので以下略。

このsetlocale(3)の第一引数で指定するLC_*(Locale Categoryの略だな)には

  • LC_CTYPE … マルチバイト/ワイド文字変換などに関するAPI(mbrtowc/wcrtombなど)
  • LC_COLLATE … 文字照合順序に関するAPI(strcoll/strxfrmなど)
  • LC_MESSAGES … yes/noなどの応答メッセージに関するデータ
  • LC_MONETARY … 通貨記号および金額表示方法に関するデータとAPI(strfmon)
  • LC_NUMERIC … 一般的な数値の表示方法に関するデータとAPI(printfなど)
  • LC_TIME … 年月日および時刻の表示などに関するデータとAPI(strftime/strptimeなど)

の6つ *4があり、LC_ALLはこれらすべてを同時に同じ値にセットするという意味。99%のケースではLC_ALLを指定しておけばOKだ。

そして第二引数、ここにはlocale -aで表示される地域・言語名を指定するのだけど、""を指定しておけば環境変数LANGおよびLC_*から値を検索するようになる。 明示的に指定しちゃうとその地域・言語固定になってしまうので意味がないからな。

なお環境変数で指定した地域・言語のサポートが無い場合、setlocale(3)はNULLを返すがこれは無視していい。どうせC localeに勝手にfallbackするので。

じゃちょっとコード書いてみようか、setlocale(3)呼ぶだけでlibcに含まれる様々な関数は指定した地域・言語に対応した振る舞いをするようになる、例えばLC_TIMEのstrftime(3)なんかが判りやすいのでサンプルコード。

$ cat >unko.c
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int
main(void)
{
	char buf[BUFSIZ];
	time_t now;
	struct tm *t;

	setlocale(LC_ALL, "");
	now = time(NULL);
	t = localtime(&now);
	strftime(&buf[0], sizeof(buf), "%c", t);
	puts(buf);
}
^D
$ make unko
$ for i in C ja_JP zh_CN ko_KR; do LANG=$i.UTF-8 ./unko; done
Tue Apr 20 00:18:11 2021
2021年4月20日  0時18分11秒
2021年4月20日  0时18分11秒
2021년4월20일  0시18분11초

環境変数変えるだけで再コンパイルの必要も無く複数の地域・言語による日付表示ができてますやね、これがシングルバイナリでの国際化、POSIX localeの魔法よ。 表示がされないのであれば

  • 地域・言語データが未インストールであるか
  • お前の使ってるOSの実装がへちょい

が原因と思われるので、パッケージ導入するか窓から投げ捨ててどうぞ。

@地域・言語データとはなんぞや

さっきの魔法だけどもなにも無から日本語/簡体中文/ハングルが捻り出された本物の魔法ではなく、ちゃんと種も仕掛けも存在する。 たとえばNでは/usr/share/locale以下、Linuxなら/usr/lib/locale以下に地域・言語そしてカテゴリ毎にデータベースが置かれ、libcはsetlocale(3)が呼ばれるとこのデータベースを読込んでlibc内部の定数などを上書きするのだ。

このデータベースをどう提供するかはC言語は定める立場に無いので実装依存なんだけど、SUS(Single UNIX Specification)ではUNIX(TM)を名乗るならlocaledef(1)というコマンドに定義ファイルを喰わせて生成することが求められている。 まぁNはlocaledef(1)ではなくもっとショボいmklocale(1)というコマンドを使ってるんだけどね。

このlocaledef(1)の仕様についてはドイツのUNIX User Groupが主体になって取りまとめて ISO/IEC TR14652を出したのだけど、そもそもが複雑怪奇な仕様で実装クソめんどくさい上に一部の文字コードの扱いに難があるので、結局TR止まりで撤回(withdrawn)されたっきりなのよね。 やっぱりジャガイモ喰ってるようなのはダメだな。

そんでlocaledefに喰わせる定義ファイルを取りまとめてた作業部会も、流行りモノ大好きなミーハー連中なのでフォーマットを時代の寵児XMLにしようぜ!文字集合もUnicodeで統一な!とCLDR(Common Locale Data Repository)に流れて行ってしまい解散してしまったというね。

まぁそんなオワコン状態ではあるけど、いまだlocaledef(1)を使ってるOSが多いのでそれ前提で話を進める。mklocale(1)とかいまだに使ってるOSは隅で腹筋でもしてろ。

@次回予告

ということでlocaledef(1)コマンドに喰わせる定義ファイルの内容を、重要なとこだけかいつまんで説明するよ。

*1:そういえばワイがRPN電卓なんてものの存在を知ったきっかけのカメラ・レンズ設計者の安原伸氏が去年お亡くなりになってたようで驚いた、「安原製作所回顧録」も枻出版社が今年に入って民事再生で復刊も無いだろうなぁ。
*2:なんかPOSIX原理主義を名乗って無意味な縛りプログラミングを他人に強要するアレなのが跋扈してると聞くので最近はチンポジックスと呼んでいる。
*3:ん?Qt?WxWidget?知らんし勝手に調べてどうぞ、さぁ僕とOSF/Motifの美について語ろうか…
*4:実装によってはもっと沢山のカテゴリ(カレンダーや用紙サイズ、住所表記とか)あるのだが移植性無いしそもそも公開API無いので無視してよろし。

2021/04/21(Wed)

[NなんとかBSD] N HEADのLC_NUMERIC/LC_MONETARY定義などが地味にぶっ壊れてる件

printf(3)の桁区切り実装の記事書きながらついでなのでオレオレN6のLC_NUMERICデータ精査してたんだけど、ginsbach@だったかがFから持ってきたLC_NUMERIC定義、間違いだらけ仕様違反だらけでウンザリしてしまった。今回ネタに使おうと思ってたインドの桁区切りなんかも順序が逆になってんじゃねーか。 他にもおかしな定義が満載で、もう全部捨ててやろうかって気分になっている。

本家の方もどうせ放置やろうなと興味本位で覗いたらもっと酷いことになってた。名前も出したくない某ドイツ人が数年前にCLDR(Common Locale Data Repository)を元にUTF-8 localeなLC_NUMERIC他を生成して上書きコミットしとるんだけど、まともに仕様読んでねえいつものパターンで thousands_sepとかdecimal_pointに仕様上許されてない(未定義動作になる)マルチバイト文字(U+00A0つまり\xc2\xa0)が入っちゃってるファイルが多数あるもよう。

# Decimal Delimiter/Radix Character (decimal_point)
,
# Separator for groups of digits left of above (thousands_sep)
\xc2\xa0
# Grouping Sequence (grouping)
#    A sequence of integers separated by semi-colons ';'.
3
# EOF

普段からUTF-8 localeなんぞに設定してるとエディタ上でスペースにしか見えないパターンなんやなw

仕様通りに1byte決め打ちで実装しとるコードがちょん切って不正なバイトシーケンス出力しちゃいますなぁこれ。

decimal_point
	The operand is a string containing the symbol that shall be used as the decimal delimiter (radix character) in numeric,
	non-monetary formatted quantities. This keyword cannot be omitted and cannot be set to the empty string.
	In contexts where standards limit the decimal_point to a single byte, the result of specifying a multi-byte operand shall be unspecified.

thousands_sep
	The operand is a string containing the symbol that shall be used as a separator for groups of digits to the left of the decimal delimiter in numeric,
	non-monetary formatted monetary quantities.
	In contexts where standards limit the thousands_sep to a single byte, the result of specifying a multi-byte operand shall be unspecified.

はい、どちらも仕様で「シングルバイトに限る、マルチバイト指定したら未定義動作」とあるよね。ほんとあいかわらず仕様読まねえ確認しねえピーだなあいつ。 うーんピーだってのは長年のやりとりで知ってたからこそ関係切りたくてNのdeveloper辞めたんだが、さすがにCLDRとPOSIX localeは似て非なるものだという事すらわからんレベルだったとはね。

ちなみにvfwprintf.cの実装もあらためて確認したけど、thousands_sepもdecimal_pointも

		case '\'':
			thousands_sep = *(localeconv_l(loc)->thousands_sep);

とか

					if (prec || flags & ALT)
						PRINT(decimal_point, 1);
...
					if (prec || flags & ALT) {
						buf[0] = *decimal_point;
...
					buf[1] = *decimal_point;

どちらもシングルバイトであると仮定したコードになってるので、ちょん切れて出力されちまうってわけよ。

ついでにvfscanf.c/vfwscanf.cもだな。

	char decpt = *localeconv_l(loc)->decimal_point;
...
	wchar_t decpt = (wchar_t)(unsigned char)*localeconv_l(loc)->decimal_point;

小数点認識できず下だけ読み飛ばされるから誤差がいつのまにか溜まってドカンとくるやつだコワ~、まぁでもscanf使ってる時点でコワ~ではあるけど。

同じ仕様上の制限はLC_MONETARYの方のmon_decimal_point/mon_thousands_sepにもあって、そっちもやっぱりマルチバイト文字入っちゃってるし、strfmon(3)もアウトやね。

	thousands_sep = *lc->mon_thousands_sep;
	if (thousands_sep == '\0')
		thousands_sep = *lc->thousands_sep;

というかstrfmon(3)なんて関数の存在をすっかり忘れていた。

そもそもなんで桁区切り文字がU+00A0(NO-BREAK SPACE)に化けるんだろうな…まぁCLDRはXMLだから&nbsp;がU+0020に変換すべきをU+00A0に変換されてこの惨状ってのは容易に想像つくんだけど、変換スクリプトすらまともに書けねえとはなぁ。(追記) これはISO-8859-1とUTF-8の非互換性の問題ですわ、確かにISO8859-1なら\xa0はシングルバイトだ。UTF-8に変換したとたん\xc2\xa0のマルチバイトに豹変するがな。

まぁISO-CやPOSIXのシングルバイト文字以外の小数点はダメってのも、中東なんかじゃMomayyez(ARABIC DECIMAL SEPARATOR, U+066B)を使うからよろしくないんだけどね。 でも今回の問題はそもそも文字が化けて不正なデータになってる時点で何一つ擁護できるポイントがねぇんですわ。

[Linux] どうやらglibc2のLC_NUMERICも仕様違反しとるもよう

うーん、glibc2もLC_NUMERICに仕様に反してマルチバイト文字列な桁区切り文字指定しとるのな。

 LC_NUMERIC
 decimal_point             ","
 thousands_sep             "<U202F>"
 grouping                  3
 END LC_NUMERIC

これU+202FってNARROW NO-BREAK SPACEかぁ、U+0020でいいじゃんこんなのほんとUnicodeって…

ただglibc2の場合は未定義動作であるマルチバイトな桁区切り文字でもちょん切らずに全部出力しとるから不正なバイト列になるとかは無いので、まぁ意図的だろうな。

            do
              *--w = thousands_sep[--cnt];
            while (cnt > 0);

小数点の方も大丈夫っぽい。

          decimal_len = strlen (decimal);
...
              cp = (char *) __mempcpy (cp, decimal, decimal_len);

まぁglibc2は書式指定子にIという拡張があるくらいだからこれくらいやるかもなとは思っていたのでソース確認したのだ。

基本的に小数点と桁区切り文字ってlocaleconv(3)やnl_langinfo(3)経由でlibc外にも公開されてるものなので、仕様違反やらかして移植性無くすのほんと勘弁してほしいですわ…

ちなみにマルチバイト文字を認めると、ISO-2022-JPのようなロッキングシフトによる状態遷移の発生する文字コードで破綻するのだよね。 まぁそれは元々破綻してるからいいんだけどさ…

[Solaris] Solaris10のバヤイ

どうもSolaris10のlocaledefもやらかしとるっぽい、フランス式桁区切り地域のUTF-8 localeは全滅かなぁこれ。

まず/usr/lib/localedef/src/locales/fr_FR.UTF-8.src.bz2

**************
LC_NUMERIC
**************

decimal_point	"<COMMA>"
thousands_sep	"<NO-BREAK_SPACE>"
grouping	3

そんで/usr/lib/localedef/src/charmaps/charmap.utf-8.bz2

<NO-BREAK_SPACE>                      \xC2\xA0

どちらのファイルも(C)はUnicode, Inc. まぁこいつらが伝染元なんだろう。昔っからお粗末な変換表なんかで世界に大迷惑かけてきた体質は変わらんな…

ところがですね、実際にSolaris10のprintf(1)コマンド使ってフランス式の桁区切りを試してみたんだけど

$ LANG=fr_FR.UTF-8
$ export LANG
$ printf "%'d\n" 1000
1 000
$ printf "%'d" 1000 | od -x
0000000 2031 3030 0030
0000005

!?定義ファイル上はU+202F(NO-BREAK SPACE)なのに、出力はU+0020(SPACE)になって仕様通りになっとる…

locale -k thousands_sepの結果もU+0020だしナニコレ。

$ locale -k thousands_sep
thousands_sep=" "
$ locale -k thousands_sep|od -x
0000000 6874 756f 6173 646e 5f73 6573 3d70 2022
0000020 0a22
0000022

さっきのglibc2みたいに全部出力するなら\xC2\x\A0のはずだし、Nみたいに先頭1バイトだけなら\xC2で不正なUTF-8シーケンスのはずだし、何が起きてるんだこれ。

まぁ中途半端な文字コードの知識だとISO-8859-1のNO-BREAK SPACEは0xA0だからUnicodeと互換性あるしUTF-8でも同じと勘違いするやついるよね。 肝に銘じろISO-8859-1とUnicodeはCCSとして互換はあるが、CESとしてUTF-8を使うのならバイトシーケンスに互換性は無いのだ。 なのでUTF-8を採用した場合にISO-8859-1ではできたことができなくなる劣化は当然ありうるってことにな! 通貨記号とかも同じだわな。

ということであとでOpenSolarisかOpenIndianaのソース探すかなぁ、国際化まわりは非公開だった気がするけど。 まぁ多分パッケージとソースが不一致なんじゃねぇかなとしょーもないオチな気はするが。

[NなんとかBSD] 続・N HEADのLC_NUMERIC/LC_MONETARY定義などが地味にぶっ壊れてる件

なるへ、最初にginsbachがFからLC_NUMERIC移植した時の仕事からしてバグっとったんだな。 src/share/locale/numeric/Makefileで *.UTF-8のソースを*.ISO8859-1とかやらかしよる。

LOCALESRC_fr_FR.ISO8859-15=     fr_FR.ISO8859-1
LOCALESRC_fr_FR.UTF-8=          fr_FR.ISO8859-1

かつてUnicode派がISO8859-1とUnicodeは上位互換だから移行作業必要なしとか大嘘ぶっこいてたのに騙された無知がこういうところで爆発するんだよなぁ。

たしかに符号化文字集合(CCS)としてはコードポイントは上位互換なんだけど、文字符号化手法(CES)に落とし込むと0x80以降が1byteから2byteになるから互換性なぞ皆無なのよな。 Unicode派のデマ信じてUTF-8としてISO8859-1を変換せずにそのまま流すと事故起きるというパターン、うおおおおおおおおおおおじゃねぇかマジで。

おそらくsend-prかなんかがあってginsbachの仕事をバグとして認識したまではいいのだが、例のドイツ人が俺は国際化に詳しいとかいってろくにLC_NUMERICの仕様読まずにISO-8859-1→UTF-8への変換してより事態が悪化したってことですわ。

このへんワイはUTF-8 localeいっぱい増えるの嫌で、すでにlegacy encodingなlocaleインストールされてたらUTF-8 locale要求された時はlibc内でiconv(3)するかなと考えてたんだけど、このthousands_sep/decimal_point問題とかあるから断念したんだっけ、ああなんかいろいろ思い出してきた…

[NなんとかBSD] 続々・N HEADのLC_NUMERIC/LC_MONETARY定義などが地味にぶっ壊れてる件

どーせglibc2がthousands_sepやgroupingにマルチバイト許容してるんだし、Nも同じ様に直せばいいやんと開き直られるとアレなので、マルチバイトにすると発生する問題を書き残して先制攻撃しておくか。

お前は一体誰と戦っているんだ、天井の染みの声とだよ!

まずは実装上のお話、過去回でちょこっと触れたけれどもNのvfwprintf.cは変換バッファに

/*
 * The size of the buffer we use as scratch space for integer
 * conversions, among other things.  Technically, we would need the
 * most space for base 10 conversions with thousands' grouping
 * characters between each pair of digits.  100 bytes is a
 * conservative overestimate even for a 128-bit uintmax_t.
 */
#define BUF     100

        CHAR_T buf[BUF];        /* buffer with space for digits of uintmax_t */

を用意し、これで128bit integer時代も万全とかいってるわけだけど、この大前提が崩れるのよな。

なおこの数字はこなみ感あふれるクッソ適当な値にみえて、1桁毎に桁区切り文字がはいったとして128bit最大値で

$ printf "3,4,0,2,8,2,3,6,6,9,2,0,9,3,8,4,6,3,4,6,3,3,7,4,6,0,7,4,3,1,7,6,8,2,1,1,4,5,6" | wc -c
77

で77バイトだからわりと妥当な数字ではある。

ところがthousands_sepにマルチバイトを指定できるってことは長さはMB_LEN_MAXすなわち無限大となるわけで、この変換バッファはもはや静的に確保できない事を意味する。 当然性能アレになるよね。

つーかこの程度でmalloc(3)呼ぶ羽目になっとったら、Nの拡張APIであるけどsnprintf_ss(シグナルセーフ版snprintf)が大概シグナルアンセーフになるのである。 さすがにdoubleは出力できませんって仕様制限は許容できてもint出力できないってのはもうこのAPIの存在意義無くなるでしょ。

まぁそれでも100%無いとは思うがもし将来的にISO-C/POSIXがマルチバイト可に仕様が変わったら、オレオレN6でも実装せざるを得ない。 ワイなら変換バッファにはthousands_sepの代わりに非digit charなマーカーつっこんどいて、出力時にthousands_sepと置き換えるって方法で実装するけど、桁区切り毎にfwrite(3)を分割して呼ぶことになるからわりと性能劣化するのがネックやな。

あとこれも前にちょっと書きかけたけど、ISO-2022-JPのようなlocking shiftを伴うstateful encodingにおいては、thousands_sepに指定する文字はinitial stateでvaild(つまりsingle byte)でないと複雑さが増す。 桁区切り文字や小数点文字を出力する時に、マルチバイトをいったんワイド文字に変換してまた戻すってやらんと状態遷移できないからな。 おそらく仕様がマルチバイトを禁止してるのはこれも理由の一つと思われる、かつて日本のメーカーが人出してた時代はちゃんとその辺考えてたそうだしぃ。

そんで仕様がシングルバイト文字縛りにしている最大の理由、それはprintf(3)の幅指定やゼロパディングとの整合性なんやな。

この幅指定とか桁区切りってのは結局のところ「見やすく文書を整形するための機能」であって、マルチバイトな桁区切り文字や小数点がここに入ってきて幅指定のバイト数を喰うと表示がグチャグチャに崩れてしまうんよな。

まぁシングルバイトの幅が1とは限らんしそもそもそういう意図なら仕様は「幅指定はバイト数でなくwcwidth(3)の幅に従う」に変えろなんだが、そもそもこの21世紀にフォントの幅が固定とかありえないwcwidth(3)自体オワコンという話なので、いまさら仕様を変える意味は無いのだ。

結局のところprintf(3)のこの辺の仕様自体もう時代にそぐわない過去の遺物なので、glibc2よろしく「拡張したぜ便利だぜ」とかぬかすのは無能の働き者の証拠よ、もう触らずにそっとしておけというお話。 古代文明の遺跡で発見されたトラックっぽい巨大ロボを原理も知らずに運転してるとそのうち宇宙ごと人類まとめて死ぬといういつもの話。

そもそも桁区切り文字にSPACE(U+0020)でなくわざわざNO-BREAK SPACE(U+00A0)なんつーのを発明した理由って、自動改行をスペースなんぞに頼っとるから数値の桁区切りは例外としたいという横着にしかみえんもんな。 なんなら日本語もいちいち辞書使って分かち書きとかしなくていいよう単語の境界を表すゼロ幅の「文字」を要求してもいいんじゃねぇかな…ってもうZERO WIDTH SPACE(U+200B)あるからそれ使えっていわれるか…うん…文字ってなんだ(哲学)

本質的にSPACEとNO-BREAK SPACEでそこに何の違いもありゃしないことを学ばずに、NARROW NO-BREAK SPACE(U+202F)とかFIGURE SPACE(U+2007)とかどんどん増えていくUnicodeほんと地獄としかいいようがない。

[NなんとかBSD] locale(1)コマンドのバグめっけた

もういっこNのしょんないバグめっけた、CitrusのNマージ時にFから持ってきたlocale(1)なんだけど、LC_NUMERICのgroupingおよびLC_MONETARYのmon_groupingの表示がバグっとるわ。

$ LANG=en_US.US-ASCII locale -k grouping
grouping="^C"
$ LANG=en_US.US-ASCII locale -k grouping| od -x
0000000     7267    756f    6970    676e    223d    2203    000a
0000015

"\003"という値が入ってるけど、これちゃんと人間が読めるように

$ LANG=en_US.US-ASCII locale -k grouping
grouping="3"

と表示されるようにせんとダメなのだ、NのHEADも直ってないねぇ。ちなみにFの方は4年前くらいに修正されとるもよう。

これginsbachがLC_NUMERIC/LC_MONETARYをFから移植した時に一緒に直すべきだったもんだけど、そもそも彼は仕様なぞまったく把握しておらずlibc内では"3"ではなく"\003"に変換しないとならないことすら知らんかったから直せるはずもなく。 というかlibc内では不正な値がgroupingに入ってるけど、locale -k groupingでみりゃ正しい値にみえてるって状態だから気づかんわなそりゃ。

なのでワイが尻ぬぐいでfix_groupingという関数を実装してこのバグ潰したんだが、同時にlocale(1)も直さんといけんことに気づくべきではあった。 でもなぁ当時ワークエリア外の人間に断りなく好き勝手やられて、その上嫌ならrevertでなくお前が直せしかも枝切りの時期だから今すぐやれ言われてマジ内心ブチ切れてたんで気づく余裕なんかねーよ。

そもそもその時俺はデスマの真っ最中に家族の大病が発覚し、会社拝み倒して介護のため現場抜けようとしたら引き継ぎ相手の家族も倒れて白紙に戻るというまともじゃない状況でな。 よくその時に我慢して時間を無理矢理作って直したと思うよ、移動中くらいしか時間ないし現場にゃPC持ち込めんから紙にコード書いてたりもしてたしな。

そんで次にワークエリア外の奴になんかやられたらはもう二度目は無いと決めてたのでmulti-localeの件でうん国際連盟を堂々と退場し、今ではBitBucketで自分の好きなように植民地経営と軍拡を行っているわけだ、ああ地上の楽園被害を被る国民も自分一人だし気楽でいいわこれから毎日核実験しようZ

2021/04/22(Thu)

[オレオレN6] distrib/sets/lists/*滅ぼしたい

チラ見しただけでひどい出鱈目っぷりを了解したので、とりあえずLC_NUMERICとLC_MONERARYの(mon_)?(thousands_sep|decimal_point|grouping)の監査をはじめることにした、よってそれが終わるまでディアゴスティーニ週刊printf(3)は中断です。

なおLC_MONETARYについてはcurrency_symbolのユーロ記号もISO-8859-15とかUTF-8では代替表示でなくU+20AC表示しないと問題あるはずだけど、一度に全部やるとグチャグチャになるし、手をつける前に欧州連合が瓦解してユーロ無くなる可能性もワンチャンあってそれに賭けたい気分である。

世界でユーザー1名のOSが国際化する意味はと一瞬考えこんでしまうが、ワイがある日ホースラヴァーファット氏のようにヴァリス神からのピンク色のレーザー照射を脳に受信してありとあらゆる言語をペラペーラになる可能性もあるのでな。

ついでにこの20年でISO8859-*ではなくUTF-8を使う人間の方が多くなったし、せいぜい数MBのストレージサイズを気にするような時代でもなくなったので、UTF-8 localeも作るように作業してるのだが、新しくインストールされるファイルが増えるたびにetc/mtree/*とdisrib/sets/lists/*の修正が発生しクソめんどくさい。

前者はmtreeはpermissionやらのsecurity checkを担ってるからまだ我慢できるのだけど、後者は前者と重複する定義があるしそもそも頓挫したsyspkgのためだけの非常に煩雑な今や用無しラ・フランスな記述が要求され、無意味な作業と脳が認知すると残り少ない脳細胞が全滅する。 現状build release時にreleasedirにゴミが残ってないかのチェックと、etcupdate(1)が警告するobsoleteとなったファイルの情報にしか使ってないのに、作業量多過ぎなのである。

そもそもアレが導入された頃にMakefileはすべてを知ってるんだからこんなもん手書きすること自体異常なのでは…という意見を述べたことあるのだが、他のdeveloperの方にはあまり賛同いただけなかったと記憶している。 ワイの意見ではmakeのターゲット叩けば自動でdisrib/sets/lists/*は更新されるべきだったのだが、反論としてはMakefileの記述が間違ってた時が怖い、distrib/sets/lists/*との整合性でそれが検出できるとかなんとかだったと思う。 でも正しく更新されたかのチェックはcvs diffの差分で一目瞭然で判るよなぁ…と思ったのだがそれ以上は黙っていた。まぁワイはワークエリア外のことについてはノータッチというポリシーだったし。

似たようなものにp**srcにもPLISTというクソめんどくせえシロモノがあるけど、あれはmake(1)ですらないかもしれん3rd partyなbuild systemをwrapするものだからまぁやむを得ない。 しかしそういう制限が前提にあってもLIBTOOLIZE_PLISTとかPLIST_TYPE=dynamicで動的に生成するようになっとるからな、結局自分でめんどくささを体験するかしないかの違いなんやな。

ということでありとあらゆることを自分の手を動かさないとどうにもならんオレオレN6では、ビルドシステムからはlint(1)に引退いただくのとdistrib/sets/lists/*滅ぼすってのはいつかやりたい。 まぁそのいつかがくる日まで生きていられるかどうかは日に日に怪しくなっているが、蝉は、やがて死ぬる午後に気づいた。ああ、私たち、もっと仕合せになってよかったのだ。 もっと遊んで、構わなかったのだ。という事なのである。

[文字コード] ARMSCIIとUnicode 7.0

アルメニアのLC_NUMERIC修正してて気づいたけど、ARMSCII-8の0xA1に存在するArmenian Eternity Signって結局Unicode 7.0で収録されたんだな。 まぁ絵文字がばんばん入る時代にいまさら「残念!これは文字じゃありません!宗教シンボルだからダメです!」って理屈は通らんもんな。今なら例の金の字とかも通るのでは?

昔のように文字で無いものを収録するスペースなどない(キリッ)どころでなく、絵だろうが何だろうがコードポイント埋めないと存在理由が無くなるグダグダUnicode会社なので、近い将来菊紋と桐紋そして家紋5000種類が入り、欧州人がそれにつられて貴族の紋章も入り、悔しくなったメリケン人が刺青をUnicodeに提案しだすまでまであと10年かからないと思う。

確かワイが最後にiconvdataの更新かけたのUnicode 8.0で誰も使わなそうなshift_jis-{docomo,kddi,softbank}とか生やしたときだっけか、見落としてたなぁいかんいかん。 つーか今13.0でもうすぐ14.0なのか…ずいぶんサボってたもんだな、まぁそれどころじゃなかったんだけど。

ということでまたひとつLegacy EncodingにあってUnicodeに無い文字が消えてしまったか、とはいえUnicode 7.0はまた余計なことして

と右巻き左巻き(政治じゃねえぞ)とどっちも採用したよとかやらかしとるんだが、これARMSCII-8の0xa1はどっちになるんだこれ。

今は消失してる ArmSCII WGのページをウェイバックすると左巻きの革命的大勝利な感じだけども、じゃあ逆にUnicodeからARMSCII-8へ変換する時に右巻きは左巻きに転向させるのかそれとも処刑か悩むなこれ。

このARMSCIIは小さな文字集合にも関わらずUnicodeのブレブレっぷり楽しめるいいサンプルであった、この宗教シンボルの収録可否とかだけでなく、他にも0xA2のArmenian Ligature "ew"は合成文字なので合成するのか分解するのかとかね。

ちなみに一部の変換表ではこの宗教シンボルを未割当領域のU+0530に勝手に割り当てたり、似てるからって❁(U+2741 EIGHT PETALLED OUTLINED BLACK FLORETTE)に勝手にフォールバックする実装があったけども、ワイの書いたCitrus iconvの変換表ではそうはせず私用領域(PUA)に割り当てていた。

ということで現状PUAに割り振っとる文字が今のUnicodeでどうなってるかも調べないとなぁ。

2021/04/25(Sun)

[オレオレN6] LC_MONETARY/LC_NUMERICの監査

とりあえずLC_NUMERICとLC_MONETARYの定義を(mon_)?(thousands_sep|decimal_point|grouping)そしてcurrency_symbolに限ってだけど監査してごっそり修正。 とにかくCLDRとか他OSの実装もまったく信用ならんので困る。

ここ10年の間に

が結構あって確認するのに丸一日潰れてしまった、まぁ欧州情勢は複雑怪奇というかそらウォッカ視点じゃジャガイモを3度目の ベイクドポテトにしたろうかって認識になるわな、おーこわ。 そら紅茶もライン川沿いに ローストチキン製造機を埋めてまとめて料理するかって考えても当然と思われる、政治ネタではなりません明日の夕食何にしようかなって話です。

あとUTF-8 localeつっこんだのでCJK Ambigious Width対応を入れんとならんのだが、昔作りかけてる最中にマシン死んでリポジトリ救出できなかったやつまた最初っから作り直しかぁと萎える。 ちなみに今のFから持ってきたUTF-8 locale定義ってそもそもどうやって生成したか一切不明なので、定義の正確性とかライセンス周りなどまったく信用ならんのだ(パラノイア)。

まぁ特にライセンス面考えると更地から作業はじめんとなのだが、結構めんどくさいんだよなあのUnicode data処理すんの。 とか思ってたら最近はあのクソなフラットファイルじゃなしにXML形式でも配布してるんだな。

それどころかCLDRもJSON化とかはじめとるし、ねえ皆さんなんでRDBMS化しないんですかね…

そいやGB18030とUnicode BMPの変換テーブルもクソめんどくさかったのを思い出した、仕様的には以下のようなコードで算術…うん一応算術だね…で出せるんだけども。

int
gbk_exists(uint32_t src)
{
  ...
}

int
main(int argc, char *argv[])
{
  int b1, b2, b3, b4;
  uint32_t ucs = 0x0, gb;

  for (b1 = 0x81; b1 <= 0xfe; ++b1) {
    for (b2 = 0x30; b2 <= 0x39; ++b2) {
      for (b3 = 0x81; b3 <= 0xfe; ++b3) {
        for (b4 = 0x30; b4 <= 0x39; ++b4, ++ucs) {
          for (; ucs <= 0xfffd; ++ucs) {
            if (ucs >= 0xd800 && ucs <= 0xdfff)
              ucs = 0xe000; /* dummy */
            if (gbk_exists(ucs))
              goto found;
          }
          exit(EXIT_SUCCESS);
found:
          gb = b1 << 24 | b2 << 16 | b3 << 8 | b4;
          printf("%#.8X = U+%.4X\n", gb, ucs);
        }
      }
    }
  }
}

当時書き捨てたコードの一部を抜粋したものだけど、お判りの通りgbk_exists()が参照する変換テーブルが間違ってると即レズるじゃなくてズレるんだよね。 これがまたGBKの私用領域の仕様が資料ごとにまちまちで、何度やってもICUの変換結果と一致しなくて困った。

気づいてしまったがGB18030も追加文字対応とか基本面以外の文字も変換表作らんとならんのかうげげ、作った当時は追加漢字面に数文字とかだったけど。

[自宅ネットワーク管理者] HPE OfficeConnect 1820買うた

以前現場で酷い目にあって窓から投げ捨てろリストに入れたHP Procurve 1810( 過去記事参照)なのだが、何の因果か マッポの手先後継機であるHPE/Aruba OfficeConnect 1820買ったよ。

以前からIEEE 802.3ad(LACP)対応スイッチ欲しかったのよね、手元にあるのはプログラマのワイにネットワーク屋の真似事させるという無茶振りやらされとった頃に自腹で学習用に買ったHP Procurve 2510無印の100baseしか無くてな。 とはいえ新規に買うのも予算が厳しかったのだが、この古いProcurve×2台をこんなんタダでも欲しがるやつおらんやろとダメ元で売りに出したら、想定の3倍くらいの値段で売れたので予算が発生してしまったのだ。

正直1810の後継ってことでどーせ以下略なんやろうなぁと躊躇したのだけど(実際ファームアップの回数見るだけで察する)、ファンレスの機種ってもう今こいつしかないのよね。 もう何年も認知症老人の深夜徘徊の気配を察知するのにちょっとの物音に反応する体になってしまったもんで、無音しか選択肢がないのだ。

同時にクッソ古いHPのKVM over IPも処分したんだけどこっちも予想の4倍くらいで売れてしまった、やっぱリモートワーク需要ですかねぇ新型コロナ(notトヨタ)以来いいことあったの初めてだよ給付金も施設代で即日溶けたからな。 でもいまどきPS/2なアダプタしか無いからUSBタイプも別途買わなきゃならんだろうし新品買った方がよくない?

まぁ安く上げたいならリモート機能無しのタイプは相変わらず中古で安いので、俺だったらラズパイでKVM over IPを作る Pi-KVMと安物のKVM(さすがにOSD対応でないと厳しいけど)を組み合わせてリモートオフィス構築するなぁ。

そもそも今時のサーバならiLOとかiDRACとかパソコンでもIntel AMT標準装備だろうしそれ使わんのかと思ったけど、あれもいざ使おうとすると専用のネットワーク作らんとならんからめんどいか。

このPi-KVMも一度試してみたいけどそのためにラズパイ買う金なぞ無いので、余ったパソコンにでも入れられるようx86イメージ欲しいんだよな、誰かやってくれクレクレ。 というかルーターでこれ内蔵した製品とか出てきてよさそうなんだけどな。

とりあえず1820のセットアップはまた後日…

2021/04/26(Mon)

[オレオレN6] UTF-8 LC_CTYPE

先日も書いたけど今のFから持ってきたUTF-8 LC_CTYPEは作者がどういう工程を経て作ったのか一切不明なので、ライセンスとか正確性が微妙なもんでワイとしてはいちから作り直すって結論に至るのであるパラノイアは生きづらいですね。

昔ディスク吹っ飛ばす前に作業してた時はフラットファイルなUCD(Unicode Character Database)のパースがめんどくせえなぁで萎えてたんだけど、今はXML形式あるから少しは楽かなと思ったら、perlのXML::XPathだとucd.all.flat.xmlどころかucd.nounihan.flat.xmlですらメモリ不足でKilledで、まぁそうだよねと本日閉店ガラガラ。

今時メモリ1GBしかない最貧VPSでデータ処理とかえーマジメモリ1GB!?キモーイメモリ1GBが許されるのは2000年までだよねーキャハハハハハハって言われるとぐうの音も出ないのだが、人類におよそ害しかなさそうな暗号資産とかなんちゃって機械学習で湯水の如く使われる無駄な電力と計算機資源による未来考えるとこのままでいいやって気にもなりますハイ。

まぁXPathとか使わねーでフフフSAX!SAX!みんなSAXし続けろ!激しく!もっと激しく!!1!で書きゃいい話なのだが、やめないか!(精神崩壊シリーズ)

そいやSAXといえば BLUE GIANTのアメリカ編は誰もがこの章で完結と思ってるけど、サン・ラ・アーケストラの影響でアフロフューチャリズムそしてスペースジャズに傾倒した大が、アフリカ大陸横断の旅そして宇宙へと続くと予言しておく。 たぶん最後はヘミングウェイオマージュでキリマンジャロの山頂近くで凍てつき眠る豹を目指して遭難エンド、よくがんばった!

サン・ラを知ったのは幼き日にテレビで見たライブ・アンダー・ザ・スカイだったなぁ、ジャズでこんだけ集客できた時代凄いよな…バブル景気スゲーわ。

まぁワイはまだフリー・ジャズさっぱり理解できんかったし、やっぱりデヴィッド・サンボーンみたいなわかりやすいジャズ・フュージョンの方に惹かれたけどな。

2021/04/27(Tue)

[オレオレN6] UTF-8 LC_CTYPE(その2)

はい今回も完全にタイトルとは無関係な内容です。

前回はXPathがUCD(Unicode Character Database)にワンパンで瞬殺されたところまで、ということで代わりにSAX的なもので対抗することにする。 どうせXMLである必要すらないデータだからツリー構造とかどうでもええねん、というかCSVとかで配布すりゃいいのだこんなもん。最適なフォーマット選べないすなわちピーなのである。

まぁこんな貧者的プログラミングなんぞ、CPU/GPU/メモリ/ストレージ/ネットワークを強欲に独占すればオッケーなモノポリーの方々からすれば虫ケラの所業であろうが、お釈迦様のいう長者の万灯より貧者の一灯の精神であり荘子のいう蟷螂の斧でもある。 そういえばカマキリって英語だとPraying Mantisであの動作は歯向かうのではなくお祈りにみえるそうっすね、しょせん力無き者の反抗なぞ命乞いにしか見えんのだろうなぁ…

このUCDのデータ量なんぞなんぞビッグデータとかいう砂金掘りからみれば極楽の蓮花が咲く池の底に落ちたゴミくらいだろうが、道具の選択間違えるとコトかもだまずはベンチ取ってから作業なので?

@perl-5.32の場合

まずはいつものPerl、XML::SAXモジュールでucd.all.flat.xml(およそ193MB)をパースするだけ(ハンドラは空っぽ)のコードで実行環境はCygwin。

#!/usr/bin/perl
package MyHandler;
use XML::SAX;
use XML::SAX::Base;
use Module::Load;
use base qw(XML::SAX::Base);
sub start_element
{
}
my $class = $ARGV[0];
load  $class;
my $parser = $class->new(Handler => new MyHandler());
open(my $fh, '<ucd.all.flat.xml') || die;
$parser->parse_file($fh);
1;

いくつかSAXドライバにも種類があるので、

  • XML::SAX::PurePerl … XML::SAX同梱のデフォルト実装
  • XML::LibXML::SAX … libxml2のSAX APIよる実装
  • XML::LibXML::SAX::Parser … ストリームでなくDOMをパースするSAX実装、当然のようにメモリ不足になるので除外
  • XML::SAX::Expat … PerlによるExpat実装
  • XML::SAX::ExpatNB … ↑のNonBlocking版
  • XML::SAX::ExpatXS … libexpatによる実装

の実行時間を比較したのだけども

$ time ./unko.pl XML::SAX::PurePerl

real    42m43.255s
user    41m48.280s
sys     0m1.453s

$ time ./unko.pl XML::LibXML::SAX

real    1m6.316s
user    1m5.796s
sys     0m0.358s

$ time ./unko.pl XML::SAX::Expat

real    2m42.115s
user    2m40.062s
sys     0m0.311s

$ time ./unko.pl XML::SAX::ExpatNB

real    2m43.091s
user    2m42.248s
sys     0m0.625s

$ time ./unko.pl XML::SAX::ExpatXS

real    1m1.332s
user    1m0.937s
sys     0m0.296s

まぁPurePerlの性能が論値つーかあまりにもひどいのとDOMベースのSAXとは(哲学)NonBlockingの効果とは(哲学)なネタ枠はさておき、下り最速のExpatXSすらオシッコ漏れちゃいそうなほどに尋常に遅い。

@python-3.8の場合

こいつはxml.parsers.expat以外の実装あるのか知らんのでこんなもん。

#!/usr/bin/python
import xml.sax
import xml.parsers.expat
from xml.sax.handler import ContentHandler
class MyHandler(xml.sax.handler.ContentHandler):
  def startElement(self, name, attr):
    pass
parser = xml.sax.make_parser()
parser.setContentHandler(MyHandler())
parser.parse(open('ucd.all.flat.xml'))

実行結果は

$ time ./unko.py

real    0m11.335s
user    0m11.015s
sys     0m0.202s

とおー速い速い、p5-XML-SAX-ExpatXSの数倍以上ですわ。

なおワイのPython経験はTracにあった国際化まわりのバグを修正した15分程度なのでよくわからない俺は雰囲気でPythonを書いている以下略

@Rのつく言語

ついでにRのつく言語でも試してみる。

library(XML)
startElement <- function(name, attrs)
{
}
xmlEventParse('ucd.all.flat.xml', handlers = list(startElement = startElement))

つい先日までPowerShellがマイブームだったがその前はRを嗜んでおったはずなんよねもう記憶から完全に消えとるが。 こいつだけcygwin binaryではないがまぁ大した違いは無いはず。

$ time /cygdrive/c/Program\ Files/R/R-4.0.5/bin/Rscript.exe unko.R

real    0m12.728s
user    0m0.000s
sys     0m0.015s

うんやっぱりこのくらいは出るよなぁという。

いやRってそっちかよ!ってネタなのだが、ちゃんとRuby-2.6も試したよ!

まずはPureRubyなREXMLとかいうの。

#!/usr/bin/ruby
require 'rexml/parsers/sax2parser'
require 'rexml/sax2listener'
class MyHandler
  include REXML::SAX2Listener
end
parser = REXML::Parsers::SAX2Parser.new(File.read('ucd.all.flat.xml'), MyHandler.new)
parser.parse
$ time ./unko1.rb

real    2m43.624s
user    2m41.718s
sys     0m0.359s

まぁp5-XML-SAX-Expatと同程度すね。

そんでlibexpatではなくlibxml2バックエンドのNokogiriだとこんな感じ。

#!/usr/bin/ruby
require 'nokogiri'
class MyHandler < Nokogiri::XML::SAX::Document
  def start_document
  end
end
parser = Nokogiri::XML::SAX::Parser.new(MyHandler.new)
parser.parse(File.open('ucd.all.flat.xml'))
$ time ./unko2.rb

real    0m32.263s
user    0m31.187s
sys     0m0.562s

ふーんp5-XML-LibXMLの倍は速い。

他にもRubyはSAX実装いっぱいあるっぽいけど他の人のベンチ見る限り期待できそうも無いので、いちばん速そうなlibexpat使うxmlparserだけ。

#!/usr/bin/ruby
require 'xml/parser'
class MyParser<XML::Parser
  def startElement(name, attr)
  end
end
parser = MyParser.new
parser.parse(File.open('ucd.all.flat.xml'))
$ time ./unko3.rb

real    0m12.909s
user    0m12.312s
sys     0m0.312s

やっぱlibexpatならこんくらいの速さ出るよね、やっぱりPerl5の遅さはアレやのう。

ただ残念なことにこれ誰もメンテしておらずobsoleteなようで

  • rb_raiseにフォーマット文字列を欠いてるケアレスミスで-Werror=format-securityによりビルドが止まる
  • ENC_TO_ENCINDEXというマクロが見つからず暗黙の関数宣言扱いになり同上

という問題があって今のRuby2.6だとまともにビルド通らんので、クッソ適当に以下のpatchあてて動かしている。

--- xmlparser.c.orig	2013-02-07 09:45:23.000000000 +0900
+++ xmlparser.c	2021-04-27 17:15:26.000000000 +0900
@@ -114,7 +114,7 @@ static ID id_skippedEntityHandler;
 #endif
 
 #define GET_PARSER(obj, parser) \
-  Data_Get_Struct(obj, XMLParser, parser)
+  Data_Get_Struct((VALUE)obj, XMLParser, parser)
 
 typedef struct _XMLParser {
   XML_Parser parser;
@@ -1780,7 +1780,7 @@ XMLParser_parse(int argc, VALUE* argv, V
       if (!ret) {
 	int err = XML_GetErrorCode(parser->parser);
 	const char* errStr = XML_ErrorString(err);
-	rb_raise(eXMLParserError, (char*)errStr);
+	rb_raise(eXMLParserError, "%s", errStr);
       }
     } while (!NIL_P(buf));
     return Qnil;
@@ -1803,7 +1803,7 @@ XMLParser_parse(int argc, VALUE* argv, V
       volatile VALUE encobj;
       volatile VALUE ustr;
       enc = rb_enc_find(parser->detectedEncoding);
-      if ((int)ENC_TO_ENCINDEX(enc) != rb_ascii8bit_encindex()) {
+      if (rb_enc_to_index(enc) != rb_ascii8bit_encindex()) {
         rb_enc_associate(str, enc);
         encobj = rb_enc_from_encoding(enc_xml);
         /* rb_str_encode may raises an exception */
@@ -1829,7 +1829,7 @@ XMLParser_parse(int argc, VALUE* argv, V
   if (!ret) {
     int err = XML_GetErrorCode(parser->parser);
     const char* errStr = XML_ErrorString(err);
-    rb_raise(eXMLParserError, (char*)errStr);
+    rb_raise(eXMLParserError, "%s", errStr);
   }
 
   return Qnil;

これで正しいかは知らないし他にもセキュリティ絡みの問題もあるかもしれないのでお勧めはしない。

ちなみにp**srcのやつは警告緩めて対処したのかUndefined symbolのままビルドされとるようで外部公開サービスで使ってたらDenial of Serviceできるよねこれ…やはり古いパッケージは抹殺すべき。

$ nm /usr/pkg/lib/ruby/vendor_ruby/2.6.0/x86_64-netbsd/xmlparser.so | grep ENC_TO_ENCINDEX
                 U ENC_TO_ENCINDEX

まぁ本家の事も知らない。

なお俺のRuby経験は仕事でRubyでって指定されたけど納期優先したけりゃ俺の知ってるPerlで書かせろと答えた15秒程度なのでわからないやっぱり雰囲気で書いて以下略。

@結論

もうLL言語なんか捨ててCでかかってこいベネット!って気分になったので以下のコードを試す。

#include <stdio.h>
#include <stdlib.h>
#include <expat.h>
static void
start(void *ctx, const XML_Char *elm, const XML_Char **attr)
{
}
static void
end(void *ctx, const XML_Char *elm)
{
}
int
main(int argc, char *argv[])
{
	FILE *fp;
	XML_Parser parser;
	char buf[BUFSIZ];
	int len;

	fp = fopen("ucd.all.flat.xml", "r");
	if (fp == NULL)
		abort();
	parser = XML_ParserCreate(NULL);
	if (parser == NULL)
		abort();
	XML_SetElementHandler(parser, &start, &end);
	while ((len = fread(buf, 1, sizeof(buf), fp)) > 0) {
		if (XML_Parse(parser, buf, len, 0) == XML_STATUS_ERROR)
			abort();
	}
	if (ferror(fp) || XML_Parse(parser, NULL, 0, 1) == XML_STATUS_ERROR)
		abort();
	XML_ParserFree(parser);
	fclose(fp);

	exit(EXIT_SUCCESS);
}

こいつの実行結果は以下の通り

$ gcc -o unko.exe unko.c -lexpat
$ time ./unko.exe

real    0m5.987s
user    0m5.859s
sys     0m0.093s

…美しい、これ以上の芸術作品は存在し得ないでしょう。

まぁCにはCなりのめんどくささがあるので単純比較はそらまあできんけど、UCD程度のしょーもない中身のデータならCで書くのが一番ストレス溜まらんなこりゃ。

2021/04/28(Wed)

[オレオレN6] LC_MONETARY定義の監査は続くよいつまでも

しかしLC_MONETARYの定義マジで酷すぎる、とっくの昔にユーロに切り替わったのにドイツのfrac_digits(補助通貨の単位、1/100なら2)がマルク-ペニヒ時代のままだったり他のフランスなんかのユーロ圏も以下同文でクッソ笑ってしまった、おいユーロ切替って20年前やぞ…それもglibc2もSolaris 10も同様に間違ったままなのでこの世に仕様を理解しとるの俺だけなのでは疑惑が浮上してヒェッとなっている、まぁバグが発覚しないってことはstrfmon(3)なぞ誰も使ってねぇってのは判る、俺も存在自体忘れていた…

(追記) うへ、ユーロの補助通貨単位も1/100だから同じでいいのだ最初に参照したスペイン(ca_ES)の定義の方が間違ってた、吊ってくる。

2021/04/29(Thu)

[オレオレN6] できればgdtoa捨てたい

先日暇潰しに実装した JSON parser for CのNumber型サポート周りをきっかけに gdtoaでprintf(3)を実装するなんて記事を何本か書いて世界で0人くらいには読まれたと思うのだが、 json-cjqのNumber型の浮動小数点数サポートはワイと同じくgdtoaベースなのだけど、 Tensent rapidjson(こいつはC++実装だけど)は自前で Grisu2アルゴリズムを実装してるのだな。

さらに高速らしい Ryuアルゴリズムにでもせんと後発の車輪の再発明としても芸が無いのだが、最終的にオレオレN6に組み込むという目的からするとIEEE754でないVAXとかで困りそうなのがネックである。

以前ちらっとソース眺めた CによるRyu実装には

#define DOUBLE_MANTISSA_BITS 52
#define DOUBLE_EXPONENT_BITS 11
#define DOUBLE_BIAS 1023

とかあって現状VAXで動かんっぽい、それさえ無ければlibcのgdtoa由来のdtoaとstrtodなんぞ捨ててこいつ突っ込んじまうんだけど(まぁでもLC_NUMERICにも対応してなくてそこいらの作業もせんとならん)。

ちなみにライセンスはBoostとApache2の選択式なので前者選べばほぼgdtoaと同等(最近だと0条項BSDLというのかな、NだとHistorical Licenseと呼んでた記憶があるが)なので、そこで困ることは無い。

まぁラッパー書いてVAXだけgdtoa使うとか、いっそのことVAXサポート消すでもいいんだけどね、そもそも俺の家の押入れにはVAXは積まれてないし…

[オレオレN6] 続・gdtoa捨てたい

ふーん SwiftDtoaねぇ、 このプルリクしか情報は無いのでよく判らんけど、とりあえずのネックはApache2ライセンスなことか。

[SCM/BTS] BitBucket重いよ

ここしばらくBitBucketがクソ遅いのだが、くらしあんしんAtlassianなんかアカンのかな。 まさか先日の「タスク管理ツールTrelloから情報流出(と報道されるが単に愚者が自分で全世界に公開してるだけ)」とかいう日本人にインターネット与えるのは早過ぎた案件はさすがに関係ないとは思うが。

ワイが如何にしてGitHubをやめてBitBucketと水爆を愛するようになったかというと、まず俺のデフォルトアイコンを ソンブレロを被ってポンチョ着たオッサンが両手広げて股間を光らせてるようなアバターにするのを止めろ(被害妄想によるロールシャッハ検査症例)。

それと普通にUIの酷さに耐えられないのだよな、そもそもトップページに草生やすってのが嫌いなのと、フォローとかいいねとかSNS要素とか開発に無関係なものが多過ぎてワイには不向きちゅうとこや。 まぁBitBucketもUIが何年か前に改悪されてクッソ重くなっとるしSnippetsがリンクを知らないトップから辿れなくなったりしてだいぶ不満は溜まってるじゃんアゼルバイジャンなんで、他にいいとこあったらそっちに移住したくはある。

まぁ自分のコードを発見してもらいたい向きにはGitHub一択なのだろうけど、老いさらばえてヘイヘイマイマイ隠遁生活を送るワイにはBitBucketは静かで大変にありがたい。 タロットカードの「隠者(The Hermit)」の逆位置いいですよね、おっこいつもう一枚「愚者(The Fool)」の逆位置も引いたぞ!

2021/04/30(Fri)

[ソースコード考古学] glibcとPOSIX:2001とのstrerror_r(3)のプロトタイプの混乱について

kbkさんとこより、 strerror_r(3)ネタ。

このstrerror_r(3)の混乱については、そもそもエラーメッセージって元はV7 UNIXにおいては sys_errlist(3)つって、文字列定数の配列から直接拾ってくるもんだったということを知っておくべき。

例としてエラーメッセージを標準出力に表示するする関数である perror(3)のソースコード、抜粋するまでもなく短いから全文でいいな!おのれSCO!訴えるなら訴えてみろ!

/*
 * Print the error indicated
 * in the cerror cell.
 */

int	errno;
int	sys_nerr;
char	*sys_errlist[];
perror(s)
char *s;
{
	register char *c;
	register n;

	c = "Unknown error";
	if(errno < sys_nerr)
		c = sys_errlist[errno];
	n = strlen(s);
	if(n) {
		write(2, s, n);
		write(2, ": ", 2);
	}
	write(2, c, strlen(c));
	write(2, "\n", 1);
}

エラーが起きた時に更なるエラーが発生することの無いよう、この"Unknown error"とsys_errlist配列にある静的なメッセージ以外を使うべきでは本来なかったのよね。

ところが4.4BSDに導入された strerror(3)において

なんて余計な拡張したのが失敗だったんだよな。

こっちはライセンス文が長いのでコード部だけ。

char *
strerror(num)
	int num;
{
	extern int sys_nerr;
	extern char *sys_errlist[];
#define	UPREFIX	"Unknown error: "
	static char ebuf[40] = UPREFIX;		/* 64-bit number + slop */
	register unsigned int errnum;
	register char *p, *t;
	char tmp[40];

	errnum = num;				/* convert to unsigned */
	if (errnum < sys_nerr)
		return(sys_errlist[errnum]);

	/* Do this by hand, so we don't include stdio(3). */
	t = tmp;
	do {
		*t++ = "0123456789"[errnum % 10];
	} while (errnum /= 10);
	for (p = ebuf + sizeof(UPREFIX) - 1;;) {
		*p++ = *--t;
		if (t <= tmp)
			break;
	}
	return(ebuf);
}

はい、"Unknown error"のケツに更にエラー番号を付け加えとりますね。

んでglibcはスレッドセーフ拡張としてstrerror_r(3)を発明するわけだけど、上記の*BSDのstrerror(3)仕様をそのまま拡張したので(libc5はそもそも*BSDのlibc由来だしな)

GNU 仕様の strerror_r() は、 エラーメッセージを格納した文字列へのポインターを返す。
返り値は、 この関数が buf に格納した文字列へのポインターか、 何らかの (不変な) 静的な文字列へのポインター、 のいずれかとなる (後者の場合は buf は使用されない)。
buf に文字列が格納される場合は、 最大で buflen バイトが格納される (buflen が小さ過ぎたときには文字列は切り詰められ、 errnum は不定である)。
文字列には必ず終端ヌル文字 ('\0') が含まれる。  

という「渡した静的バッファには何も書きこまれないケースの方が多い」という一見奇怪な仕様が生まれたわけ、でもstrerror(3)の実装の中身を知ってしまえば疑問に思うことは無いと思う。

ではなぜPOSIX:2001のstrerror_r(3)はglibc2とは異なるI/Fになってしまったのかというと、それは我々非英語民と ブタが悪いのです。

エラーメッセージだって国際化してほしいという要求により、例えば*BSDでは catgets(3)によるメッセージ文字列のうん国際化が突っ込まれたのよね、邪魔なのでifdefは削除した後のコード

extern char *sys_errlist[];
extern int sys_nerr;

char *
__strerror(num, buf)
	int num;
	char *buf;
{
#define	UPREFIX	"Unknown error: %u"
	register unsigned int errnum;

	nl_catd catd ;
	catd = catopen("libc", 0);

	errnum = num;				/* convert to unsigned */
	if (errnum < sys_nerr) {
		strcpy(buf, catgets(catd, 1, errnum, sys_errlist[errnum])); 
	} else {
		sprintf(buf, catgets(catd, 1, 0xffff, UPREFIX), errnum);
	}

	catclose(catd);

	return buf;
}

よってsys_errlistの文字列定数をそのまま使うことは無くなり、strerror(3)は常に静的なバッファにメッセージをコピーする仕様に変わったのだ。

ちなみに386BSDの頃はまだ4.4BSDのままなので誰がはじめた物語かというと やっぱりNお前か。 このjtc@って人はNのlibcの初期の国際化まわり、つまりsetlocale(3)なんかの実装を入れた人やね。

ちなみに386BSD(バハマドル)は本日の為替レートでJPY 42,027.68でございます。

まぁエラーメッセージを国際化するってのはエラーの上にエラーを重ねる可能性が高いので愚の骨頂なのだけども、時代が1994年ならしやーない。 おそらく当時の商用UNIXも同じような事やっとったからjtc@も作業したんだと思うけど、当時のことは1994年ってついこの間じゃんと思ってそうなお年寄りにでも聞いてください。

ということでPOSIX:2001においては常にエラーメッセージは静的バッファにコピーされることを前提にスレッドセーフ対応がされることになる。 なので

int strerror_r(int errnum, char *strerrbuf, size_t buflen);

というglibcとは異なるプロトタイプで仕様化されたということだ。

そもそもerrno(3)自体がなぁ、スレッドセーフという概念の無い時代の仕様のものを無理矢理スレッドローカル変数です!と誤魔化したり、やることなすこと後手後手でゴテゴテなのである。

はい、 なぜ localtime(3) の引数は time_t のポインタなのか?以来のソースコード考古学のお時間でした。 その時も書いたけど、Cの場合はどんなに理不尽と思える仕様があっても、それには必ず何らかの歴史的理由があるということだ。

それじゃバイバーイ。