Scala
ScalaDay 20

Scalaで電卓を作成

この記事はScala Advent Calendar 2017の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の場合に指定します。
詳細はこちらを参照

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

電卓のGUIを作成する

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

フレーム作成

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

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

フレームの中身を作成

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

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
  }
}

計算処理

計算式を上手に逆ポーランド記法に並べ替えられるかどうかが肝です。
アルゴリズムはこちらを参考にしました。
参考にしただけで、この通りに作ってはいないです。
特殊な計算式を入力しなければこのプログラムでも大丈夫なはずです。

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
    }
  }
}

最後に

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

以上です。