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

:::note info
WinUI3を学びたく、練習として簡単な電卓を作ってみることにしました。
今回は入力したデータを元に計算処理を実装します。

全て一気に実装するのは私のレベルでは難しいので、まずはこの中でも簡単そうな加算・減算処理から実装していこうと思います。

既にこういった解析はスマートな方法が世に出回っていると思いますが、ここではそういった手法を調べず、自分で考えて実装します。

これは少しでも考える力を付けたいからですね。

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

## 前回の記録

https://qiita.com/nogino_sanebou/items/4e285778805d7c6f3631

## 考えてみた実装の流れ
式を左辺、右辺、演算子に分け、「左辺 演算子 右辺」で計算できるようにします。

「1 + 2 + 3 + 4 + 5」のような複数の演算が行われている場合は左辺を入れ子の構造にします。

![数式図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/0c4be8f2-ef7e-9839-6ed0-e5c8cc708c67.jpeg)

後は入れ子の内側から順番に演算していけば解答を導きだせそうです。

この表現をここでは「左辺 演算子 右辺」を一つのクラスとして扱うことで実現しようと思います。

左辺には数字、またはこのクラスが設定され、演算子は列挙型の表現を設定し、右辺は数字を設定するようにします。

一番外側のクラスの演算を発火点とし、左辺にクラスがあれば更にそのクラスで演算をし、そのクラスの中でも左辺がクラスであればまたそのクラスで演算をし、と連鎖をさせていくことで全ての計算を実現します。

### 式の解析処理

ではその形にするためには式をどう解析するかを考えてみたいと思います。

今回は単純な加算、減算処理を実装するため、優先順位の考慮をせず、式の文字列表現を先頭から末尾まで順番に解析していけば実現できそうです。

最初に式を1文字ずつ読み込み、数字とドット(.)が続く限りこれを結合し、演算子が来た時点でクラスを生成し、数値を左辺プロパティーに、演算子は演算子プロパティーに設定します。

更に式の最後、或いは次の演算子が出現するまでを数値とし、この数値が完成した時、右辺プロパティーにこの数値を設定します。

右辺の完成の理由が演算子であった場合、新しくクラスを作成し、左辺プロパティーを最初に、生成したクラスに設定します。

これを繰り返すことにより、上図で表した構造を実現します。

処理は前回作成した`ResultViewModel`の`Calculation`メソッドに作成します。

## クラスの定義
一つの演算を表現するクラス、`Formula`クラスを定義します。

`Formula`クラスにはプロパティーとして左辺プロパティー、演算子プロパティー、右辺プロパティーを持ち、メソッドには計算を実装するメソッドを一つ定義します。

### 演算子の定義
演算子を表現するために、列挙型を定義します。
```diff_c_sharp:ResultModelView.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略

+   /// <summary>
+   /// 演算子を表現します。
+   /// </summary>
+   private enum Operator
+   {
+       Addition, 
+       Subtraction,
+   }
}
```

### 左辺・右辺の定義
左辺・右辺の値には数値、または`Formula`クラスを設定するため、この二つの情報を持てるクラス`Value`を定義します。

#### フィールド変数
`Value`クラスは数値を持つフィールド変数`_number`と、`Formula`クラスを持つフィールド変数`_formula`を定義します。

フィールド変数は`_number`、`_formula`のどちらか片方にしか値が入らないため、これらの変数は`null`を許可するようします。

また、この数値は`double`型で定義します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略

    private enum Operator
    {
        Addition, 
        Subtraction,
    }
+   /// <summary>
+   /// 左辺、右辺の値を表現します。
+   /// このクラスではフィールド変数として数値と計算式クラス(Formula)を保持します。
+   /// </summary>   
+   private class Value
+   {
+       private Formula? _formula = null;
+       private double? _number = null;
+   }
}
```

#### コンストラクター
数値または`Formula`を受け取り、それぞれ数値の場合は`_number`、`Formula`クラスの場合は`_formula`に設定します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略
   
    private class Value
    {
        private Formula? _formula = null;
        private double? _number = null;

+       /// <summary>
+       /// 計算式を設定してこのクラスのインスタンスを生成します。
+       /// </summary>
+       /// <param name="data">計算式</param>
+       public Value(Formula formula)
+       {
+           _formula = formula;
+       }
+       
+       /// <summary>
+       /// 数値を設定してこのクラスのインスタンスを生成します。
+       /// </summary>
+       /// <param name="data">数値</param>
+       public Value(double number)
+       {
+           _number = number;
+       }
    }
}
```

#### Getメソッド
`_number`が`null`でない場合は`_number`をそのまま返し、`null`であった場合は`_formula`の実行結果を返します。

後述しますが、`Formula`クラスの実行結果は`Execute`というメソッドを実装します。

基本的にどちらも`null`であることはありませんが、念のためどちらも`null`であった場合は`ArithmeticException`をスローするようにします。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略
   
    private class Value
    {
           // 省略
 
        public Value(double number)
        {
            _number = number;
        }

+       /// <summary>
+       /// 設定した数値を取得します。
+       /// 値が計算式であった場合は計算結果を取得します。
+       /// </summary>
+       /// <returns>設定した数値</returns>
+       /// <exception cref="ArithmeticException">数値が設定されていなかった場合</exception>
+       public double Get()
+       {
+           if (_number != null)
+           {
+               return (double)_number;
+           }
+           if (_formula != null)
+           {
+               return _formula.Execute();
+           }
+           throw new ArithmeticException();
+       }
    }
```

### Formulaコンストラクターの実装
`Formula`クラスのコンストラクターは左辺・右辺(`Value`)、演算子(`Operator`)を受け取ります。
この値は特に変更する事がありませんので`readonly`を付けておきます。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略
   
    private class Value
    {
        // 省略
    }

+   /// <summary>
+   /// 計算式を表現します。
+   /// Executeメソッドを実行することで式の計算結果を取得できます。
+   /// </summary>
+   /// <param name="lhs">左辺の値</param>
+   /// <param name="rhs">右辺の値</param>
+   /// <param name="op">演算子</param>
+   private class Formula(Value lhs, Value rhs, Operator op)
+   {
+       private readonly Value _lhs = lhs;
+       private readonly Value _rhs = rhs;
+       private readonly Operator _operator = op;
+   }
}
```

### 実行処理
実行処理は`Execute`メソッドを定義します。
`_operator`の値によって、加算または減算をします。
```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(),
+               _ => throw new InvalidEnumArgumentException(),
+           };
+       }
    }
}
```

## 解析処理の実装
最初に式の文字列表現を一文字ずつ読み込んでいき、数字・ドット(.)が文字列の最後または演算子までを一つの文字列として結合し、リストに追加します。
演算子はその一文字を`string`型にしてリストに追加します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略

+   /// <summary>
+   /// 計算式を解析し、数値と演算子に切り分けたリストを生成します。
+   /// </summary>
+   /// <param name="formula">計算式</param>
+   /// <returns>数値と演算子を切り分けたリスト</returns>
+   private List<string> AnalyzeFormula(string formula)
+   {
+       StringBuilder sb = new();
+       List<string> tokens = new();
+       foreach (char token in formula.ToArray())
+       {
+           switch (token)
+           {
+               // 演算子が出現した場合
+               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;
+   }

    /// <summary>
    /// 演算子を表現します。
    /// </summary>
    private enum Operator
    {
        Addition, 
        Subtraction,
    }
    
       // 省略
}
```

## Formulaクラスの生成処理
解析したリストを元に`Formula`クラスを生成します。

リストを順番に読み込んでいき、左辺・右辺・演算子プロパティーを取り出します。
このデータを元に`Formula`クラスを生成します。

右辺の後に更に演算子が出現した場合、次の右辺までを取り出し、左辺を先ほど生成した`Formula`クラスにし、後はリストに出現した順に値を右辺と演算子にして`Formula`クラスを生成します。

更に演算子が続いた場合、同様に処理していきます。

リストを読み終わった後、最初に生成した`Formula`クラスを返します。

```diff_c_sharp:ResultViewModel.cs
public partial class ResultViewModel : INotifyPropertyChanged
{
       // 省略

    private List<string> AnalyzeFormula(string formula)
    {
           // 省略
    }

+   /// <summary>
+   /// 計算式のリストよりFormulaクラスを生成します。
+   /// </summary>
+   /// <param name="tokens">計算式のリスト</param>
+   /// <param name="left">左辺が計算式である場合はここにインスタンスが渡ります</param>
+   /// <returns>Formulaクラス</returns>
+   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,
+            _ => throw new InvalidEnumArgumentException(),
+       };
+
+       // リストの2番目をdouble型に変換した値を右辺とします。
+       Value rhs = new (double.Parse(tokens[2]));
+
+       // 設定したデータを元にFormulaクラスを生成します。
+       Formula formula = new (lhs, rhs, op);
+
+       // リストに3番目の値が存在した場合、
+       // 左辺としてFormulaクラスを生成します。
+       if (tokens.Count >= 4)
+       {
+           formula = CreateFormula(tokens[2..], formula);
+       }
+
+       return formula;
+   }
    
       // 省略
}
```

## 計算処理を実行
最後に`Calculation`メソッドに計算処理を実装します。

ここの処理は単純で、`AnalyzeFormula`で計算式を解析し、`CreateFormula`でその解析結果を元に`Formula`クラスを生成し、生成した`Formula`クラスから`Execute`メソッドを実行し、計算結果を取得します。

取得した計算結果を文字列にし、`ResultViewModel`の`Result`プロパティーに設定します。

また、例外が発生した場合も対応するエラーメッセージを`Result`プロパティーに設定します。

| 例外内容                      | エラーメッセージ |
|:-----------------------------|:------------|
| InvalidEnumArgumentException | 未知のOperatorが出現しました。 |
| ArithmeticException          | Valueに値が設定されていません。 |
| FormatException              | 数値のフォーマットが不正です。 |
| その他例外                    | 何らかの例外が発生しました。(例外クラス名) |

```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();
+           Result = r.ToString();
+       }
+       catch (InvalidEnumArgumentException)
+       {
+           Result = "未知のOperatorが出現しました。";
+       }
+       catch (ArithmeticException)
+       {
+           Result = "Valueに値が設定されていません。";
+       }
+       catch (FormatException)
+       {
+           Result = "数値のフォーマットが不正です。";
+       }
+       catch (Exception e)
+       {
+           Result = $"何らかの例外が発生しました。({e.Message})";
+       }
    }
    
       // 省略
}
```

## おわりに
以上で加算と減算処理の実装を終わります。

次回は乗算・除算を加えた四則演算の処理を実装します。
優先順位がでてくるのでもう少し頭を捻る必要がありますね。

`AnalyzeFormula`の処理がカギになってくるのではないかと思っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/4fe06814-d713-bb21-b03b-287154fdfbd6.png)

## 次の記事

https://qiita.com/nogino_sanebou/items/9655dd9b7040652355bc