WinUI3で超簡易電卓を作る練習(計算処理-乗算・除算編)

:::note info

WinUI3を学びたく、練習として簡単な電卓を作ってみることにしました。

今回は入力したデータを元に計算処理のうち、乗算・除算処理を実装していきます。

C#とxamlに関しても初心者ですのでお手柔らかにお願いします。
:::

## 前回の記録

https://qiita.com/nogino_sanebou/items/87a6f73967b80b215b46

## どのような実装が必要が考えてみる

`n [×÷] n [+-] n`のパターンは順番に計算していけばいいので前回までの実装でカバーできます。

しかし他の乗算・除算パターン(例えば`n [+-] n [×÷] n`)は優先順位が付きますので今までの実装ではカバーできません。

そこで考えたのが右辺の処理をする際に、この数値以降の値も確認していき、乗算・除算であった場合は`Formula`クラスを生成するというものです。

つまり右辺も左辺と同様に`Formula`クラスも持つということですね。

## 解析処理の修正
最初に解析処理を[×÷]に対応します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略
     
    private List<string> AnalyzeFormula(string formula)
    {
        StringBuilder sb = new();
        List<string> tokens = new();
        foreach (char token in formula.ToArray())
        {
            switch (token)
            {
                // 演算子が出現した場合
                case '+':
                case '-':
+               case '×':
+               case '÷':
                    // 結合した数値の文字列表現をリストに追加します。
                    // リストに追加した後、次の数値の結合を行うため、
                    // StringBuilderをクリアします。
                    tokens.Add(sb.ToString());
                    sb.Clear();
                    // 演算子の文字列表現をリストに追加します。
                    tokens.Add(token.ToString());
                    break;
                // 演算子以外はStringBufferに追加していきます。
                default:
                    sb.Append(token);
                    break;
            }
        }
        // 最後の右辺をリストに追加します。
        tokens.Add(sb.ToString());

        return tokens;
    }

       // 省略
}
```
## Operator列挙型の修正
Operator列挙型に[×÷]の表現を追加します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略
     
    private enum Operator
    {
        Addition, 
        Subtraction,
+       Multiplication,
+       Division,
    }

       // 省略
}
```

## Formulaクラスの計算処理に[×÷]の処理を追加
`Execute`メソッドに以下を追加します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略
     
    private class Formula(Value lhs, Value rhs, Operator op)
    {
        private readonly Value _lhs = lhs;
        private readonly Value _rhs = rhs;
        private readonly Operator _operator = op;

        /// <summary>
        /// 左辺・右辺の値、演算子を元に計算処理を行います。
        /// </summary>
        /// <returns>計算結果</returns>
        /// <exception cref="InvalidEnumArgumentException">未知のOperator値が出現した場合</exception>
        public double Execute()
        {
            return _operator switch
            {
                Operator.Addition => _lhs.Get() + _rhs.Get(),
                Operator.Subtraction => _lhs.Get() - _rhs.Get(),
+               Operator.Multiplication => _lhs.Get() * _rhs.Get(),
+               Operator.Division => _lhs.Get() / _rhs.Get(),
                _ => throw new InvalidEnumArgumentException(),
            };
        }
    }
}
```

## 右辺の処理をする関数の実装

右辺の処理は関数名`CreateRightHandSide`として実装します。

この関数は引数のリストから次に出現する演算子が[×÷]の場合、この位置から`Formula`クラスを生成し、これを値として`Value`クラスを生成します。
そうでなかった場合は数値(double型)として`Value`クラスを生成します。

`Formula`クラスを生成する際、更に[×÷]演算が続く事を見越してこの`Formula`クラスの右辺の設定にも`CreateRightHandSide`の実行結果を設定します。

`Formula`クラスを生成した際、リストの解析位置がこの関数を呼ぶ前より先に移動しています。
しかし何処まで移動しているのかは呼び出し元からは分からないため、`CreateRightHandSide`では解析に使用したリストも返します。

これは`(Value, List<string>)`というタプル型にすることにより実現します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
     // 省略
    private Formula CreateFormula(List<string> tokens, Formula? left)
    {
        // 省略
    }

+   /// <summary>
+   /// 右辺の値を検証し、
+   /// 乗算・除算であった場合はFormulaクラスを作成しこれをValueの値にして返します。
+   /// 
+   /// そうでない場合は右辺の値を数値にし、これをValueの値にして返します。
+   /// </summary>
+   /// <param name="tokens">検証する計算式のリスト</param>
+   /// <returns>(生成したValue, 計算式のリスト)</returns>
+   /// <exception cref="InvalidEnumArgumentException"></exception>
+   private (Value, List<string>) CreateRightHandSide(List<string> tokens)
+   {
+       // リストに値が1つしかなかった場合は数値のValueを生成して返します。
+       if (tokens.Count == 1)
+       {
+           return (new Value(double.Parse(tokens[0])), []);
+       }
+
+       // 開始位置の次の値が[×÷]であった場合はFormulaクラスを生成し、
+       // これを値としてValueを生成します。
+       if ("×÷".Contains(tokens[1]))
+       {
+           // 左辺を設定します。
+           Value lhs = new(double.Parse(tokens[0]));
+           // Operator型に変換した値を演算子とします。
+           Operator op = tokens[1] switch
+           {
+               "×" => Operator.Multiplication,
+               "÷" => Operator.Division,
+               _ => throw new InvalidEnumArgumentException(),
+           };
+           // 右辺を設定します。
+           (Value, List<string>) rhs = CreateRightHandSide(tokens[2..]);
+           tokens = rhs.Item2;
+           
+           Formula formula = new(lhs, rhs.Item1, op);
+           return (new Value(formula), tokens);
+       }
+
+       // [+-]であった場合は数値のValueを生成します。
+       return (new Value(double.Parse(tokens[0])), tokens);
+   }

       // 省略
}
```

## CreateFormula関数の修正

演算子の設定に[×÷]を加えます。
更に`CreateFormula`内で右辺を設定する際に実装した`CreateRightHandSide`関数を呼び出します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
     // 省略
     
    private Formula CreateFormula(List<string> tokens, Formula? left)
    {
        // leftがnullでなかった場合は、この値を左辺とし、
        // そうでない場合はリストの0番目をdouble型に変換した値を左辺とします。
        dynamic data = left != null 
            ? left 
            : double.Parse(tokens[0]);
        Value lhs = new(data);

        // リストの1番目をOperator型に変換した値を演算子とします。
        Operator op = tokens[1] switch
        {
            "+" => Operator.Addition,
            "-" => Operator.Subtraction,
+           "×" => Operator.Multiplication,
+           "÷" => Operator.Division,
             _ => throw new InvalidEnumArgumentException(),
        };

-       // リストの2番目をdouble型に変換した値を右辺とします。
-       Value rhs = new (double.Parse(tokens[2]));
-
-       Formula formula = new(lhs, rhs, op);
+       // 右辺を設定します。
+       (Value, List<string>) rhs = CreateRightHandSide(tokens[2..]);
+       tokens = rhs.Item2;
+       
+       // 設定したデータを元にFormulaクラスを生成します。
+       Formula formula = new(lhs, rhs.Item1, op);

-       // リストに3番目の値が存在した場合、
-       // 左辺としてFormulaクラスを生成します。
-       if (tokens.Count >= 4)
-       {
-           formula = CreateFormula(tokens[2..], formula);
-       }
+       // リストにまだ式が残っていた場合、
+       // 左辺としてFormulaクラスを生成します。
+       if (tokens.Count >= 3)
+       {
+           formula = CreateFormula(tokens, formula);
+       }

        return formula;
    }

       // 省略
}
```

この実装で様々なパターンの計算に対応できました。

![2025.01.29-01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/228b6dfc-a4c1-8be5-7bc3-9bb3fdea7f39.jpeg)

![2025.01.29-02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/0b36442f-d837-a1eb-6317-383defcecbc6.jpeg)

## 0除算をした場合の処理
最後に0除算をした場合の処理を実装します。

これは`Calculation`メソッドで結果がdouble.Infinityであった場合にメッセージを設定するだけです。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
     // 省略
     
    public void Calculation(string formula)
    {
        try
        {
            List<string> tokens = AnalyzeFormula(formula);
            Formula f = CreateFormula(tokens, null);
            double r = f.Execute();
+           if (double.IsInfinity(r))
+           {
+               Result = "0除算をしています。";
+               return;
+           }
            Result = r.ToString();
        }
        catch (InvalidEnumArgumentException)
        {
            Result = "未知のOperatorが出現しました。";
        }
        catch (ArithmeticException)
        {
            Result = "Valueに値が設定されていません。";
        }
        catch (FormatException)
        {
            Result = "数値のフォーマットが不正です。";
        }
        catch (Exception e)
        {
            Result = $"何らかの例外が発生しました。({e.Message})";
        }
    }

       // 省略
}
```

![2025.01.29-03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/ac0b6098-c989-05dd-1d3a-6b550a9dc464.jpeg)


## おわりに
以上で乗算・除算処理の実装も終わりました。

記事的には割とボリューム少な目で実装できましたが、実際ここに来るまで結構トライアンドエラーをしていました(何だかんだ4半日くらいかかりました)。
加算・減算は1発でうまくいったのですが……。

流石優先順位のある演算子ですね。
過程を全て載せようかとも考えましたが、かなり冗長になりそうでしたので、成功例だけを挙げました。

ただ動作を確認したパターンはそこまで多くないため、潜在的な障害はあるかもしれません。
何か出てきたらその時対応ですね。

----

次はカッコの処理です。
これも同じ優先順位の付く処理ですが、乗算・除算よりも難易度が高いのではないかと震えています。

ここができればこの練習は一旦の完了になりそうです。