Scalaで電卓を作成

この記事は[Scala Advent Calendar 2017](https://qiita.com/advent-calendar/2017/scala)の12月20日の記事です。

Qiita初投稿です。
第一回目はScalaで電卓を作ってみようと思います。
Scala初心者なので邪悪な書き方をしてるところもあるかもしれません。
気づいた方は優しく教えてください（笑）

## 環境
以下の環境が整っていることが前提です。OSはMac OS High Sierraです。
 - Eclipse(Oxygen)
 - Scala IDE for Eclipse
 - sbt

## scala.swingを使用可能にする
build.sbtに以下を記述すると、scala.swingがインポートできるようになります。
2.0.xのブランチはscalaのバージョンが2.11もしくは2.12の場合に指定します。
詳細は[こちら](https://github.com/scala/scala-swing)を参照

```scala
libraryDependencies += "org.scala-lang.modules" % "scala-swing_2.12" % "2.0.1"
```

## 電卓のGUIを作成する
GUI自体は簡単に言ってしまうと以下の手順で作成できます。
1. フレームを作る
2. フレームの中身を作る

### フレーム作成
フレームを作成するには以下のコードを記述します。
タイトル、最小サイズ、フレームの中身を設定しています。

```scala
  // フレーム作成
  def top = new MainFrame {
    title = "電卓"
    minimumSize = new Dimension(400, 500)
    contents = gridBagPanel
  }
```

### フレームの中身を作成
ボタンに表示する文字と、ボタンの位置をMapで定義して、ボタンの生成、ボタン押下時の動作をまとめて設定しています。
GridBagPanelというのは、部品をマス目状に配置するレイアウトです。

```scala
val gridBagPanel = new GridBagPanel() {
    // ボタンのテキストと表示位置
    val buttonMap = Map (
      0 -> ("0", pair2Constraints(0, 5)),
      1 -> ("1", pair2Constraints(0, 4)),
      2 -> ("2", pair2Constraints(1, 4)),
      3 -> ("3", pair2Constraints(2, 4)),
      4 -> ("4", pair2Constraints(0, 3)),
      5 -> ("5", pair2Constraints(1, 3)),
      6 -> ("6", pair2Constraints(2, 3)),
      7 -> ("7", pair2Constraints(0, 2)),
      8 -> ("8", pair2Constraints(1, 2)),
      9 -> ("9", pair2Constraints(2, 2)),
      10 -> ("+", pair2Constraints(3, 5)),
      11 -> ("-", pair2Constraints(3, 4)),
      12 -> ("×", pair2Constraints(3, 3)),
      13 -> ("÷", pair2Constraints(3, 2)),
      14 -> ("AC", pair2Constraints(3, 1)),
      15 -> ("(", pair2Constraints(0, 1)),
      16 -> (")", pair2Constraints(1, 1)),
      17 -> ("%", pair2Constraints(2, 1)),
      18 -> (".", pair2Constraints(1, 5)),
      19 -> ("=", pair2Constraints(2, 5))
    )
    
    // ボタンの設定
    val buttonArray = new Array[Button](buttonMap.size)
    for (buttonNum <- 0 until buttonMap.size) {
      // ボタンの生成
      buttonArray(buttonNum) = new Button(buttonMap(buttonNum)._1) {
        preferredSize = new Dimension(100, 50)
        // ボタン押下時の動作を登録
        reactions += {
          case e: ButtonClicked => {
            e.source.text match {
              case "=" => {
                val result = Calculator.calculate(textArea.text)
                textArea.text_=(result)
              }
              case "AC" => textArea.text_=("")
              case _ => textArea.text_=(textArea.text + e.source.text)
            }
          }
        }
      }
      // ボタンの配置を設定
      layout += buttonArray(buttonNum) -> buttonMap(buttonNum)._2
    }
    
    // 計算結果を表示する部分を生成
    val a = pair2Constraints(0, 0)
    a.gridwidth = 4
    val textArea = new TextArea {
      preferredSize = new Dimension(400, 50)
      background = Color.white
    }
    textArea.editable_=(false)
    layout += textArea -> a
  }
}
```

## 計算処理
計算式を上手に逆ポーランド記法に並べ替えられるかどうかが肝です。
アルゴリズムは[こちら](http://home.hiroshima-u.ac.jp/java2/javaenshu-hosoku-2.pdf)を参考にしました。
参考にしただけで、この通りに作ってはいないです。
特殊な計算式を入力しなければこのプログラムでも大丈夫なはずです。

```scala
object Calculator {
  def calculate(formula: String): String = {
    val tmpStack = new scala.collection.mutable.Stack[String]
    
    try {
      // 計算式を逆ポーランド記法にする
      val rpn = convertRPN(formula)
      
      // 計算開始
      while(!rpn.isEmpty) {
        val strTmp = rpn.pop()
        if(strTmp.equals("+") || strTmp.equals("-") || strTmp.equals("×") || strTmp.equals("÷")) {
          val topElem = tmpStack.pop().toFloat
          val secondElem = tmpStack.pop().toFloat
          
          strTmp match {
            case "+" => tmpStack.push((secondElem + topElem) toString)
            case "-" => tmpStack.push((secondElem - topElem) toString)
            case "×" => tmpStack.push((secondElem * topElem) toString)
            case "÷" => tmpStack.push((secondElem / topElem) toString)
            case _ => 
            }
        }
        else {
          tmpStack.push(strTmp)
        }
      }
      if(tmpStack.top.endsWith(".0")) {
        tmpStack.pop().dropRight(2)
      }
      else {
        tmpStack.pop()
      }
    } catch {
        case e: Exception => "エラー"
    }
  }
  
  // 計算式を逆ポーランド記法に並べ替える
  def convertRPN(formula: String): scala.collection.mutable.Stack[String] = {
    val tmp: StringBuffer = new StringBuffer
    val st1 = new scala.collection.mutable.Stack[String]
    val st2 = new scala.collection.mutable.Stack[String]
    
    formula.foreach(
      c =>
        // 演算子が検出されるまで数字を退避
        if ((c >= '0' && c <= '9') || c.equals('.')) {
          tmp.append(c)
        }
        else if (c.equals(')')) {
          if(tmp.length() != 0) {
            st1.push(tmp.toString())
            tmp.setLength(0)
          }
          while(!st2.top.toString().equals("(")) {
            st1.push(st2.pop())
          }
          if(!st2.isEmpty) {
            st2.pop()
          }
        }
        else if (c.equals('(')) {
          st2.push(c.toString())
        }
        else if (c.equals('%')) {
          st1.push((tmp.toString().toFloat / 100).toString())
          tmp.setLength(0)
        }
        // 演算子が加減乗除の場合
        else {
          if(tmp.length() != 0) {
            st1.push(tmp.toString())
            tmp.setLength(0)
          }
          if(!st2.isEmpty && !st2.top.equals("(")) {
            // 計算優先度を比較
            if(chkPriority(c.toString()) == 0 && chkPriority(st2.top) == 1) {
              st2.push(c.toString())
            }
            else {
              st1.push(st2.pop())
            }
          }
          else {
            st2.push(c.toString())
          }
        }
    )
    
    if (tmp.length() != 0) {
      st1.push(tmp.toString())
      tmp.setLength(0)
    }
    
    while(!st1.isEmpty) {
      st2.push(st1.pop())
    }
    
    st2
  }
  
  // 演算子の優先度を決定
  def chkPriority(str: String): Int = {
    str match {
      case x if x.equals("×") || x.equals("÷") => 0
      case x if x.equals("+") || x.equals("-") => 1
      case _ => 2
    }
  }
}
```

### 最後に
まだ改良の余地はあるので、納得がいくアルゴリズムに出会えるまで思案してみます。

以上です。