【Ruby】四則演算に対応した電卓を実装する方法

Rubyで四則演算に対応した電卓を実装する方法

くるみ

Rubyで四則演算ができる計算機を実装するんだぞ
ということで今回はRubyで「四則演算に対応した電卓」を実装します。

ブログ主の単なる備忘録であり至らない点もあると思いますが参考になれば幸いです⸝⸝- ̫ -⸝⸝

電卓の完成イメージ

>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

構文解析で四則演算を考える

四則演算の優先順位は「()内の計算→掛け算・割り算→足し算→引き算」ですよね。なので配列に変換する際にそれを考慮する必要があります。

ちょっと大変そうだけど頑張ろう

くるみ

そんな配列への変換(構文解析)をするために、expression・term・factorという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

まとめ

以上「Rubyで四則演算に対応した電卓を実装する方法」でした。

参考になれば幸いです!では⸝⸝- ̫ -⸝⸝

コメントを残す