有限小数、循環小数、分数、無理数

2進数の循環小数に注意
数の概念
複素数実数有理数整数自然数整数
ゼロゼロ
負の整数負の整数
有限小数分数有限小数
循環小数循環小数
無理数無限小数無限小数
虚数虚数
長さや重さといった計測値は、小数で表すことが多い。小数を表すデータ属性には、固定小数と浮動小数がある。あらわせる数の範囲と誤差、とくに 2進数で循環小数になる場合に注意が必要だ。

サンプル・プログラム

固定小数

たとえば 8 ビットのデータを 2 つに分割し、5 ビットを整数部、3 ビットを小数部に充てるとする。ビット数が固定されているところから、固定小数と呼ぶ。
固定小数で 01101.011 は

01101.011 = 23 + 22 + 20 + 2-2 + 2-3 = 13.375


となる。
5+3 の固定小数では、整数部の範囲は +15~-16 で、小数部は 2-3 = 0.125 未満は扱うことができない(誤差になってしまう)。
データ幅を 64 ビットに増やしたとしても、固定小数の位置によって、その数量を計算するのにふさわしくない可能性がある。
このため、固定少数は、黎明期を除いてコンピュータで利用されることは無かった。

浮動小数

実数を、符号、仮数、指数の 3 つ組データで表現するのが浮動小数である。
たとえば -123.4 = -1.23×102 とあらわし、1.23 が仮数、2 が指数となる。
浮動小数は IEEE754 という国際規格で定められており、符号は 1 ビット、仮数は 23 ビット、指数は 8 ビットの合計32 ビットである。
表現可能な最小値は ±2-126 ≒ ±1.2×10-38、最大値は ±2127 ≒ ±3.4×1038 となる。32 ビット整数で扱える最大値が 2.1×109 であることを考えると、それよりはるかに大きな数を扱うことができる。
また、浮動小数は 3 つのデータから成ることから、簡単なデータ構造であるといってもいい。

名数の計算では単位を揃える必要があることは、すでに述べたとおりだ。たとえば長さの単位を SI 単位系であるメートルに統一すると、水素原子核の直径から宇宙の果てまでの距離まで、浮動小数の範囲で扱うことができる。
対象メートル
水素原子核の直径1.75×10-15
最新CPUの配線の幅1.4×10-8
インフルエンザウイルスの大きさ1.0×10-7
大腸菌の大きさ1.0~3.0×10-6
ヒトの毛髪の直径1.0×10-4
日本人成人男子の平均身長1.67×100
東京駅~新大阪駅5.526×106
成田空港~ヒースロー空港9.585×107
地球~月3.844×1010
地球~太陽1.496×1012
太陽~冥王星(平均距離)5.954×1013
最も近い恒星(プロキシマケンタウリ)までの距離4.02×1016
天の川銀河中心部までの距離2.65×1020
ソンブレロ星雲(M104)までの距離2.65×1023
宇宙の果てまでの距離1.31×1026
システムで扱おうとしている数が、浮動小数の範囲内に収まるかどうか注意が必要だ。SI 単位系は、SI 接頭辞(マイクロ、ミリ、キロ、メガ‥‥)を付けることで 10±3ずつ指数部の桁数を変えることができるから、範囲内に収まるような単位と接頭辞を選ぼう。

また、データ幅を 64 ビットに拡張した倍精度浮動小数も定義されている。
C や C#では、単精度を float、倍精度を double で型宣言する。
 符号仮数指数範囲
単精度1ビット23ビット8ビット±1.2×10-38~±3.4×1038
倍精度1ビット52ビット11ビット±2.2×10-308~±1.8×10308

数の精度

単精度浮動小数では 1.2×10-38 より小さな数が、倍精度浮動小数では 2.2×10-308 より小さな数を扱うことができない。つまり、これより小さい数は誤差となる。
測定値の場合は有効数字として桁数を使うことが多いが、浮動小数の場合は指数のビット数が精度を左右する。

循環小数

1 ÷ 3 = 0.333‥‥循環小数である。
このため、データ属性が浮動小数の場合、1 ÷ 3 × 3= 0.999‥‥≠1 ということが起こりうる。システム開発する際、処理系の制約を確認しておこう。

10進数では有限小数だが、2進数で循環小数になる場合もある。たとえば 0.4 がそうである。

0.4 = 2-2 + 2-3 + 2-6 + 2-7‥‥

0001: <?php
0002: //演算誤差
0003: $a = 6 - 5.6;
0004: $b = 0.4;
0005: echo  $a - $b;
0006: ?>

"dt02-04-01.php" は PHP による簡単な引き算だが、処理系によってはゼロにならない。

0001: #演算誤差
0002: a = 6 - 5.6
0003: b = 0.4
0004: print(a - b)

Python プログラム "dt02-04-02.py" でも同様に誤差が発生する。

0001: #include <iostream>
0002: #include <iomanip>
0003: #include <limits>
0004: using namespace std;
0005: 
0006: //演算誤差
0007: int main() {
0008:     float ab;
0009:     a = 6 - 4.6;
0010:     b = 0.4;
0011:     cout << setprecision(8) << setiosflags(ios::fixed) << a - b << endl;
0012:     return(0);
0013: }

一方、C++(g++でコンパイル)プログラム "dt02-04-21.cpp" では誤差が発生しない。
このように循環小数になると、数の精度の範囲内であっても演算誤差が発生する場合がある。とくにループの終了条件で循環小数が発生したりすると、システム設計は正しくてもループ回数が狂うといったバグの温床になりかねない。

分数

Ruby や Python は分数を扱うクラス Fraction が備わっている。

0001: #分数クラス
0002: from fractions import Fraction
0003: a = 6 - Fraction(56, 10)
0004: b = Fraction(4, 10)
0005: print(a - b)

先ほどの循環小数の計算を分数で行う "dt02-04-03.py" では、正しい答えが求まる。

PHPによる分数クラスの作成

PHP には分数を扱う機能が備わっていない。そこで、分数の四則計算を行うことができるユーザー・クラス pahooFrac を作成し、"pahooDataStructure.php" を  require_once  することで再利用できるようにした。次のメソッドを備えている。
メソッド機能
pahooFrac($n, $d)インスタンス生成。$nが分子、$mが分母。$n, $dは負数でも小数でもよい。
dec()分数を小数に変換する。
isFiniteDecimal()分数が有限小数かどうかが判定する。
printFrac($dec)分数表記を返す。$decがTRUEなら。有限小数の時は小数表記に変換する。
add($x, $reduce)分数クラス $x を加算する。$reduceがFALSEなら約分しない。
sub($x, $reduce)分数クラス $x を減算する。$reduceがFALSEなら約分しない。
mul($x, $reduce)分数クラス $x を乗算する。$reduceがFALSEなら約分しない。
div($x, $reduce)分数クラス $x を除算する。$reduceがFALSEなら約分しない。

 

0013: // pahooFracクラス =========================================================
0014: class pahooFrac {
0015:     var $error$errmsg;         //エラーフラグ,エラーメッセージ
0016:     var $numerator;              //分子
0017:     var $denominator;                //分母
0018: 
0019: /**
0020:  * コンストラクタ
0021:  * @param double $numerator   分子(小数可;初期値=0)
0022:  * @param double $denominator 分母(小数可;初期値=1)
0023:  * @return なし
0024: */
0025: function __construct($numerator=0, $denominator=1) {
0026:     $this->error  = FALSE;
0027:     $this->errmsg = '';
0028:     $this->numerator   = $numerator;
0029:     $this->denominator = $denominator;
0030:     $this->dec2frac();
0031: }
0032: 
0033: /**
0034:  * デストラクタ
0035:  * @param なし
0036:  * @return なし
0037: */
0038: function __destruct() {
0039: }
0040: 
0041: /**
0042:  * 符号は分子に寄せる
0043:  * @param なし
0044:  * @return なし
0045: */
0046: function __sign() {
0047:     if ($this->denominator < 0) {
0048:         $this->numerator *= (-1);
0049:         $this->denominator = abs($this->denominator);
0050:     }
0051: }
0052: 
0053: /**
0054:  * 小数を整数部と小数部に分解する
0055:  * @param double $a 小数
0056:  * @return array(整数部, 小数部)
0057: */
0058: function split_dec($a) {
0059:     $b = $a > 0 ? floor($a) : ceil($a);
0060:     $c = $a - $b;
0061:     return array($b$c);
0062: }
0063: 
0064: /**
0065:  * 分子・分母を整数に正規化する
0066:  * @param なし
0067:  * @return なし
0068: */
0069: function dec2frac() {
0070:     if (is_float($this->numerator)) {
0071:         list($a$b) = $this->split_dec($this->numerator);
0072:         $n = 0;
0073:         while (TRUE) {
0074:             list($c$d) = $this->split_dec($b);
0075:             if ($d == 0)    break;
0076:             $b *= 10;
0077:             $n++;
0078:         }
0079:         $this->numerator = $a * pow(10, $n) + $b;
0080:         $this->denominator *= pow(10, $n);
0081:     }
0082:     if (is_float($this->denominator)) {
0083:         list($a$b) = $this->split_dec($this->denominator);
0084:         $n = 0;
0085:         while (TRUE) {
0086:             list($c$d) = $this->split_dec($b);
0087:             if ($d == 0)    break;
0088:             $b *= 10;
0089:             $n++;
0090:         }
0091:         $this->denominator = $a * pow(10, $n) + $b;
0092:         $this->numerator *= pow(10, $n);
0093:     }
0094:     $this->reduce();
0095: }
0096: 
0097: /**
0098:  * 分数を小数に変換する
0099:  * @param なし
0100:  * @return double 小数
0101: */
0102: function dec() {
0103:     if ($this->denominator == 0) {
0104:         $this->error = TRUE;
0105:         $this->errmsg = 'pahooFrac: illegal denominator';
0106:         $res = NULL;
0107:     } else {
0108:         $res = $this->numerator / $this->denominator;
0109:     }
0110:     return $res;
0111: }
0112: 
0113: /**
0114:  * 最大公約数を求める(ユークリッドの互除法)
0115:  * @param int $m, $n 整数
0116:  * @return int 最大公約数
0117: */
0118: function gcdEuclidean($m$n) {
0119:     if ($n == 0)    return $m;
0120:     return $this->gcdEuclidean($n, (int)$m % $n);
0121: }
0122: 
0123: /**
0124:  * 最小公倍数を求める(ユークリッドの互除法)
0125:  * @param int $m, $n 整数
0126:  * @return int 最小公倍数
0127: */
0128: function lcmEuclidean($m$n) {
0129:     if ($n == 0)    return $m;
0130:     return $m * $n / $this->gcdEuclidean($m$n);
0131: }
0132: 
0133: /**
0134:  * 素数を求める(エラトステネスのふるい)
0135:  * @param int   $max     最大値
0136:  * @param array $primes  素数を格納する配列
0137:  * @return array $primes
0138: */
0139: function EratosthenesSieve($max, &$primes) {
0140:     for ($i = 2; $i <= $max$i++) {
0141:         $flag = TRUE;
0142:         for ($k = 2; $k < $i$k++) {
0143:             if ($i % $k === 0) {
0144:                 $flag = FALSE;
0145:                 break;
0146:             }
0147:         }
0148:         if ($flag)  $primes[$i] = 0;
0149:     }
0150:     return $primes;
0151: }
0152: 
0153: /**
0154:  * 素因数分解
0155:  * @param array $primes  素因数を格納する配列
0156:  * @param int   $n       素因数分解する整数
0157:  * @return array $primes
0158: */
0159: function primeDecomposition($n, &$primes) {
0160:     $max = floor(sqrt($n));
0161:     $this->EratosthenesSieve($max$primes);
0162: 
0163:     //2から平方根までの素因数を求める
0164:     for ($i = 2; $i <= $max$i++) {
0165:         if (isset($primes[$i])) {
0166:             do {
0167:                 $b = $n % $i;
0168:                 if ($b == 0) {
0169:                     $primes[$i]++;
0170:                     $n = floor($n / $i);
0171:                 }
0172:             } while ($b == 0);
0173:         }
0174:     }
0175:     //残った素数
0176:     if ($n > $max)  $primes[$n] = 1;
0177: 
0178:     return $primes;
0179: }
0180: 
0181: /**
0182:  * 有限小数かどうか
0183:  * @param なし
0184:  * @return bool TRUE/FALSE
0185: */
0186: function isFiniteDecimal() {
0187:     $gcd = $this->gcdEuclidean($this->denominator$this->numerator);
0188:     $max = $this->denominator / $gcd;
0189: 
0190:     //分母に2, 5以外の素因数が含まれているかどうか
0191:     $primes = array();
0192:     $this->primeDecomposition($max$primes);
0193:     $res = TRUE;
0194:     foreach ($primes as $key=>$val) {
0195:         if ($key != 2 && $key != 5 && $val > 0) {
0196:             $res = FALSE;
0197:             break;
0198:         }
0199:     }
0200:     return $res;
0201: }
0202: 
0203: /**
0204:  * 約分する
0205:  * @param なし
0206:  * @return なし
0207: */
0208: function reduce() {
0209:     $m = $this->gcdEuclidean($this->numerator$this->denominator);
0210:     $this->numerator /= $m;
0211:     $this->denominator /= $m;
0212:     $this->__sign();
0213: }
0214: 
0215: /**
0216:  * 分数表記を返す
0217:  * @param bool $dec = TRUE:有限小数なら小数表記
0218:  * @return string 
0219: */
0220: function printFrac($dec=FALSE) {
0221:     return ($dec && $this->isFiniteDecimal()) ? sprintf("%g", $this->dec()) :
0222:                 sprintf("%d/%d", $this->numerator$this->denominator);
0223: }
0224: 
0225: /**
0226:  * 分数を加算する
0227:  * @param pahooFrac $x 加える分数
0228:  * @param bool TRUE:約分する/FALSE:約分しない
0229:  * @return なし
0230: */
0231: function add($x$reduce=TRUE) {
0232:     $denominator = $this->lcmEuclidean($this->denominator$x->denominator);
0233:     $this->numerator = $this->numerator * ($denominator / $this->denominator)
0234:                         + $x->numerator * ($denominator / $x->denominator);
0235:     $this->denominator = $denominator;
0236:     $this->__sign();
0237:     if ($reduce)    $this->reduce();
0238: }
0239: 
0240: /**
0241:  * 分数を減算する
0242:  * @param pahooFrac $x 減ずる分数
0243:  * @param bool TRUE:約分する/FALSE:約分しない
0244:  * @return なし
0245: */
0246: function sub($x$reduce=TRUE) {
0247:     $denominator = $this->lcmEuclidean($this->denominator$x->denominator);
0248:     $this->numerator = $this->numerator * ($denominator / $this->denominator)
0249:                         - $x->numerator * ($denominator / $x->denominator);
0250:     $this->denominator = $denominator;
0251:     $this->__sign();
0252:     if ($reduce)    $this->reduce();
0253: }
0254: 
0255: /**
0256:  * 分数を乗算する
0257:  * @param pahooFrac $x 乗ずる分数
0258:  * @param bool TRUE:約分する/FALSE:約分しない
0259:  * @return なし
0260: */
0261: function mul($x$reduce=TRUE) {
0262:     $this->numerator   *= $x->numerator;
0263:     $this->denominator *= $x->denominator;
0264:     $this->__sign();
0265:     if ($reduce)    $this->reduce();
0266: }
0267: 
0268: /**
0269:  * 分数を除算する
0270:  * @param pahooFrac $x 除する分数
0271:  * @param bool TRUE:約分する/FALSE:約分しない
0272:  * @return なし
0273: */
0274: function div($x$reduce=TRUE) {
0275:     $this->numerator   *= $x->denominator;
0276:     $this->denominator *= $x->numerator;
0277:     $this->__sign();
0278:     if ($reduce)    $this->reduce();
0279: }
0280: 
0281: // End of Class
0282: }

通分に用いる分母同士の最小公倍数、約分に用いる分子・分母の最小公約数、また、メソッド isFiniteDecimal は、「PHP で分数が有限小数かどうか判定する」で紹介したプログラムを流用した。

0011: //データ構造クラス
0012: require_once('pahooDataStructure.php');
0013: 
0014: // メイン・プログラム ======================================================
0015: printf("\n");
0016: 
0017: $b = new pahooFrac(1, 3);        //分数
0018: 
0019: $a = new pahooFrac(3, 5);        //分数
0020: printf("%s", $a->printFrac(FALSE));
0021: $a->add($b);                  //加算
0022: printf(" + %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
0023: 
0024: $a = new pahooFrac(0.6, 1);   //分数;分子が小数でもOK
0025: printf("%s", $a->printFrac(FALSE));
0026: $a->sub($b);                  //減算
0027: printf(" - %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
0028: 
0029: $a = new pahooFrac(1.5, 2.5);  //分数;分母が小数でもOK
0030: printf("%s", $a->printFrac(FALSE));
0031: $a->mul($b);                  //乗算
0032: printf(" × %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));
0033: 
0034: $a = new pahooFrac(3, 5);        //分数
0035: printf("%s", $a->printFrac(FALSE));
0036: $a->div($b);                  //除算
0037: printf(" ÷ %s = %s\n", $b->printFrac(FALSE), $a->printFrac(TRUE));

PHPによる分数計算
pahooFrac クラスを用いて分数の四則計算を行うサンプル・プログラムが "dt02-04-12.php" である。
実行結果は左図の通り。3 分の 1 に関わる計算も循環小数とならない。

無理数

平方根や対数など、各処理系には無理数を扱う数学関数が用意されている。
ここでも誤差が問題になる。

0001: <?php
0002: //無理数
0003: $a = sqrt(3);
0004: $b = $a * $a;
0005: echo  $b;
0006: ?>

"dt02-04-11.php" を Windows用PHP バージョン 7.2.6(64 ビット)で実行したところ、誤差は発生しなかった。

0001: #無理数
0002: import math
0003: a = math.sqrt(3)
0004: b = a * a
0005: print(b)

"dt02-04-12.py" を Windows用Python バージョン 3.6.5(64 ビット)で実行したところ、誤差が発生した。

0001: #include <iostream>
0002: #include <iomanip>
0003: #include <limits>
0004: #include <cmath>
0005: using namespace std;
0006: 
0007: //無理数
0008: int main() {
0009:     float ab;
0010:     a = sqrt(3);
0011:     b = a * a;
0012:     cout << setprecision(8) << setiosflags(ios::fixed) << b << endl;
0013:     return(0);
0014: }

"dt02-04-22.py" を C++(g++)で実行したところ、誤差は発生しなかった。
(この項おわり)
header