くるみ
ブログ主の単なる備忘録であり至らない点もあると思いますが参考になれば幸いです⸝⸝- ̫ -⸝⸝
お品書き
電卓の完成イメージ
>810+19
829.0
>810-1*9
801.0
>810/(1+9)+114514
114595.0
このように、入力で数式を受け付けてその計算結果を出力するものを目指します。
くるみ
計算機を実装するにあたって
数式はノードとエッジで表現する
今回、数式は配列として表現します。
例えば「1+2」と「(1+2)*3」の場合、このように配列に変換して考えます。
[:add, 1, 2] #1+2
[:mul, [:add, 1, 2], 3] #(1+2)*3
構文解析で四則演算を考える
四則演算の優先順位は「()内の計算→掛け算・割り算→足し算→引き算」ですよね。なので配列に変換する際にそれを考慮する必要があります。
くるみ
そしてそれぞれではその配列の3つの要素を見て以下のように判断していきます。
expresssion | 足し算か引き算をしていれば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で四則演算に対応した電卓を実装する方法
StringScannerを使って字句解析
require "strscan"
def analysis
if @scanner.scan(/\d+|[\+\-\*\/()]/) #数字または記号かどうか
if @scanner[0] =~ /[\+\-\*\/()]/ #符号の場合
@@tokens[@scanner[0]]
else
@scanner[0]
end
else
nil
end
end
まず、そのトークンが数値なのか演算子なのかを判定するanalysisメソッド。
StringScannerクラスのインスタンスに対してscanメソッドを使って、数値もしくは演算子であった場合にそれを返すようにしています。
参考 StringScannerクラスRuby 2.7.0 リファレンスマニュアル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 result
end
def term
result = factor()
token = analysis()
while token == :mul || token == :div #掛け算か割り算の場合
result = [token, result, factor()]
token = analysis()
end
reverse_analysis() #判定のために1回多く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
先程話題にあがったexpression・term・factorの3つを実装。
expressionでは足し算か引き算がある限り、配列の中に配列(term)を格納していき最終的にその配列を返します。
termでも掛け算か割り算がある限り、配列の中に配列(factor)を格納していき最終的にその配列を返します。
factorでは字句解析で得られたものが一つの数値であった場合はそれを返し、開き括弧であればexpression呼び出します。
def reverse_analysis
if !(@scanner.eos?) #「previous match record not exist」の対処。トークンが末尾にいってるとエラーになる。
@scanner.unscan()
end
end
その3つのメソッドの中では符号の判定のためにanalysisメソッドを呼び出しているのですが、そのままだとトークンが一つ前にずれてしまうので、それを防ぐためにトークンを一つ前に戻すreverse_analysisメソッドも作っておきます。
計算結果を返す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
end
end
そして計算用に変換した配列を実際に計算してくれるevalメソッドを実装。
符号それぞれをシンボルに割り当てることによって、返す計算結果を変えます。
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) #結果を出力
end
end
最後に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
end
Calc.new
まとめ
参考になれば幸いです!では⸝⸝- ̫ -⸝⸝