DEVELOPERS BLOGデベロッパーズブログ

  1. HOME > 
  2. 加藤 正人のデベロッパーズブログ > 
  3. 分数電卓を作ってみる・その1

加藤 正人のデベロッパーズブログ

加藤 正人

氏名
加藤 正人
役職
多分SE
血液型
秘密
出没
美味しいもののあるところ
特色
タヒチ大好き。ちょいメタボ。

加藤 正人

2015/03/10

分数電卓を作ってみる・その1

 

最近子供の宿題で、分数の計算をする機会が増えた。

正解例はもらえないので、答え合わせをするにも50問も100問もあると親が実際に計算をすることになり少々 (かなり) 大変だ。

ということで、簡単な分数電卓を CakePHP で Web ページ上に実装してみた。(実装例はこちら。)

基本構想

  • 電卓ページの実装はフォームのテキスト入力欄に分数と四則演算を入力し、ボタンクリックで結果を表示する。
  • 分数の表記は入力の容易さと構文解析の容易さのバランスを取り、帯分数 $$1 \frac{\;2\;}{\;3\;}$$ (1と3分の2、古い読み方なら 1荷(か)3分の2) なら [1&2/3]、帯分数でない分数 $$\frac{\;17\;}{\;23\;}$$ (23分の17) なら [17/23] のようにする。
  • 結果は「テキスト」ではなく、MathJax を使用して数式として見やすい形式で出力する。

必要なもの

分数電卓を実装するためには、以下のものが必要となる。

  • 分数のデータの保持および演算機能を提供するクラス (Fraction クラス)
  • 入力した分数式を解釈し演算順序を判定するための構文パーサ

今回は Fraction クラスについて解説する。

PHP には分数に対応する型は存在していないので、クラスでデータ保持構造と演算機能を実現する。クラス構造としては、分母と分子だけからなる BaseFraction クラスをまず定義する。これは、分子・分母の2つの要素だけで演算することにより四則演算の実装が容易なためだ。そして BaseFraction クラスを拡張する形で帯分数に対応した Fraction クラスを実装する。

BaseFraction クラス

まずはコア機能を実装するための BaseFraction の説明から。

001class BaseFraction
002{
003    // 真分数または仮分数を実装するための基本クラス。帯分数はこのクラスを拡張した Fraction クラスで実装する。
004    // 分子・分母は GMP 数で実装するので、サーバ側 PHP 環境に GMP (GNU Multiple Precision) が実装されていることが前提となる。
005    // 表示用に TeX コマンド表現への変換をサポートする。
006    const StringFracFormat = '[%s/%s]'; // 分数の文字表現
007    const TeXFracFormat = '\frac{\;%s\;}{\;%s\;}'// 分数の TeX 書式 (分数の横棒が長くなるように調整済み)
008 
009    protected $sign = 0;    // 符号 (1: 正数; 0: 0; -1: 負数)
010    protected $numerator = NULL;    // 帯分数の分子 (Numerator)
011    protected $denominator = NULL;  // 帯分数の分母 (Denominator)
012 
013    protected $e_divided_by_zero = 'Divided by 0 (zero).';
014 
015    // 分数オブジェクトを生成する (分子・分母は与えられたままの値を使用する。明示的に行わない限り約分はされない。)
016    public function __construct($num, $den=1)
017    {
018        if ( is_numeric($num) ) $num = gmp_init($num);
019        if ( is_numeric($den) ) $den = gmp_init($den);
020 
021        $this->numerator = gmp_abs($num);
022        if ( is_null($den) )
023        {
024            // 分母が NULL ⇒ 整数を表す
025            $this->denominator = NULL;
026            $this->sign = gmp_sign($num);
027        }
028        else
029        {
030            if ( $den === 0 || gmp_strval($den) == '0' )
031            {
032                throw new Exception($this->e_divided_by_zero);
033            }
034            // 分母が NULL でない ⇒ 分数を表す
035            $this->denominator = gmp_abs($den);
036            $this->sign = gmp_sign($num) * gmp_sign($den);
037        }
038    }
039 
040    // 分数オブジェクトのコピーを作成する
041    protected function copy()
042    {
043        return new BaseFraction(gmp_strval($this->numerator)*$this->sign, $this->denominator);
044    }
045 
046    // 分数の符号を取得する (整数値; 1: 正; 0: ゼロ; -1: 負)
047    public function getSign()
048    {
049        return $this->sign;
050    }
051 
052    // 分子を取得する (GMP 数)
053    public function getDenominator()
054    {
055        return $this->denominator;
056    }
057 
058    // 分母を取得する (GMP 数)
059    public function getNumerator()
060    {
061        return $this->numerator;
062    }
063 
064    // 分数を約分し自オブジェクトを返す
065    public function reduction()
066    {
067        if ( !is_null($this->denominator) )
068        {
069            // 分母が NULL でない場合のみ約分処理を実行
070            $gcd = gmp_gcd($this->numerator, $this->denominator);
071            $this->numerator = gmp_div_q($this->numerator, $gcd);
072            $this->denominator = gmp_div_q($this->denominator, $gcd);
073        }
074        return $this;
075    }
076 
077    ///////////////////////////////
078    // 分数の表現
079    ///////////////////////////////
080    private function __display($format)
081    {
082        if ( $this->sign === 0 )
083        {
084            // 符号情報が 0 ⇒ 0 を表示
085            $ret = '0';
086        }
087        if ( is_null($this->denominator) )
088        {
089            // 分母が NULL ⇒ 整数として表示
090            if ( $this->sign > 0 )
091            {
092                $ret = "{$this->numerator}";
093            }
094            else
095            {
096                $ret = "-{$this->numerator}";
097            }
098                 
099        }
100        else
101        {
102            // 分母が NULL でない ⇒ 分数表示
103            if ( $this->sign > 0 )
104            {
105                $ret = sprintf($format, gmp_strval($this->numerator), gmp_strval($this->denominator));
106            }
107            else
108            {
109                $ret = sprintf('-'.$format, gmp_strval($this->numerator), gmp_strval($this->denominator));
110            }
111        }
112        return $ret;
113    }
114 
115    public function toTeX()
116    {
117        return $this->__display(self::TeXFracFormat);
118    }
119 
120    public function toString()
121    {
122        return $this->__display(self::StringFracFormat);
123    }
124 
125    ///////////////////////////////
126    // 算術演算
127    ///////////////////////////////
128 
129    // 自オブジェクトに分数を1つ加算し、約分した結果を新しいオブジェクトとして返す。
130    public function add($f)
131    {
132        if ( $this->sign === 0 )
133        {
134            // 自分自身が 0 ⇒ 加算値を複製して返す
135            $ret = $f->copy()->reduction();
136        }
137        else if ( $f->getSign() == 0 )
138        {
139            // 加算値側が 0 ⇒ 自分自身を複製して返す
140            $ret = $this->copy()->reduction();
141        }
142        else
143        {
144            // どちらも 0 でない
145            $num = gmp_add(
146                    gmp_mul(gmp_mul($this->numerator, $this->sign), $f->denominator),
147                    gmp_mul($this->denominator, gmp_mul($f->numerator, $f->sign))
148            );
149            $den = gmp_mul($this->denominator, $f->denominator);
150 
151            $ret = new BaseFraction($num, $den);
152        }
153        return $ret->reduction();
154    }
155 
156    // 自オブジェクトに分数を1つ乗算し、約分した結果を新しいオブジェクトとして返す。
157    public function mul($f)
158    {
159        if ( $this->sign === 0 || $f->sign === 0 )
160        {
161            // 自オブジェクトと乗数のうち少なくとも一方が 0 だった ⇒ 新たに 0 オブジェクトを作成し返す
162            $ret = new BaseFraction(0);
163        }
164        else
165        {
166            $num = gmp_mul(gmp_mul($this->sign, $this->numerator), gmp_mul($f->sign, $f->numerator));
167            $den = gmp_mul($this->denominator, $f->denominator);
168            $ret = new BaseFraction($num, $den);
169        }
170 
171        return $ret->reduction();
172    }
173 
174    // 自オブジェクトの逆数を新しいオブジェクトとして返す。(約分はしない)
175    public function invert()
176    {
177        $sign = $this->sign;
178        $num = $this->numerator;
179        $den = $this->denominator;
180        if ( gmp_cmp($num, '0') == 0 )
181        {
182            throw new Exception($this->e_divided_by_zero);
183        }
184        return new BaseFraction(gmp_mul($sign, $den), $num);
185    }
186 
187    // 自オブジェクトのコピーを新たに生成し、符号を反転して返す
188    public function neg()
189    {
190        $ret = $this->copy();
191        $ret->sign *= -1;
192        return $ret;
193    }
194 
195    public static function is_base_fraction($o)
196    {
197        $class_name = @get_class($o);
198        return ($class_name === __CLASS__);
199    }
200}
主なプロパティ (メンバー変数)
protected $sign

分数全体の符号を表す整数値をとる。-1 ⇒ 負、0 ⇒ ゼロ、1 ⇒ 正。「分数が 0 (ゼロ) である」とは、分子 (および分母) の値の如何に関わらず「$sign が 0 であること」である。

protected $numerator

分子。値は GMP (Gnu Multiple Precision) 数で実装することで、多倍長整数を扱う。

protected $denominator

分母。値は GMP 数で実装し、多倍長整数を扱う。なお 0 (ゼロ) を指定した場合は例外 Exception をスローする。

主なメソッド
public function __construct($num, $den=1)

コンストラクタ。分母($den) を省略した場合は 1。なお、与えられた分母と分子は約分せずそのまま設定する。約分が必要な場合は、パブリックメソッド reduction() を使用する。

protected function copy()

当該オブジェクトのコピーを作成して返す。

public function getSign()

BaseFraction の符号情報 (プロパティ $sign の値) を返す。

public function getDenominator()

分子の値 (GMP 数) を取得する。

public function getNumerator()

分母の値 (GMP 数) を取得する。

public function reduction()

分数を約分し、当該オブジェクト自身を返す。

public function add($f)

引数 $f で与えられる BaseFraction オブジェクトを、当該オブジェクトに加算する。結果は既約分数。

public function mul($f)

引数 $f で与えられる BaseFraction オブジェクトを、当該オブジェクトに掛ける。結果は既約分数。

public function invert()

当該オブジェクトの逆数を BaseFraction オブジェクトとして返す。なお、当該オブジェクトの分子が 0 の場合は例外 Exception を発生する。

public function neg()

当該オブジェクトのコピーを作成し、符号を反転して返す。

public static function is_base_fraction($o)

引数 $o が BaseFraction のインスタンスであるかどうかを調べる。

Fraction クラス

続いて BaseFraction を拡張した Fraction を定義する。

001class Fraction extends BaseFraction
002{
003    const StringFracRegex = '/^(-)?\[([0-9]*&)?([0-9]+)\/([0-9]+)\]$/'; // 帯分数のテキスト表現読み込み用正規表現
004 
005    // 与えられた引数が GMP リソース (
006    private function is_GMP($obj)
007    {
008        // GMP 数は PHP 5.6 より前は「リソース」。5.6 以降は GMP オブジェクト (実装による)。
009        return (is_resource($obj) && get_resource_type($obj) == 'GMP integer') || (is_object($obj) && (get_class($obj) == 'GMP'));
010    }
011 
012    public function __construct($int, $num=0, $den=1, $sign=NULL)
013    {
014        if ( is_string($int) && preg_match(self::StringFracRegex, $int, $match) )
015        {
016//debug(compact('match'));
017            // TODO (テキスト表現
018            $sign = 1;
019            if ( $match[1] === '-' )
020            {
021                $sign = -1;
022            }
023            $int = rtrim($match[2], '&');
024            $num = $match[3];
025            $den = $match[4];
026            self::__construct($int, $num, $den, $sign);
027        }
028        else if ( is_numeric($int) )
029        {
030             $int = gmp_init($int);
031            if ( is_numeric($num) )
032            {
033                $num = gmp_init($num);
034                if ( is_numeric($den) )
035                {
036                    $den = gmp_init($den);
037                    if ( !is_numeric($sign) && !is_null($sign) )
038                    {
039                        throw new Exception('The fourth parameter for ' . __METHOD__ . ' MUST be an integer or numeric string.');
040                    }
041                }
042                else if ( !$this->is_GMP($den) )
043                {
044                    throw new Exception('The third parameter for ' . __METHOD__ . ' MUST be an integer, numeric string or GMP number.');
045                }
046            }
047            else if ( !$this->is_GMP($num) )
048            {
049                throw new Exception('The second parameter for ' . __METHOD__ . ' MUST be an integer, numeric string or GMP number.');
050            }
051        }
052        else if ( Fraction::is_fraction($int) )
053        {
054            // 第1引数が分数 ⇒ 新しい分数オブジェクトを生成して返す
055            self::__construct(0, $int->numerator, $int->denominator, $int->sign);
056            return;
057        }
058        else if ( !$this->is_GMP($int) )
059        {
060            throw new Exception('The first parameter for ' . __METHOD__ . ' MUST be an integer, numeric string or GMP number.');
061        }
062 
063        if ( is_string($int) && preg_match(self::StringFracRegex, $int, $match) )
064        {
065            debug(compact('match'));
066            // TODO (テキスト表現
067            $sign = 1;
068            if ( $match[1] === '-' )
069            {
070                $sign = -1;
071            }
072            $int = rtrim($match[2], '&');
073            $num = $match[3];
074            $den = $match[4];
075            self::__construct($int, $num, $den, $sign);
076        }
077 
078        if ( is_null($den) )
079        {
080            if ( is_null($num) )
081            {
082                // 分母・分子ともに NULL ⇒ 整数
083                parent::__construct($int);
084            }
085            else
086            {
087                throw new Exception('3rd argument for ' . __METHOD__ . ' cannot be a NULL.');
088            }
089        }
090        else
091        {
092            if ( is_null($sign) )
093            {
094                parent::__construct(gmp_add(gmp_mul($int, $den), $num), $den);  // 帯分数は仮分数として保存する
095            }
096            else if ( $sign === 1 || $sign === 0 || $sign === -1 )
097            {
098                parent::__construct(gmp_mul($sign, gmp_add(gmp_mul($int, $den), $num)), $den);  // 帯分数は仮分数として保存する
099            }
100        }
101    }
102 
103    // 分数のデータアクセスメソッドは、Fraction で拡張した部分だけを定義すればOK。
104 
105    // 整数部を取得する
106    public function getIntegralPart()
107    {
108        if ( is_null($this->denominator) )
109        {
110            // 分母が NULL ⇒ 整数 (分子)
111            $ret = $this->numerator;
112        }
113        else
114        {
115            // 分母が NULL でない ⇒ 分子を分母で割った商
116            $ret = gmp_div_q($this->numerator, $this->denominator);
117        }
118        return $ret;
119    }
120 
121    // 分子を取得する (GMP 数)
122    public function getNumerator($raw=FALSE)
123    {
124        if ( is_null($this->denominator) || $raw )
125        {
126            // 分母が NULL の場合、および生データ指定 (=帯分数化しない) がある場合
127            $ret = $this->numerator;    // 分子そのもの
128        }
129        else
130        {
131            $ret = gmp_mod($this->numerator, $this->denominator);   // 分子を分母で割った剰余を返す
132        }
133        return $ret;
134    }
135 
136/****
137    // 分母を取得する (GMP 数)
138    public function getDenominator()
139    {
140        return $this->denominator;
141    }
142 
143    // 分数の符号を取得する (整数 1: 正数; 0: ゼロ; -1: 負数
144    public function getSign()
145    {
146        return $this->sign;
147    }
148    // 分数を通分する
149    public function reduction()
150    {
151        return parent::reduction();
152    }
153****/
154 
155    /////////////////////////////////////
156    // 表示用メソッド群
157    /////////////////////////////////////
158 
159    const StringFracFormat = '[%s&%s/%s]'// 帯分数の文字表現
160    const TeXFracFormat = '%s\frac{\;%s\;}{\;%s\;}';    // 帯分数の TeX 書式 (分数の横棒が長くなるように調整済み)
161    // 分数を読み戻し可能なテキスト表記にする
162    public function __display($format1, $format2, $mixform=TRUE)
163    {
164        if ( $mixform )
165        {
166            // 帯分数表示
167            $int = $this->getIntegralPart();
168            $num = $this->getNumerator();
169            $den = $this->denominator;
170            $sign = $this->sign;
171 
172            if ( gmp_cmp($den, '1') == 0 )
173            {
174                // 分母が1 ⇒ 仮分数の分子に符号をつけてそのまま整数
175                $ret = gmp_strval(gmp_mul($this->numerator, $sign));
176            }
177            else
178            {
179                // 分母がNULL でない ⇒ 分数表示
180                if ( $sign == 0 )
181                {
182                    // 0
183                    $ret = '0';
184                }
185                else if ( $sign > 0 )
186                {
187                    // 正
188                    if ( gmp_cmp($int,0) == 0 )
189                    {
190                        $ret = sprintf($format1, gmp_strval($num), gmp_strval($den));
191                    }
192                    else
193                    {
194                        $ret = sprintf($format2, gmp_strval($int), gmp_strval($num), gmp_strval($den));
195                    }
196                }
197                else
198                {
199                    // 負
200                    if ( gmp_cmp($int,0) == 0 )
201                    {
202                        $ret = sprintf('-' . $format1, gmp_strval($num), gmp_strval($den));
203                    }
204                    else
205                    {
206                        $ret = sprintf('-' . $format2, gmp_strval($int), gmp_strval($num), gmp_strval($den));
207                    }
208                }
209            }
210        }
211        else
212        {
213            // 純分数表示 ⇒ 親クラスの toTeX() で処理
214            $ret = parent::toTeX();
215        }
216        return $ret;
217    }
218 
219    public function toString($mixform=TRUE)
220    {
221        return $this->__display(parent::StringFracFormat, self::StringFracFormat, $mixform);
222    }
223 
224    // 分数を TeX 表記文字列に変換する
225    public function toTeX($mixform=TRUE)
226    {
227        return $this->__display(parent::TeXFracFormat, self::TeXFracFormat, $mixform);
228    }
229 
230    // TeX 文字列を GoogleCharts の Math で画像化する img タグを生成する
231    public static function googleTeX($tex)
232    {
233        return sprintf('<img src="https://chart.googleapis.com/chart?cht=tx&chl=%s" />', urlencode($tex));
234    }
235 
236    // TeX 文字列を MathJax コマンド文字列に変換する
237    public static function MathJax($tex, $inline=TRUE)
238    {
239        if ( $inline )
240        {
241            $delimiter = '$';   // inline 用デリミタ
242        }
243        else
244        {
245            $delimiter = '$$'// display 用デリミタ
246        }
247        return $delimiter . $tex . $delimiter;
248    }
249 
250    // 型判定
251    public static function is_fraction($o, $strict=FALSE)
252    {
253        $class_name = @get_class($o);
254        if ( $strict )
255        {
256            $ret = ($class_name === __CLASS__ || parent::is_base_fraction($o)) && ($o->getNumerator() != 0);
257        }
258        else
259        {
260            $ret = ($class_name === __CLASS__ ||parent::is_base_fraction($o));
261        }
262        return $ret;
263    }
264 
265    public function is_zero()
266    {
267        return ($this->sign === 0);
268    }
269 
270    public function is_positive()
271    {
272        return ($this->sign > 0);
273    }
274 
275    public function is_negative()
276    {
277        return ($this->sign < 0);
278    }
279 
280    //////////////////////////////////////////////
281    // 算術演算
282    //////////////////////////////////////////////
283 
284    public function add($f)
285    {
286        if ( !self::is_fraction($f) )
287        {
288            if ( is_string($f) && preg_match('/^[+-]?[0-9]+$/', $f) )
289            {
290                $f = new Fraction($f);
291            }
292            else if ( is_numeric($f) )
293            {
294                $f = $f + 0;
295                if ( is_integer($f) )
296                {
297                    $f = new Fraction($f);
298                }
299                else if ( is_float($f) )
300                {
301                    $s = sprintf('%f', $f);
302                    if ( ($pos=strpos($s, '.')) === FALSE )
303                    {
304                        // 小数点が見つからなかった
305                        $f = new Fraction($f);
306                    }
307                    else
308                    {
309                        // 小数点が見つかった
310                        list($a, $b) = explode('.', $s);    // $a は符号を含むことがある
311                        $num = $a . $b;
312                        $den = '1' . str_repeat('0', strlen($b));
313                        $f = new Fraction(0, $num, $den);
314                    }
315                }
316            }
317            else
318            {
319                throw new Exception('invalid argument given for ' . __METHOD__ . ' ' . print_r($f, TRUE));
320            }
321        }
322        $base_fraction = parent::add($f);
323        $base_fraction = $base_fraction->reduction();   // 結果は Fraction ではなく BaseFraction なので、帯分数形式にするには Fraction に直す。
324         
325        $ret = new Fraction(0, $base_fraction->numerator, $base_fraction->denominator, $base_fraction->sign);
326 
327        return $ret->reduction();
328    }
329 
330    public function mul($f)
331    {
332        if ( !self::is_fraction($f) )
333        {
334            if ( is_string($f) && preg_match('/^[+-]?[0-9]+$/', $f) )
335            {
336                $f = new Fraction($f);
337            }
338            else if ( is_numeric($f) )
339            {
340                $f = $f + 0;
341                if ( is_integer($f) )
342                {
343                    $f = new Fraction($f);
344                }
345                else if ( is_float($f) )
346                {
347                    $s = sprintf('%f', $f);
348                    if ( ($pos=strpos($s, '.')) === FALSE )
349                    {
350                        // 小数点が見つからなかった
351                        $f = new Fraction($f);
352                    }
353                    else
354                    {
355                        // 小数点が見つかった
356                        list($a, $b) = explode('.', $s);    // $a は符号を含むことがある
357                        $num = $a . $b;
358                        $den = '1' . str_repeat('0', strlen($b));
359                        $f = new Fraction(0, $num, $den);
360                    }
361                }
362            }
363            else
364            {
365                throw new Exception('invalid argument given for ' . __METHOD__ . ' ' . print_r($f, TRUE));
366            }
367        }
368        $base_fraction = parent::mul($f);
369        $base_fraction = $base_fraction->reduction();   // 結果は Fraction ではなく BaseFraction なので、帯分数形式にするには Fraction に直す。
370         
371        $ret = new Fraction(0, $base_fraction->numerator, $base_fraction->denominator, $base_fraction->sign);
372 
373        return $ret->reduction();
374    }
375 
376    public function neg()
377    {
378        $ret = $this->copy();
379        if ( parent::is_base_fraction($ret) )
380        {
381            $ret = new Fraction($ret);
382        }
383        $ret->sign *= -1;
384        return $ret;
385    }
386}
主なプロパティ

Fraction クラスでは、プロパティは定義しない。親クラス BaseFraction のプロパティを継承。

主なメソッド
public function __construct($int, $num=0, $den=1, $sign=NULL)

コンストラクタ。$int は帯分数の整数部、$num は分子、$den は分母、$sign は分数全体の符号を指定する。

なお、実際には帯分数を仮分数に変換し、その仮分数で BaseFraction オブジェクトを生成する。

public function getIntegralPart()

Fraction オブジェクトの整数部分を返す。実際には整数部はデータとしては保持していないので、分子と分母から毎回計算する。

public function getNumerator($raw=FALSE)

分子を取得する。パラメータ $raw が TRUE の場合は仮分数の分子を、そうでない場合は帯分数化した場合の分子を計算して返す。

public static function is_fraction($o, $strict=FALSE)

与えられたオブジェクト $o が分数かどうかを判定する。

第2引数 $strict が FALSE (既定値) の場合、分子が 0 の場合は「整数」とみなし分数とはしない。

public function is_zero()

当該分数オブジェクトが 0 (ゼロ) かどうかを判定する。

public function is_positive()

当該分数オブジェクトが正かどうかを判定する。

public function is_negative()

当該分数オブジェクトが負かどうかを判定する。

public function add($f)

引数 $f を当該オブジェクトに加算する。結果は既約分数。

public function mul($f)

引数 $f を当該オブジェクトに掛ける。結果は既約分数。

public function neg()

当該オブジェクトの複製を作成し、符号を反転させたものを返す。

次回は分数演算の式パーサを解説する予定。

関連タグ: 分数演算  CakePHP2 

関連エントリー

CakePHP のレンダリング結果を保存したい

CakePHP 2.x の Cookie と js.cookie.js

時刻入力用 jQuery Plugin TimePicki の不具合調整

CakePHP プラグインで HTTPS 判定

作業用モデルビヘイビア