【Ruby】四則演算に対応した電卓を実装する方法
電卓の完成イメージ
>123+13135.0>123-1*9114.0>123/(1+9)+123456123468.3
このように、対話形式で数式を受け付けてその計算結果を出力する電卓を作ります。
前提知識
数式はノードとエッジで表現する
今回、数式は配列として表現します。
[:add, 1, 2] # 1+2[:mul, [:add, 1, 2], 3] # (1+2)\*3
例えば1+2
と(1+2)\*3
の場合、このように配列に変換して考えます。
構文解析で四則演算を考える
四則演算の優先順位は「()内の計算→掛け算・割り算→足し算→引き算」ですよね。なので配列に変換する際にそれを考慮する必要があります。
そんな配列への変換(構文解析)をするために、expression
・term
・factor
という3つの構文に置き換えて考え、それぞれをメソッドとして実装します。
そしてそれぞれではその配列の3つの要素を見て以下のように判断していきます。
構文 | 詳細 |
---|---|
expression | 足し算か引き算をしていればterm へ |
term | 掛け算か割り算をしていればfactor へ |
factor | () があればterm へ。無ければその数値を返す |
これだけだと分かりにくい思うので具体例を挙げるとこんな感じ。
1+(2-3)*4→term+term→factor+term→1+term→1+factor*facter→1+(expression)*facter→1+(term-term)*facter→1+(facter-term)*facter→1+(2-term)*facter→1+(2-facter)*facter→1+(2-3)*facter→1+(2-3)\*4
まだ漠然としているかもしれませんが、実際に書いていった方が分かりやすいと思うので本題に移っていきましょう。
Rubyで四則演算に対応した電卓を実装する方法
1. StringScannerを使って字句解析
require "strscan"def analysis if @scanner.scan(/\d+|[\+\-\*\/()]/) # 数字または記号かどうか if @scanner[0] =~ /[\+\-\*\/()]/ # 符号の場合 @@tokens[@scanner[0]] else @scanner[0] end else nil endend
まず、そのトークンが数値なのか演算子なのかを判定するanalysis
メソッドを実装します。
StringScanner
クラスのインスタンスに対してscan
メソッドを使って、数値もしくは演算子だった場合にそれを返すようにしています。
参考:class StringScanner (Ruby 3.2 リファレンスマニュアル)
2. expression・term・factorで構文解析
def expression result = term() token = analysis() while token == :add || token == :sub #足し算か引き算の場合 result = [token, result, term()] token = analysis() end reverse_analysis() #判定のために1回多くanalysisを呼んでいるため戻す return resultend
def term result = factor() token = analysis() while token == :mul || token == :div #掛け算か割り算の場合 result = [token, result, factor()] token = analysis() end reverse_analysis() #判定のために1回多くanalysisを呼んでいるため戻す return resultend
def factor token = analysis() if token =~ /\d+/ # リテラルの場合 result = token.to_f # ここで実数に変換 elsif token == :lpar result = expression() # 単なる配列の形にしたいのでexpressionを呼び出すだけ analysis() # 閉じ括弧をスキップ else raise Exception,"エラーです" end return resultend
前提で触れたexpression
・term
・factor
の3つを実装。
expression
では足し算か引き算がある限り、配列の中に配列(`term)を格納していき最終的にその配列を返します。
term
でも掛け算か割り算がある限り、配列の中に配列(factor
)を格納していき最終的にその配列を返します。
factor
では字句解析で得られたものが1つの数値の場合はそれを返し、開き括弧の場合はexpression
を呼び出します。
def reverse_analysis if !(@scanner.eos?) #「previous match record not exist」の対処。トークンが末尾にいってるとエラーになる。 @scanner.unscan() endend
その3つのメソッドの中では符号の判定のためにanalysis
メソッドを呼び出しているのですが、そのままだとトークンが1つ前にずれてしまうので、それを防ぐためにトークンを1つ前に戻すreverse_analysis
メソッドも作っておきます。
3. 計算結果を返すevalメソッドを実装
@@tokens = { '+' => :add, '-' => :sub, '*' => :mul, '/' => :div, '(' => :lpar, ')' => :rpar }def eval(exp) if exp.instance_of?(Array) #配列以外は弾く case exp[0] when :add return eval(exp[1]) + eval(exp[2]) when :sub return eval(exp[1]) - eval(exp[2]) when :mul return eval(exp[1]) * eval(exp[2]) when :div return eval(exp[1]) / eval(exp[2]) end else return exp endend
そして計算用に変換した配列を実際に計算してくれるeval
メソッドを実装。
符号それぞれをシンボルに割り当てることによって、返す計算結果を変えます。
4. evalメソッドを呼び出す
def initialize while true do print ">" input = STDIN.gets if input == "q\n" p "終了します" exit end @scanner = StringScanner.new(input.chomp) p eval(expression) # 結果を出力 endend
最後にexpression
で返ってきたものを引数としてeval
を呼び出すコンストラクタを実装してあげれば、完成です 🎉
以下に全体のコードを載せておきます。
require "strscan"
class Calc @@tokens = { '+' => :add, '-' => :sub, '*' => :mul, '/' => :div, '(' => :lpar, ')' => :rpar } def analysis if @scanner.scan(/\d+|[\+\-\*\/()]/) # 数字または記号かどうか if @scanner[0] =~ /[\+\-\*\/()]/ # 符号の場合 @@tokens[@scanner[0]] else @scanner[0] end else nil end end
def eval(exp) if exp.instance_of?(Array) case exp[0] when :add return eval(exp[1]) + eval(exp[2]) when :sub return eval(exp[1]) - eval(exp[2]) when :mul return eval(exp[1]) * eval(exp[2]) when :div return eval(exp[1]) / eval(exp[2]) end else return exp end end
def reverse_analysis if !(@scanner.eos?) #「previous match record not exist」の対処。トークンが末尾にいってるとエラーになる。 @scanner.unscan() end end
def expression result = term() token = analysis() while token == :add || token == :sub # 足し算か引き算の場合 result = [token, result, term()] token = analysis() end reverse_analysis() return result end
def term result = factor() token = analysis() while token == :mul || token == :div # 掛け算か割り算の場合 result = [token, result, factor()] token = analysis() end reverse_analysis() return result end
def factor token = analysis() if token =~ /\d+/ # リテラルの場合 result = token.to_f # ここで実数に変換 elsif token == :lpar result = expression() # 単なる配列の形にしたいのでexpressionを呼び出すだけ analysis() # 閉じ括弧をスキップ else raise Exception,"エラーです" end return result end
def initialize while true do print ">" input = STDIN.gets if input == "q\n" p "終了します" exit end @scanner = StringScanner.new(input.chomp) p eval(expression) # 結果を出力 end end endCalc.new