- ベストアンサー
剰余演算した時の結果が正しく得られません
剰余演算した時の結果が正しく得られない時があります。 例えば 1020 を 100 で割った余りは 20 です。 これが 19 になる時があるのですが原因がわかりません。 正確には 10.20 を 100 倍したものを 100 で割った余りなのですが、 いずれにしても結果は 20 が正しいはずです。 調査のためにプログラムを作成してみました。 ア:1020÷100の余りを表示 イ:10.20×100÷100の余りを表示 ウ:増分値毎の余りを表示 <?php //ア:1020÷100の余りを表示 echo "ア:",1020%100,"<br>"; //イ:10.20×100÷100の余りを表示 echo "イ:",10.20*100%100,"<br>"; //ウ:増分値毎の余りを表示 $a=array(0.05,0.10,0.01,0.02,0.04); foreach($a as $aa){ echo "増分値:$aa<br>"; for ($i=10.00;$i<=10.30;$i=$i+$aa){ $j=$i*100; $k=$j%100; echo "$i,$j,$k<br>"; } } ?> 結果は以下のようになりました。 ア:20 イ:19 増分値:0.05 10,1000,0 10.05,1005,5 10.1,1010,10 10.15,1015,15 10.2,1020,20 10.25,1025,25 増分値:0.1 10,1000,0 10.1,1010,10 10.2,1020,19 10.3,1030,30 増分値:0.01 10,1000,0 10.01,1001,1 10.02,1002,2 10.03,1003,2 10.04,1004,3 10.05,1005,4 10.06,1006,5 10.07,1007,6 10.08,1008,7 10.09,1009,8 10.1,1010,9 10.11,1011,10 10.12,1012,11 10.13,1013,12 10.14,1014,13 10.15,1015,14 10.16,1016,15 10.17,1017,16 10.18,1018,17 10.19,1019,18 10.2,1020,19 10.21,1021,20 10.22,1022,21 10.23,1023,22 10.24,1024,23 10.25,1025,24 10.26,1026,25 10.27,1027,26 10.28,1028,27 10.29,1029,28 10.3,1030,29 増分値:0.02 10,1000,0 10.02,1002,2 10.04,1004,3 10.06,1006,5 10.08,1008,7 10.1,1010,9 10.12,1012,11 10.14,1014,13 10.16,1016,15 10.18,1018,17 10.2,1020,19 10.22,1022,21 10.24,1024,23 10.26,1026,25 10.28,1028,27 10.3,1030,29 増分値:0.04 10,1000,0 10.04,1004,3 10.08,1008,7 10.12,1012,11 10.16,1016,15 10.2,1020,19 10.24,1024,23 10.28,1028,27 アは正しいのですが、イは正しくありません。 増分値:0.05の時はすべて正しく、 増分値:0.1の時は 1020 の時だけ 19 で正しくありません。 他の増分値では 1003 以降が正しくありません。 ア、イの結果の違いも不思議ですが、 増分値:0.01,0.02,0.04の時が 1003 以降がすべて正しくないのに、 増分値:0.1の時は 1020 以降ではなく 1020 のみなのも不思議です。 PHPのバージョンは「5.2.5」と「4.4.8」で動作確認しましたが、 どちらも同じ結果でした。 小数値を整数になるよう位上げしたものの剰余演算は、 こうなってしまうものなのでしょうか。 もともとの話は、 「10時20分」と言う時刻データが「10.20」と記録されていて、 分の部分を取り出すために100倍して100の剰余を求めていたところ、 結果が「19」になるので気づきました。 しかも、この現象が発生するのは、 「8.03」「8.04」「8.20」「9.03」「9.04」「9.20」 「10.03」「10.04」「10.20」だけで、 他の時刻(他の時、他の分)では発生しません。 全部の時刻で調べてはいませんが、 7時以前や11時以降でのこれら共通の分では正しい結果がでます。 調査プログラムの増分値では「10.03」以降が正しくなかったりしますが、 もともとの話のデータでは「10.05」は正しいのです。 これには何かわけがあるのでしょうか。 また、正確な剰余、あるいは、この場合の「分」を求める方法がありましたら、 何かしらのアドバイスをいただきたく思います。 よろしくお願いします。
- みんなの回答 (4)
- 専門家の回答
質問者が選んだベストアンサー
>>・・あるいは、この場合の「分」を求める方法がありましたら 丸め誤差の話題に気を取られて 質問の本質を見失っていました。 元の値が時間と分かっているのなら、回りくどい計算は不要な 下記方法が一般的かな。 元の時刻(10.20)が文字列なら <?php $x='10.20'; list($h,$m) = sscanf($x,"%d.%d"); echo "${h}時${m}分"; ?> 元の時刻(10.20)が少数点の数値なら、 <?php $x=10.20; list($h,$m) = sscanf(sprintf("%01.2f",$x),"%d.%d"); echo "${h}時${m}分"; ?>
その他の回答 (3)
- mpx
- ベストアンサー率71% (149/209)
>> また、正確な剰余、あるいは、この場合の「分」を求める方法がありましたら、 >> 何かしらのアドバイスをいただきたく思います。 原因は既に理解されているようなので、解決案だけ提示します <?PHP echo "イ:",round(10.20*100)%100,"<br>"; ?>
お礼
ご回答ありがとうございます。 round()で丸めればよかったのですね。 試しに $j=round($i*100); とすると、 増分値:0.01で 0:00 から 23:59 まで、 すべて正しい結果がでました。 2進数の近似値の誤差を調整するためのround()ではないとは思いますが、 この範囲では正しいようですし、他でもたくさん使えそうですから、 動作確認しながら、活用していこうと思います。 ありがとうございました。
循環小数ってのがあるね。例えば、10÷3は、循環小数になって、10進法では正確な値を表現できない。コンピュータは「永遠の桁」なんて扱えないから、どこかで切り捨てて値を扱うしかない。だから、10÷3のような循環小数は、どうしても正しい値が扱えなくなってしまい、どこかで「近似値」となる。 プログラミングの場合、重要なのは「内部では、数値は2進数で扱われている」ということ。これが、実は意外に問題となってくる。なぜかっていうと、「0.1」ってのは、2進数では循環小数になるんだよ。つまり、10進法の0.1(1÷10)という値は、2進法では永遠に割り切れない小数になってしまうわけだ。このため、0.1は途中で値を切り捨て、近似値としてしか扱うことができない。 というわけで、プログラミングにおける実数(浮動小数)は、常に「近似値である」ということを考えて扱わないといけない。例えば、10.20という実数でなく、"10.20"というテキストの値として保管をしておき、必要に応じてドットでテキストを分割してからそれぞれを整数値にキャストして計算をしたらどうなるだろうか。整数値ならば、少なくとも浮動小数における循環小数の問題はないから。
お礼
ありがとうございました。
補足
ご回答ありがとうございます。 なるほど、ご説明していただいた内容はよくわかりました。(ただのわかったつもりかも知れません) 加えて、ANo.1のアドバイスから2進数の小数も少しだけ勉強してまいりました。(ほんの少しだけです) ところで、少しばかり疑問が残っています。 よろしければ、もう一度、質問にあげた数値の羅列で増分値の部分をご覧になってください。 ($i)時刻(整数部:時、小数部:分)、($j)時刻を100倍したもの、($k)$iを100で割った余り、を表示しています。 $i、$jの値としては、増分値に関係なく、10.10は10.10であり、1010は1010であり、10.20は10.20であり、1010は1010だと思います。 なのに、増分値が0.05の時は10.10も10.20も剰余が正しく、増分値が0.1の時は10.10は正しく10.20は正しくなく、他の増分値では10.10も10.20も正しくありません。 $jを浮動小数点でコンピューターが処理していた場合、実際に2進数ではどうなるのかはわかりませんが、いずれの場合も結果が同じになると思うのです。 それがなぜ、バラバラな結果になるのかがわかりません。 コンピューターが処理した近似値にばらつきがあるものなのでしょうか。 それなら処理するたびに不規則な結果が出てもおかしくないと思うのに、常に実行結果は同じです。 なんらかの規則があるにしても、0.05ずつ加算したか、0.1ずつ加算したかで変わるものなのでしょうか・・・。 とここまで書いて、ふと気づきました。 加算する0.05や0.1も近似値で処理され、近似値+近似値でさらに余計な誤差が出ている、と言う事ですね。 PHPが出力する結果を、$i=10.20としか見ていませんでしたが、$i=10.00+0.05+0.05+0.05・・・と$i=10.00+0.1+0.1+0.1・・・の違いがあると言う事ですね。 と言っても、それでしか確認できないので同じだと誰もが思い勝ちだとは思いますが、PHPも$i=10.20を表示した時に剰余の結果が「19」になるような近似値なら、$iも近似値として「10.19」を表示してくれたらいいのにね、と思いました。 解決方法としては、言われたように、「.」で分割して処理する事にしてみます。
- SAYKA
- ベストアンサー率34% (944/2776)
答え:小数だから。 解法:どこかで整数化する http://www.google.com/search?lr=lang_ja&q=2%E9%80%B2%E6%95%B0%20%E5%B0%8F%E6%95%B0%E3%81%AE%E8%A1%A8%E7%8F%BE
お礼
ありがとうございました。
補足
アドバイスありがとうございます。 いただきましたURLから、 「2進数 小数の表現」については勉強してみます。 整数化については、PHPでの整数化の方法がわからなかったので、 intval()で試してみましたがうまくいきませんでした。 どのような方法で整数化すればいいのでしょうか。
お礼
この場を借りましてご回答いただきましたみなさまにお礼を申し上げます。 「.」で区切る方法や、sscanfとsprintfの併用の方法のどちらでもうまくできました。 実際に、開発プログラムの各所でそれぞれを組み込んでいましたが、別の問題が発生したため、MySQLへ時間をDOUBLE型で記録していたのを、TIME型にするよう仕様変更されてしまいました。 TIME型ですと、秒を除き時分だけの書式で出力すればいいので、演算などして誤差に迷う事もなかったです。 sprintfの謎の方は、改めて勉強しようと思います。 今回の質問で色々と勉強になりました。 ありがとうございました。
補足
2度目のご回答ありがとうございます。 元の時刻は、MySQLのDOUBLE型から参照したデータなので、たぶん数値です。 10時20分の場合は 10.2 で参照されていましたので、後者のソースでうまくいけました。 sscanfは「PHPマニュアル」を読んでなんとなく理解できました。 http://php.benscom.com/manual/ja/function.sscanf.php sprintfの方が「PHPマニュアル」を読んでも少しわからない点がありました。 http://php.benscom.com/manual/ja/function.sprintf.php 「%01.2f」の「.2f」は、浮動小数点として小数部を2桁として出力する(10.2を10.20にする)と言う意味だと理解しましたが、「01」の部分がなぜ「01」なのかがわかりませんでした。 整数部を1桁かと思ったのですが「03」にしても「010.20」になるわけではなさそうですし、「01」で「0.20」になるわけでもありません。 「01」を「1」にしても変化がなさそうですし、なぜ「01」と記述するのかがマニュアルからは読み取れませんでした。 よろしければ、このところも教えてください。