電卓を作る

はじめに

数式を渡すとその計算結果を出力する電卓を作る。
仕様は次のとおり。

  • 四則演算のみ行える
  • 数値は、小数、整数どちらも指定可能。
    • 10進数の整数表記は /\d+/
    • 8進数の整数表記は /0[0-7]+/
    • 16進数の整数表記は /0[xX][\da-fA-F]+/
  • 「*/+-」を演算子とする
    • *は乗算
    • /は除算
    • +は加算
    • -は減算

トークン分割をする

数式を処理する前準備として、渡された文字列をトークンに分割します。
識別できないトークンが混じっていた場合は例外を投げます。

ソース
OPERATOR = Regexp.union("*", "/", "+", "-")
OPERAND  = Regexp.union(/^[1-9]\d*$/, /^0\d*$/, /^0x[\da-f]+$/i)

def tokenizer(line)
  tokens = line.split(/\b/).map! {|token| token.strip}
  tokens.each {|token|
    case token
    when OPERATOR
      puts "#{token} は演算子です。"
    when OPERAND
      puts "#{token} は被演算子です。"
    else
      raise "#{token} は認識できない識別子です。"
    end
  }
  tokens
end

p tokenizer("3+ 4 * 5+0x52dF*07")
実行結果
3 は被演算子です。
+ は演算子です。
4 は被演算子です。
* は演算子です。
5 は被演算子です。
+ は演算子です。
0x52dF は被演算子です。
* は演算子です。
07 は被演算子です。
["3", "+", "4", "*", "5", "+", "0x52dF", "*", "07"]

中置記法を後置記法へ変換する

中置記法とは、演算子を被演算子の間に書く記法。普段から使用している方法のこと。
後置記法とは、演算子を被演算子の後に書く記法。直感的じゃない。


中間記法は被演算子の間に必ず演算子が挟まるため、区切り文字が要らないというメリットがある。しかし、優先順位を持たせたい場合は括弧を付けなくてはいけないというデメリットがある。


それに対し後置記法は、区切り文字が必要というデメリットがあるが、括弧が不要というメリットがある。そして何より、後置記法だとスタックで簡単に計算ができるというメリットがあり、プログラムを書く上ではこれが何よりのメリット。

ソース
OPERATOR_PRIORITY = {"*" => 1, "/" => 1, "+" => 2, "-" => 2 }
OPERAND  = Regexp.union(/^[1-9]\d*$/, /^0\d*$/, /^0x[\da-f]+$/i)
OPERATOR = Regexp.union(*OPERATOR_PRIORITY.keys)
TOKEN    = Regexp.union(OPERAND, OPERATOR)

# 中置記法の文字列をトークン分割する
def infix_tokenizer(line)
  tokens = line.split(/\b/).map! {|t| t.strip}
  tokens.each {|token|
    raise "#{token} は認識できない識別子です。" if TOKEN !~ token
  }
  tokens
end

# lhsがrhsより優先度が低ければtrue
def less(lhs, rhs)
  if OPERAND =~ rhs
    true
  elsif OPERAND =~ lhs
    false
  else
    OPERATOR_PRIORITY[lhs] >= OPERATOR_PRIORITY[rhs]
  end
end

# 中置記法を後置記法へ変換
def infix_to_postfix(infix)
  postfix = []
  stack   = []
  infix.each {|token|
    postfix << stack.pop while !stack.empty? and less(token, stack.last)
    stack << token
  }
  postfix += stack.reverse
end


infix_string = "3+ 4 * 5+0x52dF*07"
infix   = infix_tokenizer(infix_string)
postfix = infix_to_postfix(infix)
puts "元の文字列: #{infix_string}"
puts "中置記法:   #{infix.join(" ")}"
puts "後置記法:   #{postfix.join(" ")}"
実行結果
元の文字列: 3+ 4 * 5+0x52dF*07
中置記法:   3 + 4 * 5 + 0x52dF * 07
後置記法:   3 4 5 * + 0x52dF 07 * +

後置記法を評価する

後置記法を評価するには、後置記法の式の要素を一つずつ取り出し、その要素が被演算子であればスタックへ積み、演算子であれば、スタックの上から2つを取り出し、その2つの値をその演算子で評価する。評価した結果はスタックへ積み、以後、式の要素がなくなるまでこれを続ける。


これを行うだけで、最後にスタックに残った要素が評価結果となっているという仕組み。


以下のソースでは、スタックから取り出した値の評価に eval関数を利用している。演算子ごとに分岐したり、被演算子の進数変換がめんどうだったので…。


また、自力で計算した結果が合っているかどうかなの検算用にも eval関数を利用している。

ソース
OPERATOR_PRIORITY = {"*" => 1, "/" => 1, "+" => 2, "-" => 2 }
OPERAND  = Regexp.union(/^[1-9]\d*$/, /^0\d*$/, /^0x[\da-f]+$/i)
OPERATOR = Regexp.union(*OPERATOR_PRIORITY.keys)
TOKEN    = Regexp.union(OPERAND, OPERATOR)

# 中置記法の文字列をトークン分割する
def infix_tokenizer(line)
  tokens = line.split(/\b/).map! {|t| t.strip}
  tokens.each {|token|
    raise "#{token} は認識できない識別子です。" if TOKEN !~ token
  }
  tokens
end

# lhsがrhsより優先度が低ければtrue
def less(lhs, rhs)
  if OPERAND =~ rhs
    true
  elsif OPERAND =~ lhs
    false
  else
    OPERATOR_PRIORITY[lhs] >= OPERATOR_PRIORITY[rhs]
  end
end

# 中置記法を後置記法へ変換
def infix_to_postfix(infix)
  postfix = []
  stack   = []
  infix.each {|token|
    postfix << stack.pop while !stack.empty? and less(token, stack.last)
    stack << token
  }
  postfix += stack.reverse
end

# 後置記法の式を評価
def eval_postfix(postfix)
  stack = []
  postfix.each {|token|
    case token
    when OPERATOR
      rhs = stack.pop
      lhs = stack.pop
      stack << eval([lhs, token, rhs].join)
    when OPERAND
      stack << token
    end
  }
  stack.pop
end


infix_string = "3+ 4 * 5+0x52dF*07"
infix   = infix_tokenizer(infix_string)
postfix = infix_to_postfix(infix)
result  = eval_postfix(postfix)
puts "元の文字列: #{infix_string}"
puts "中置記法:   #{infix.join(" ")}"
puts "後置記法:   #{postfix.join(" ")}"
puts "結果:       #{result}"
puts "検算:       #{eval(infix_string)}"
実行結果
元の文字列: 3+ 4 * 5+0x52dF*07
中置記法:   3 + 4 * 5 + 0x52dF * 07
後置記法:   3 4 5 * + 0x52dF 07 * +
結果:       148528
検算:       148528

中置記法の括弧に対応する

中置記法では計算の優先順位を示すために括弧が必須。電卓としては括弧をサポートしないわけにはいかない。括弧に対応するために、トークン分割と、中置記法から後置記法への変換関数に若干手を加えた。

ソース
OPERATOR_PRIORITY = {"*" => 1, "/" => 1, "+" => 2, "-" => 2 }
OPERAND           = Regexp.union(/^[1-9]\d*$/, /^0\d*$/, /^0x[\da-f]+$/i)
OPERATOR          = Regexp.union(*OPERATOR_PRIORITY.keys)
BRACKET_BEGIN     = '('
BRACKET_END       = ')'
BRACKET           = Regexp.union(BRACKET_BEGIN, BRACKET_END)
TOKEN             = Regexp.union(OPERAND, OPERATOR, BRACKET)

# 中置記法の文字列をトークン分割する
def infix_tokenizer(line)
  tokens = line.split(/\s+|\b/).map{|t| OPERAND =~ t ? t : t.split(//)}.flatten
  tokens.each {|token|
    raise "#{token} は認識できない識別子です。" if TOKEN !~ token
  }
  tokens
end

# lhsがrhsより優先度が低ければtrue
def less(lhs, rhs)
  if OPERAND =~ rhs
    true
  elsif OPERAND =~ lhs
    false
  elsif BRACKET =~ lhs
    true
  elsif BRACKET =~ rhs
    false
  else
    OPERATOR_PRIORITY[lhs] >= OPERATOR_PRIORITY[rhs]
  end
end

# 中置記法を後置記法へ変換
def infix_to_postfix(infix)
  postfix = []
  stack   = []
  infix.each {|token|
    case token
    when BRACKET_BEGIN
      stack << token
    when BRACKET_END
      postfix << stack.pop while stack.last != BRACKET_BEGIN
      stack.pop
    else
      postfix << stack.pop while !stack.empty? and less(token, stack.last)
      stack << token
    end
  }
  postfix += stack.reverse
end

# 後置記法の式を評価
def eval_postfix(postfix)
  stack = []
  postfix.each {|token|
    case token
    when OPERATOR
      rhs = stack.pop
      lhs = stack.pop
      stack << eval([lhs, token, rhs].join)
    when OPERAND
      stack << token
    end
  }
  stack.pop
end


infix_string = "(3+ 4) * (5+0x52dF)*07"
infix   = infix_tokenizer(infix_string)
p infix
postfix = infix_to_postfix(infix)
result  = eval_postfix(postfix)
puts "元の文字列: #{infix_string}"
puts "中置記法:   #{infix.join(" ")}"
puts "後置記法:   #{postfix.join(" ")}"
puts "結果:       #{result}"
puts "検算:       #{eval(infix_string)}"
実行結果
["(", "3", "+", "4", ")", "*", "(", "5", "+", "0x52dF", ")", "*", "07"]
元の文字列: (3+ 4) * (5+0x52dF)*07
中置記法:   ( 3 + 4 ) * ( 5 + 0x52dF ) * 07
後置記法:   3 4 + 5 0x52dF + * 07 *
結果:       1039780
検算:       1039780

見え方のテスト用ページ

必ず1日1つの記事で登録すること。
日記の日付とタイトルは表示されないので、見出しがそのページのタイトルとなる。
なので見出しの順番が変わる。

以下は、その例。

小見出し

小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章。
小見出し後の文章小見出し後の文章小見出し後の文章小見出し後の文章。
小見出し後の文章。

小々見出し

小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章小々見出し後の文章。
小々見出し後の文章小々見出し後の文章小々見出し後の文章。
小々見出し後の文章。

リストのテスト

リストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテストリストのテスト。

  • リスト
    • リスト
    • リスト
  • リスト
リストのテスト2

リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2リストのテスト2。

  1. リスト
    1. リスト
    2. リスト
  2. リスト
定義済みリストのテスト

定義済みリストのテスト定義済みリストのテスト定義済みリストのテスト定義済みリストのテスト定義済みリストのテスト定義済みリストのテスト定義済みリストのテスト定義済みリストのテスト

京都府
京都市
滋賀県
大津市
三重県
津市
引用文のテスト

引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト引用文のテスト。

ここは引用文です。
ただのブロックとして使うこともできます。

整形済みテキストのテスト

整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト整形済みテキストのテスト。

整形済みテキストです。
整形済みテキストのテスト(スーパーpre

整形済みテキストのテスト(スーパーpre)整形済みテキストのテスト(スーパーpre)整形済みテキストのテスト(スーパーpre)整形済みテキストのテスト(スーパーpre)整形済みテキストのテスト(スーパーpre)整形済みテキストのテスト(スーパーpre)。

#!/usr/bin/perl -w
use strict;
print <<END;</ppp> 
<html><body>
</body></html>
END
texのテスト

texのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテストtexのテスト。
x^2 + y^2 = z^2
e^{i\pi} = -1

リンクのテスト

リンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテストリンクのテスト。
http://www.hatena.ne.jp/
https://www.hatena.ne.jp/login
info@hatena.ne.jp
はてな
はてなのトップページ

http://www.hatena.ne.jp/images/top/h1.gif
http://www.hatena.ne.jp/images/top/h1.gif
http://www.hatena.ne.jp/images/top/h1.gif

isbn,asinのテスト

isbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテストisbn,asinのテスト。
isbn:4886487319
asin:4886487319
「はてな」ではじめるブログ生活―はてな公式ハンドブック
「はてな」ではじめるブログ生活―はてな公式ハンドブック
「はてな」ではじめるブログ生活―はてな公式ハンドブック
「はてな」ではじめるブログ生活―はてな公式ハンドブック

テーブルのテスト

テーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテストテーブルのテスト。

項目1 項目2 項目3 項目4
1111111111111111111 22222222222222 33333333333333 44444444444
1111111111111111111 22222222222222 33333333333333 44444444444
1111111111111111111 22222222222222 33333333333333 44444444444
1111111111111111111 22222222222222 33333333333333 44444444444
1111111111111111111 22222222222222 33333333333333 44444444444