2.3 * 100,000 のナゾ2008年03月27日 04時01分12秒

ちと意味わかんないんすけど

今日(あけてるので昨日か)の昼間、「PHPで 2.3を10万倍して int に cast したら誤差発生」という現象に見舞われた。最初はまたPHPのクサレ素敵仕様かバグかと思ったが、試してみたところ少なくともJavaScriptとC#(1.0)でまったく同様の結果になった。なので他の言語でもある話なのかも。

ちょっと理屈がわかんないんだよなー。

まずはすなおに出力

まずはこれ。

C:\Documents and Settings\dara-j>php -r "var_dump(2.3 * 100000);"
float(230000)

C:\Documents and Settings\dara-j>

まぁ、普通です。

キャストしてみる

んで、これをintにcastしてみる。

C:\Documents and Settings\dara-j>php -r "var_dump((int)(2.3 * 100000));"
int(229999)

C:\Documents and Settings\dara-j>

...えーっと...

もっともマニュアルの「浮動小数点」の説明を見ると、

こうなる理由のひとつとして、「有限小数に変換できない分数がある」 という事実があります。たとえば 1/3 を小数で表そうとすると 0.3333333. . . となります。

よって、小数の最後の桁を信用してはいけませんし、 小数が等しいという比較を行ってはいけません。より高い精度が必要な場合には、 任意精度数学関数または gmp 関数を代わりに使用してください。
とかあるので、まぁ精度の問題かしら、と思えなくもないのだが。

ちなみにJSでも

alert( Math.floor( 2.3 * 100000 ) );
で「229999」になる。

これは納得いかないんだが

こんどは1万倍と100万倍をやってみると、なんだか納得いかない。

C:\Documents and Settings\dara-j>php -r "var_dump((int)(2.3 * 10000));"
int(23000)

C:\Documents and Settings\dara-j>php -r "var_dump((int)(2.3 * 1000000));"
int(2300000)

C:\Documents and Settings\dara-j>
...なぜに意図どおりの結果が??いや、これで正しいんだけど。ってことは100,000だけ仲間はずれっすか。

さらに納得いかないんだが

よし、じゃあ10万倍が特別なのはよしとしましょう。でも、これは?

C:\Documents and Settings\dara-j>php -r "var_dump((int)(2.3 * 10 * 10000));"
int(230000)

...えええっと... えええっと...

内部での実行順序の違いってのがあるのかも知らんけど、* 100000と違う結果になるのはちとひどくないか?

この現象って、冒頭でも書いたけど別にPHP固有の現象ではなく、少なくともJavaScriptとC#ではまったく同じ結果になる。ちなみにここまでのコード例ではcastしてるけど、floor()使っても同じ結果になる。

10進型が使えりゃ気にしないのだが

実際、2.3 * 100000 で誤差がでると困るのでなんとか対応せにゃならなかったんだけど、さすがに * 10 してから * 10000 って、なんだかバッドノウハウくさいので、結局いったんstringを経由させてからintにキャストすることにしたんだけども。

こんな感じ。

function hoge($v) {
	// 元はこんな感じ
	// return (int)( $v * 100000 );
	
	// それをこんな感じに
	$v = $v * 100000;
	return (int)( "$v" );
	
	// なんというか、なんでこんなことを...
}

...なんだかなぁ。10進型(decimal)が使えればこんなことしなくてすむんだけどね。

あ、もちろんBCMath任意精度数学関数ってのがあるのは知ってるし、このケースで bcmul() なら問題ないことも確認してるんだけど、今回は実行環境にlibbcmathがなかったので。

オマケ

さらにさらに、これどうよ?

C:\Documents and Settings\dara-j>php -r "var_dump((int)(2.3 * 100 * 1000));"
int(229999)

C:\Documents and Settings\dara-j>php -r "var_dump((int)(2.3 * 1000 * 100));"
int(230000)

...ここまでくると、もはやどうでもいいや

おクスリトラバを強調するGMスクリプト2008年03月27日 12時53分48秒

こないだのスパムがらみエントリでも書いたが、アサブロのトラックバック管理画面で「spam」指定しておくとアサブロ側で情報を収集してくれるっぽいので即削除をやめてspam指定をするようにしてみた。

が、トラックバック管理画面はデフォルトで 10件 / 1ページ の表示なのであっという間に画面はspamだらけになり、管理しづらい。

なので、せめてもう少し見やすくしようとGreasemonkeyでユーザスクリプトを書いてみた。グリモンでしか確認してないが、ほかのブラウザのユーザスクリプトにも移植できるかも。そうでもないかも。

以下のスクリプトを、実行対象のページに「http://www.asablo.jp/app?cmd=edit_tb*」を指定しておくと、トラックバック一覧を表示したときに指定キーワードに一致する行の文字を赤(すでにspam指定済みの場合は暗い赤)で強調する。

var K = function(x) { return x; }

Object.extend = function(destination, source) {
	for(var key in source) destination[key] = source[key];
	return destination;
}

var $break = {};
var $continue = {};
var Enumerable = {
	each : function(iterator) {
		var i = 0;
		try {
			this._each( function(value) {
				try {
					iterator(value, i++);
				} catch(e) {
					if( e != $continue ) throw e;
				}
			} );
		} catch(e) {
			if( e != $break ) throw e;
		}
	},
	find : function(iterator) {
		var result = null;
		this.each( function(value, index) {
			if( iterator(value, index) ) {
				result = value;
				throw $break;
			}
		} );
		return result;
	},
	findAll : function(iterator) {
		var results = [];
		this.each( function(value, index) {
			if( iterator(value, index) ) results.push( value );
		} );
		return results;
	}
}
Array.prototype._each = function(iterator) {
	for(var i = 0, l = this.length; i < l; i++) iterator(this[i]);
}
Object.extend( Array.prototype, Enumerable );

Array.from = $A = function(list) {
	Object.extend( list, { _each : Array.prototype._each } );
	return Object.extend( list, Enumerable );
}

document.getElementsByClassName = function(className) {
	return $A( (arguments[1] || document).getElementsByTagName("*") ).findAll( function(ele) {
		var classes = ( ele.className || "" ).split(" ");
		return classes.find( function(c) { return c == className; } );
	} );
}

/* spam認定キーワード。うまく引っかからんやつもある */
var ng_words = /(drug|buy|price|cheap|tramadol|medication|side\seffects|hydrocodone|oxycodone|zithromax|phentermine)/ig;

$A(document.getElementsByClassName("list")[0].getElementsByTagName("tr")).findAll( function(tr) {
	return ng_words.test( tr.innerHTML );
} ).each( function(tr) {
	var state = tr.getElementsByTagName("select")[0].value;
	Object.extend( tr.style, {
		color : state == "spam" ? "brown" : "red"
	} );
} );

Enumerableだとか、prototype.jsの機能を一部実装してるのでちょっと長いが。

追記

ちなみにトラックバック一覧から絞込みや並べ替えやっちゃうと「http://www.asablo.jp/app?cmd=edit_tb*」にマッチしないのでこのスクリプト無効になっちゃいます。

もう少し使えるフィルタを実装してほしいなぁ > アサブロ