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で管理してくだち!県北の土手でプルリク盛りあおうぜ。