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)...ここまでくると、もはやどうでもいいや。
Delegateクラス ― 2008年03月11日 03時00分03秒
string | array が気持ち悪いので
PHPで動的な関数・メソッド呼び出しを行う場合、関数名を示す文字列を使うか、配列にオブジェクト(or クラス名)とメソッド名を格納して渡す。マニュアル中で「callback」という呼称で呼ばれている擬似的な型なやつだ。
これ、短いスコープで使う場合はまぁそれでもいいかと思えるんだけど、大きいスコープの変数に格納した場合、本当に適切かどうかの判断がすぐにつかなくて気持ち悪い。$callbackとかって変数にint放り込んでも許容されるので実行時にチェックするのって面倒くさいじゃん。is_callback()とかあるわけじゃなし。
だもんで、.NETのDelegateのような感じで、あらかじめラッピングしちゃえば、関数やメソッドが実在するかはともかくとして「呼び出し可能なオブジェクト」として保持しておけるだろうと思って、こんなクラスを作ってみた。まんま「Delegate」というクラス。使いどこ、少なそうなんですが。
<?php
class Delegate {
/**
* @static
*
* 指定の引数がコールバック形式かを判別する
*
* @param mixed $callback コールバック形式かを判別する引数
* @return bool $callbackがコールバック形式の場合はtrue、それ以外はfalse
*/
public static function isCallback($callback) {
// 文字列か配列のみ許容
if( is_string($callback) || is_array( $callback ) ) {
if( is_array($callback) ) {
if(
// 配列長が2ではないか
count($callback) != 2 ||
// 第一要素がオブジェクトまたは文字列ではないか
( ! is_object($callback[0]) && ! is_string($callback[0]) ) ||
// 第二要素が文字列ではない場合は
! is_string($callback[1])
) {
// コールバックではない
return false;
}
}
return true;
} else {
// 文字列・配列以外はコールバックではない
return false;
}
}
/**
* @access protected
*
* コールバックオブジェクト
*
* @var string|array
*/
protected $_callback;
/**
* Delegateの新しいインスタンスを初期化する
*
* @param mixed $obj コールバック関数名、クラス名またはオブジェクトインスタンス
* @param string|null $method $objがクラス名かオブジェクトインスタンスの場合はメソッド名
*/
public function __construct($obj, $method = null) {
$callback = ( is_string($obj) && empty($method) ) ?
// $objが文字列で$methodがnullの場合は関数名指定
$obj :
// それ以外はメソッド指定
array( $obj, "$method" );
// コールバック形式かをチェック
if( ! self::isCallback($callback) ) {
throw new Exception( '引数がコールバック形式ではありません' );
}
$this->_callback = $callback;
}
/**
* コールバック呼び出しを実行する
*
* @param [mixed parameter [, mixed ...]] 可変長パラメータ
* @return mixed コールバックの実行結果
*/
public function invoke() {
return $this->invokeArray(func_get_args());
}
/**
* パラメータを配列で指定してコールバック呼び出しを実行する
*
* @param array $params コールバック関数・メソッドに指定する引数の配列
* @return mixed コールバックの実行結果
*/
public function invokeArray(array $params) {
return call_user_func_array($this->_callback, $params);
}
}
ま、元の発想のまま「Delegate」なんて名前にしてるけど、別に「Callback」クラスとか「MethodInvoker」とかでもいいです。
ドキュメントコメントが適当なのは勘弁してください。ちゃんと調べよう、そのうち。
「Delegate」とはいいつつも
当然ながら、.NETのDelegateクラスとはちみっと違う。まぁ、あちらは明示的にDelegate/MulticastDelegateから派生型作れるわけではなく、構文からコンパイラが勝手に派生させるっていう特殊な型なんだけども。
コールバックの引数でバインドするわけではないので、invoke()するときの引数に関してなんの保証もないので、ランタイムエラーになりやすいかも。
使い方
コンストラクタの引数に、「callback」型を指定する。
例えば関数の場合ならその関数名の文字列。クラスメソッド(スタティックメソッド)だったら「array( 'ClassName', 'methodName' )」のように文字列を2つ、インスタンスメソッドなら「array( $obj, 'methodName' )」みたいな感じ。
んで、格納したコールバックを呼び出す場合はinvoke()メソッドを使う。引数は可変長で受け取るので、元の関数(またはメソッド)に与える引数と同じような引数を与えてやる。元の関数(メソッド)が値を返すならinvoke()の戻りで受け取れる。
サンプル
以下はZend_Logの各種ログ出力メソッドをDelegateに収めてループで呼び出しするサンプル。
<?php
require_once 'Delegate.php';
require_once 'Zend/Log.php';
require_once 'Zend/Log/Writer/Stream.php';
$log = new Zend_Log( new Zend_Log_Writer_Stream('php://output') );
$list = array(
// Zend_Log::emerg()を実行するDelegate
array( 'name' => 'EMERG', 'callback' => new Delegate( $log, 'emerg' ) ),
// Zend_Log::err()を実行するDelegate
array( 'name' => 'ERR', 'callback' => new Delegate( $log, 'err' ) ),
// Zend_Log::warn()を実行するDelegate
array( 'name' => 'WARN', 'callback' => new Delegate( $log, 'warn' ) ),
// Zend_Log::debug()を実行するDelegate
array( 'name' => 'DEGUB', 'callback' => new Delegate( $log, 'debug' ) ),
);
// フィルタ設定なしで実行 → すべてのログが出力される
foreach( $list as $config ) {
$callback = $config['callback'];
$callback->invoke( "name = {$config['name']}" );
}
// フィルタ条件を変えてもう一度ループ実行
// フィルタでZend_Log::ERR以下のプライオリティをブロックする
echo "Zend_Log::ERRでフィルタ指定\n";
$log->addFilter( new Zend_Log_Filter_Priority( Zend_Log::ERR ) );
foreach( $list as $config ) {
$callback = $config['callback'];
$callback->invoke( "name = {$config['name']}" );
}
このサンプルではDelegateのリストをベタでコーディングしちゃったけど、Zend_Logのプライオリティ名の配列を元にループ処理で組み立ててもよいかも。
しかし、微妙か。
こんな感じで使うんだけど、微妙。独自クラスにしちゃってるので当然array_filter()とかのコールバックにはそのまま使えないし($_callbackを返すgetterでも実装すればいいのだが)、コンストラクタで引数の型はチェックしているけど、別に関数・メソッドの実在をチェックしてるわけじゃないので、ランタイムでエラーになるかもしれないし。第一、ちょっとした用途なら素直にcall_user_func()とか使ったほうが手っ取り早いし。
それでも個人的には上のサンプルみたいに同じ形のメソッドを呼び出すちょっとしたテストに使ったりとかでそれなりに便利には使ってるんだけどね。
多様的にメソッド呼び出し行うなら、インターフェイス(か抽象クラス)から派生・実装させるのが本筋なんだろうけど、これなら組み込み関数も自作クラスのインスタンスメソッドもシグニチャが同じものなら同じように代替させられるので、その点だけはメリットかな、と思うんだけど。でもやっぱりcall_user_func()するのがPHPの流儀かしら。
gmmktime()の挙動の違い ― 2008年03月01日 00時43分08秒
ここしばらくgelatoがマイブームなのでこにょこにょといじっていたりするのだが、JSAスクリプトからPOSTした場合とadminページからPOSTした場合で時差がでることに気がついた。
自宅やら会社の実験サーバやら開発用PCやら何箇所かgelatoを入れているのだが、このうちあるホストだけ時差が発生する。 調べてみると時差の源は、gelatoが新規エントリを追加する際のタイムスタンプを「gmmktime」で生成している部分らしかった。
このあたりはまたgelatoネタとして別エントリにする予定なのだが、なんで違いが出たり出なかったりするんだろうかといろいろ試したところ、PHP4とPHP5で挙動が違う場合があることが判明した。(ひょっとして有名なネタかしら?)
問題のgelatoのコードでは
gmmktime()と、パラメータなしで使用している。ここがミソ。
こんな検証コードを書いてみた。
<?php
$current_time = mktime();
$d = array();
foreach( split(',', 'H,i,s,m,d,Y') as $key ) {
$d[ $key ] = date( $key, $current_time );
}
foreach( array(
array(
'name' => 'mktime()',
'value' => $current_time
),
array(
'name' => 'gmmktime()',
'value' => gmmktime()
),
array(
'name' => 'gmmktime(mktime())',
'value' => gmmktime(
$d['H'], $d['i'], $d['s'], $d['m'], $d['d'], $d['Y']
)
)
) as $conf ) {
$date = date('Y-m-d H:i:s', $conf['value']);
echo "{$conf['name']} --> $date\n";
}
?>
mktime()で基準のタイムスタンプを取得し、
- パラメータなしのgmmktime()
- 基準タイムスタンプを元にしたgmmktime()
これをPHP5で実行するとこんな感じ。
-sh-2.05b$ php test.php mktime() --> 2008-03-01 00:22:57 gmmktime() --> 2008-03-01 00:22:57 gmmktime(mktime()) --> 2008-03-01 09:22:57同じコードをPHP4で実行するとこんな感じ。
-sh-2.05b$ php4 test.php mktime() --> 2008-03-01 00:23:01 gmmktime() --> 2008-03-01 09:23:01 gmmktime(mktime()) --> 2008-03-01 09:23:01
これを見ると、パラメータなしでgmmktimeを実行すると、PHP4では現在のローカル時刻が与えられたものとして動作し、PHP5ではmktime()と同じ動作をすることがわかった。
これ、マニュアルには特に何も書いてないので、もうちょっと細かいバージョンの違いがでるのかもしれない。
mktime()はパラメータなしの場合に「現在時刻を与えられたもの」として振舞っているので、どっちかっていうとPHP4の挙動のほうが正しい感じはするんだけどね。
しかし、普段はZend_Dateばっかり使っているのであんまり役に立たない発見な気が。
gelatoを試してみたり。 ― 2008年02月25日 21時32分03秒
久々の更新なのに全然 Zend じゃないし。
前々から興味があったのだ
しばらく前にMOONGIFTで紹介されていて、激しく興味があったオープンソースCMS「gelato」。これ、要するにtumblrクローン。
まぁ、まだバージョンが0.95なので鋭意開発中、って感じなんだろうけど、tumblr感覚でローカルにぽこぽこPOSTできるとちょっといいよな、と思って試しにインストールしてみた。
インストール
インストールはこちらの記事を参考に。つか、まんまです。PHPのアプリケーションインストールしたことあるならほとんど迷わないと思うけど、install.phpでDBがらみの情報入力させる割りに先に「config.php」にDB設定書き込んどかなきゃなんなかったのがイマイチ腑に落ちないけど。
それから先ほどのLiner Noteさんとこから、日本語化ファイルをダウンロードできるのでそちらも忘れずに。ありがたや、ありがたや。
使ってみる
管理画面はtumblrのダッシュボードのようにはいかず、ちょっぴり使いづらいんだけど、まぁ普通にPOSTできるわな。
ただ、設定でタイムゾーン選択できるんだけど、POSTに上手く反映されてないような。
あと、わかりづらいけど一応ブックマークレットもついてます。図の赤枠のところがリンクになってて、これをブックマークすれば一応ブックマークレット。
- 普通にクリックするとlink。
- なにか選択してると、選択部分のテキストを本文にして、regular post。
- 画像を表示しているとphoto。
JSActionのスクリプトを書いてみる
んで、実はこっからが本番。そもそもローカルで使いたい動機は、社内に設置している内向きのブログ書くときに、スクリーンショットとかを手間なくホストしときたいなーってとこだったわけ。なので、
- まずはJSActionでphoto直行
- その次はcapture.tumblr.jsをローカルgelato対応に
capture.tumblrはちとソース読むのが大変そうなので、まずはフォームの構造を調べがてらJSActionスクリプトを作ってみた。こんな感じ。
// post photo to gelato
new function(base) {
function h2q(h) {
var buf = [];
for(var key in h) {
if( typeof(h[key]) != "function" ) {
buf.push( encodeURIComponent(key) + "=" + encodeURIComponent( h[key] == null ? "" : h[key].toString() ) );
}
}
return buf.join( "&" );
}
var url = window.location.href;
var title = window.title || window.document.title || window.location.href;
var img = _jsaCScript.context.target;
if( ! /^img$/i.test( img.localName ) ) return;
var xhr = new XMLHttpRequest();
xhr.open( "post", base, false );
var hash = {
title : title,
url : img.src,
date : parseInt( new Date().valueOf() / 1000 ),
type : 2,
description : "<a href=\"" + url + "\" title=\"" + title + "\">(via: " + title + " )</a>",
btnAdd : "Create post"
};
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send( h2q( hash ) );
} ( "http://localhost/gelato/admin/index.php" );
あ、gelatoってGPL v2なんだけど、こういうスクリプトってGPLに引きずられるのかなぁ、と心配になったのでこのスクリプトも「GPL v2」ということにします。
このスクリプトをJSActionのimageフォルダに保存して、一番最後の関数呼び出しに使ってる引数にPOST先のURLに変更して使う。この例では「http://localhost/gelato/」がgelatoのURLね。
実はちょっと注意点があって、POSTデータの「description」(linkのcaption)に元ページのURLをanchorでくっつけてるんだけど、gelatoはdescriptionをstrip_tags()しちゃうのでこのままだとリンクにならない。
そのままでもいいやってなら別になにもする必要はないけど、admin/index.phpの72行目あたりの
$_POST["description"] = strip_tags($_POST["description"]);を
$_POST["description"] = $_POST['description'];に変更すればHTMLタグ使い放題。あー、自己責任でおながいします。
あ、あとほんとに最低限の実装なので、例えばflickrで「all sizes」がある場合はそっちへ飛んでからじゃないと上手くPOSTできません。あとPOSTエラーも特に何もしてないので、「POSTされてなかったらなんかエラーでたんだなー」といった感じで使ってください。使う人いるかわからんけど。
その他の情報
- 一応read apiも実装してるっぽいけど、コメントアウトの仕方が悪くてエラーになったり、そのあたり解消してもまともに情報返してくれなかったり。
- なぜか編集ができない。新規POST扱いになる。
- photoはlightbox使って表示するんだけど、上手く表示できないときがある。
- lightboxとかで使ってる画像リソースのパス解決が上手くいかない場合があるっぽい。
- アカウントを複数作れるけど、アカウント別に表示できたりするわけじゃないし、他のアカウントのPOST削除できちゃうし。
- svnから最新ソースをチェックアウトしてみたけどクラスだか関数の重複定義とかでエラーがでて、もうぐだぐだ。
次はがんばってcapture.tumblr.jsの改造だ。多分。
$なし変数のカラクリと数値変換のお話 ― 2007年12月18日 03時44分58秒
しつこくPHPのクサレ独特の仕様についてのエントリ。だって予想外の動作ばっかりなんですもの。
できそこない変数が文字列として解釈されるワケ
昨日のエントリの「$を付け忘れた変数が文字列として解釈される」についてだが、k_37to氏のはてブコメントに解説があった。
この場合「ijk」は定数としてパースされ、定数が存在しない場合は定数名の文字列として変換される。PHPのはまりやすい罠。定数か! 普段は慣習的に定数を大文字で記述してるので見落としていたが、なるほど定数名は定義上先頭に「$」が付かないこと以外は変数と同じだ。k_37toのブックマーク / 2007年12月17日(強調:dara-j)
そんで、マニュアルの「定数」の項を確認すると確かに
未定義の定数を使用した場合、ちょうどstringとして コールしたかのように(CONSTANT vs "CONSTANT")、 PHPはその定数自体の名前を使用したと仮定します。と書いてある。これで納得。つか、未定義の定数の仕様を許容するなよ。PHP: 定数 - Manual(強調:dara-j)
大きな数値の怖いお話
これも暗黙の型変換に関連するのだが、今度は数値。PHPSPOTさんの記事で紹介されていた記事より。
ここで上げられていた例は、2つの17桁の数値「11111111111111111」と「11111111111111112」が等値と判断されるという、なんとも理解しがたい現象。
詳しくはリンク先を見ていただくとして、要約すると
17桁の整数 → INTの範囲外なので自動的にFLOATに → FLOATの桁精度が足りなく丸め誤差発生 → 同じ値ってことで。ということらしい。
いや、数値なので精度による丸め誤差が発生するのはわかるんだけど、これに例の必殺技「==で比較すると整数に見なしちゃうぞ」が絡むと、
C:\Documents and Settings\dara-j>php -r "echo '11111111111111111' == '11111111111111112' ? 'equal' : 'not equal';" equal C:\Documents and Settings\dara-j>なんて、常識的なアタマではとても思いつかない奇ッ怪な結果が提示されたりする。回避するには文字列にキャストしてstrcmp()を使用するなんて、なんとも
結論
PHPは余計なお世話至れり尽せりの自動変換を施してくれるので、なるべく型を意識し、値の比較時は「===」による厳密な比較を行うか、文字列経由でstrcmp()を使うなどの工夫が必要、ってとこか。って、変数に型がないのに型を強く意識したプログラミングっておかしくないか?

最近のコメント