【Unity】角度を扱うときはfloatじゃなくて専用のAngle構造体を用意すると捗る

##角度の扱いづらさ
角度を普通にfloatやdoubleで扱おうとすると、次のような問題が発生して扱いづらいです。

- __度数法(degree)__なのか__弧度法(radian)__なのかわからない
- 360°以上になると__角度値は違うけど見た目は同じ__になってしまいややこしくなる
    - 360°と0°では角度値は違えど見た目は変わらない
    - 180°と-180°、45°と405°なども同様。見た目は同じだけど内部値が全く違うので意図しない動きになったりする

これらの問題を専用の変換メソッド等を用意して解決しても良いのですが、__グローバルなメソッドにすれば角度以外の`float`値に対して適切ではないメソッドが呼べてしまう__し、__必要な部分だけにスコープを限定したメソッドとして定義すれば再利用性が失われてしまいます__。

それに、「角度」のように何らかの意味のある数値として扱う場合は、プリミティブ型をそのまま代用するのではなく、専用のオブジェクトとして定義したほうが、専用の操作をオブジェクト内に隠蔽することができるため、可読性や保守性に富みます。

というわけで、角度を表すAngle構造体を作りました。

##角度を表す`Angle`構造体

作成した`Angle`構造体の機能を紹介します。
今すぐコードが見たい方は[こちら](#ソースコード本体)。


###インスタンスの生成を行うファクトリ

度数法と弧度法を混同させないため、コンストラクタは隠蔽しています。
その代わりに各種ファクトリメソッドを用意しています。

####`Angle.FromDegree`ファクトリメソッド
度数法の値から`Angle`構造体のインスタンスを取得します。

```csharp
Angle angle = Angle.FromDegree(60);
```

周回数を指定することもできます。

```csharp
Angle angle = Angle.FromDegree(1, 60); //360°+60°
```

####`Angle.FromRadian`ファクトリメソッド
弧度法の値から`Angle`構造体のインスタンスを取得します。

```csharp
Angle angle = Angle.FromRadian(UnityEngine.Mathf.PI);
```

こちらも同様に、周回数を指定することもできます。

```csharp
Angle angle = Angle.FromRadian(-2, UnityEngine.Mathf.PI); //-4π+π
```

####`Angle.Zero`ファクトリプロパティ
角度が0の`Angle`構造体のインスタンスを取得します。

```csharp
Angle angle = Angle.Zero; //0°
```

####`Angle.Round`ファクトリプロパティ
角度が360°の`Angle`構造体のインスタンスを取得します。

```csharp
Angle angle = Angle.Round; //360°
```

###各種変換を行うメソッド

各種変換メソッドを提供します。
イミュータブルな設計とするため、変換メソッドを実行しても元のインスタンスは変更されず、新しいインスタンスを返すようになっています。

####`Normalize`メソッド
角度を-180°<θ<=180°の範囲で正規化します。

<img width="200px" alt="Normalize.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/302e2c96-c802-9087-ab4f-96a753461e5e.png">

例えば、225°の角度は-180°<θ<180°の間には入っていないため、-135°に正規化されます。

```csharp
Angle angle = Angle.FromDegree(225).Normalize(); //-135°
```

![Normalize1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/11859f43-7632-204e-926d-fcbd2abaab4a.png)

同様に、-450°は-90°に正規化されます。

```csharp
Angle angle = Angle.FromDegree(-450).Normalize(); //-90°
```

![Normalize2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/ea9f62b3-fdf4-fd3d-c700-807044f493ce.png)

####`PositiveNormalize`メソッド
角度を0°<=θ<360°の範囲で正規化します。

<img width="200px" alt="PositiveNormalize.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/a29d20b0-a428-b803-22e4-d12309f70552.png">

例えば、-135°を0°<=θ<360°の範囲に正規化すると、225°となります。

```csharp
Angle angle = Angle.FromDegree(-135).PositiveNormalize(); //225°
```

![PositiveNormalize1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/cf3b40f4-ca8d-c1b4-b2e1-f1e0d1046702.png)

同様に、-450°は270°に正規化されます。

```csharp
Angle angle = Angle.FromDegree(-450).PositiveNormalize(); //270°
```

![PositiveNormalize2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/7d1a6dcb-beef-02a5-214a-0263717ddfd4.png)

####`Reverse`メソッド
`Reverse`メソッドは、次のように見た目上の角度を変更せずに、方向のみを反転させます。

```csharp
Angle angle = Angle.FromDegree(90).Reverse(); //-270°
```

![Reverse1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/f482a52d-27ad-0c6f-bc5d-e97297cc1486.png)

```csharp
Angle angle = Angle.FromDegree(-450).Reverse(); //630°
```

![Reverse2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/b0219221-ac0f-e3a3-de8b-326324849ed1.png)

####`SignReverse`メソッド
`SignReverse`メソッドは、角度の符号を単純に反転させます。

```csharp
Angle angle = Angle.FromDegree(90).SignReverse(); //-90°
```

![SignReverse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/123adf3e-da33-9b94-c777-beb2cd880d05.png)

####`Absolute`メソッド
`Absolute`メソッドは、`SignReverse`メソッドの正の方向への片道切符バージョンです。

```csharp
Angle angle = Angle.FromDegree(90).Absolute(); //90°
Angle angle = Angle.FromDegree(-90).Absolute(); //90°
```

###情報の取得を行うプロパティ
`Angle`構造体から角度情報を取得することができます。

####`TotalDegree`プロパティ
角度値を度数法で取得します。

```csharp
float deg1 = Angle.FromRadian(UnityEngine.Mathf.PI).TotalDegree; //180f
float deg2 = Angle.FromDegree(-1, -90).TotalDegree; //-450f
```

####`TotalRadian`プロパティ
角度値を弧度法で取得します。

```csharp
float rad1 = Angle.FromRadian(UnityEngine.Mathf.PI).TotalRadian; //π
float rad2 = Angle.FromDegree(-1, -90).TotalRadian; //-5π/4
```

####`NormalizedDegree`プロパティ
`Normalize`した角度値を度数法で取得します。

```csharp
float deg1 = Angle.FromRadian(UnityEngine.Mathf.PI).NormalizedDegree; //180f
float deg2 = Angle.FromDegree(-1, -90).NormalizedDegree; //-90f
```

####`NormalizedRadianプロパティ`
`NormalizedDegree`プロパティの弧度法バージョン。

####`PositiveNormalizedDegree`プロパティ
`NormalizedDegree`プロパティの`PositiveNormalize`したバージョン。

####`PositiveNoramlizedRadian`プロパティ
`PositiveNormalizedDegree`プロパティの弧度法バージョン。

####`Lap`プロパティ
角度が何周しているかを取得します。

```csharp
int lap1 = Angle.FromDegree(180).Lap; //0
int lap2 = Angle.FromDegree(360).Lap; //1
int lap3 = Angle.FromDegree(-730).Lap; //-2
```

####`IsCircled`プロパティ
角度が1周以上回っているかどうかを取得します。

```csharp
bool circled1 = Angle.FromDegree(180).IsCircled; //false
bool circled2 = Angle.FromDegree(360).IsCircled; //true
bool circled3 = Angle.FromDegree(-730).IsCircled; //true
```

####`IsTrueCircle`プロパティ
角度が360°の倍数かどうかを取得します。

```csharp
bool trueCircle1 = Angle.FromDegree(180).IsTrueCircled; //false
bool trueCircle2 = Angle.FromDegree(360).IsTrueCircled; //true
bool trueCircle3 = Angle.FromDegree(-730).IsTrueCircled; //false
```

####`IsPositive`プロパティ
正の方向への角度かどうか取得します。

```csharp
bool circled1 = Angle.FromDegree(180).IsPositive; //true
bool circled2 = Angle.FromDegree(360).IsPositive; //true
bool circled3 = Angle.FromDegree(-730).IsPositive; //false
```

###演算子
各種演算子をオーバーロードしており、プリミティブ型と同じように各種演算をすることができます。

####+-演算子
角度を加算/減算します。

```csharp
var plusAngle = Angle.FromDegree(120) + Angle.Round; //480°
var minusAngle = Angle.FromDegree(45) - Angle.Round; //-315°
```

####*/演算子
角度を実数で乗算/除算します。

```csharp
var multiAngle = Angle.FromDegree(120) * -3; //-360°
var divideAngle = Angle.FromDegree(120) / 4; //30°
```

####==,!=,<,<=,>,>=演算子
角度の大きさを比較します。

```csharp
var b1 = Angle.FromDegree(90) == Angle.FromDegree(90); //true 
var b2 = Angle.FromDegree(90) != Angle.FromDegree(450); //true 
var b3 = Angle.FromDegree(90) < Angle.FromDegree(45); //false
var b4 = Angle.FromDegree(90) <= Angle.FromDegree(90); //true 
var b5 = Angle.FromDegree(90) > Angle.FromDegree(45); //true 
var b6 = Angle.FromDegree(90) >= Angle.FromDegree(45); //true 
```

###インターフェイス実装
次の2つのインターフェイスを実装しています。

####`IEquatable<Angle>`インターフェイス

等価性比較のために`IEquatable<Angle>`インターフェイスを実装しています。

==演算子があるのにわざわざ`IEquatable<Angle>`インターフェイスを実装するメリットは__[この記事](https://qiita.com/cactuaroid/items/f277a097ecf51eb247c0)__が参考になりますが、要約すると次のようになります。

- `IEquatable<Angle>`がないと`object`版の`Equals(object obj)`が呼ばれることになる。値型を`object`にキャストすると__ボックス化__が発生するので__オーバーヘッドがかかってしまう__。
- 構造体の`object`版の`Equals(object obj)`メソッドの既定の動作は、すべてのフィールドの等価性を比較すること。これが==演算子の比較内容と異なると、__==演算子の結果と`Equals`メソッドの結果に相違が生じ、混乱を招いてしまう__。
    - しかも、等価性比較はリフレクションを用いて行われる模様なので、__速度が圧倒的に遅い__。

このような理由から、構造体の場合は基本的に`IEquatable<T>`インターフェイスを実装したほうが良いようです。

####`IComparable<Angle>`インターフェイス

LINQの`OrderBy`メソッドや`Max`,`Min`メソッドを利用できるようにするために`IComparable<Angle>`インターフェイスを実装しています。


###オーバーライド

`object`型に定義されている次のメソッドをオーバーライドしています。

####`ToString`メソッド
周回数と残りの角度を返します。

```csharp
string str = Angle.FromDegree(2, 45).ToString(); //2x + 45°
```

`ToString`をオーバーライドしていると、VisualStudioのデータヒントにも同様のフォーマットで表示されるので便利です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/22538a48-f3c1-f500-5d50-5b7aa8c58119.png)

####`Equals(object o)`メソッド

`IEquatable<Angle>`インターフェイスの`Equals`メソッドも実装したのですが、`object`クラスの`Equals`メソッドもオーバーライドしています。
理由としては、前述の通り`Equals(object o)`メソッドの既定の動作が全フィールドの等価性比較であること、しかもそれがリフレクションによる比較であるためです。
`Angle`構造体は内部で持つ角度値を単純に比較するだけで良いので、リフレクションを使う既定の動作をオーバーライドして封印します。

ちなみに、VisualStudioでは==演算子をオーバーロードすると`Equals`メソッドと`GetHashCode`メソッドもオーバーライドしなさいと警告が出ます。
==演算子をオーバーロードしている→自前で等価性比較処理が書けている→既定の`Equals`を使う必要がない→だったらオーバーライドしろ、ということですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/1ccd910b-c32c-72e5-00e0-cf497748bc90.png)

####`GetHashCode`メソッド
`GetHashCode`メソッドは`Dictionary`のキーとして使うときに使われるようです。

>A hash code is a numeric value that is used to insert and identify an object in a hash-based collection such as the Dictionary<TKey,TValue> class, the Hashtable class, or a type derived from the DictionaryBase class. The GetHashCode method provides this hash code for algorithms that need quick checks of object equality.

https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-5.0

これをオーバーライドしないと`Dictionary `のキーとして使ったときに正しく動作しない可能性がある模様。

VisualStudioがオーバーライドをおすすめしてくれたのでオーバーライドします。
しかも有能VisualStudioが中身も自動で実装してくれます。

ぶっちゃけあまり詳しくない。


##ソースコード本体

上記機能を備えた`Angle`構造体のソースコードです。
そのままコピペで使えます。

[追記]
編集リクエスト頂きライセンスを明記しました。
改変等自由ですので是非ご利用ください。

```csharp

/*
Angle.cs

Copyright (c) 2021 yutorisan

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System;
using UnityEngine;

namespace UnityUtility
{
    /// <summary>
    /// 角度
    /// </summary>
    public readonly struct Angle : IEquatable<Angle>, IComparable<Angle>
    {
        /// <summary>
        /// 正規化していない角度の累積値
        /// </summary>
        private readonly float m_totalDegree;

        /// <summary>
        /// 角度を度数法で指定して、新規インスタンスを作成します。
        /// </summary>
        /// <param name="angle">度数法の角度</param>
        /// <exception cref="NotFiniteNumberException"/>
        private Angle(float angle) => m_totalDegree = ArithmeticCheck(() => angle);
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを作成します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="angle">度数法の角度</param>
        /// <exception cref="NotFiniteNumberException"/>
        /// <exception cref="OverflowException"/>
        private Angle(int lap, float angle) => m_totalDegree = ArithmeticCheck(() => checked(360 * lap + angle));

        /// <summary>
        /// 度数法の値を使用して新規インスタンスを取得します。
        /// </summary>
        /// <param name="degree">度数法の角度(°)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromDegree(float degree) => new Angle(degree);
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを取得します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="degree">度数法の角度(°)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromDegree(int lap, float degree) => new Angle(lap, degree);
        /// <summary>
        /// 弧度法の値を使用して新規インスタンスを取得します。
        /// </summary>
        /// <param name="radian">弧度法の角度(rad)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromRadian(float radian) => new Angle(RadToDeg(radian));
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを取得します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="radian">弧度法の角度(rad)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromRadian(int lap, float radian) => new Angle(lap, RadToDeg(radian));
        /// <summary>
        /// 角度0°の新規インスタンスを取得します。
        /// </summary>
        public static Angle Zero => new Angle(0);
        /// <summary>
        /// 角度360°の新規インスタンスを取得します。
        /// </summary>
        public static Angle Round => new Angle(360);

        public bool Equals(Angle other) => m_totalDegree == other.m_totalDegree;

        public override int GetHashCode() => -1748791360 + m_totalDegree.GetHashCode();

        public override string ToString() => $"{Lap}x + {m_totalDegree - Lap * 360}°";

        public override bool Equals(object obj)
        {
            if (obj is Angle angle) return Equals(angle);
            else return false;
        }

        public int CompareTo(Angle other) => m_totalDegree.CompareTo(other.m_totalDegree);

        /// <summary>
        /// 正規化された角度(-180° &lt; degree &lt;= 180°)を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle Normalize() => new Angle(NormalizedDegree);

        /// <summary>
        /// 正の値で正規化された角度(0° &lt;= degree &lt; 360°)を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle PositiveNormalize() => new Angle(PositiveNormalizedDegree);

        /// <summary>
        /// 方向を反転させた角度を取得します。
        /// 例：90°→-270°, -450°→630°
        /// </summary>
        /// <returns></returns>
        public Angle Reverse()
        {
            //ゼロならゼロ
            if (this == Zero) return Zero;
            //真円の場合は真逆にする
            if (IsTrueCircle) return new Angle(-Lap, 0);
            if (IsCircled)
            { //1周以上している
                if (IsPositive)
                { //360~
                    return new Angle(-Lap, NormalizedDegree - 360);
                }
                else
                { //~-360
                    return new Angle(-Lap, NormalizedDegree + 360);
                }
            }
            else
            { //1周していない
                if (IsPositive)
                { //0~360
                    return new Angle(m_totalDegree - 360);
                }
                else
                { //-360~0
                    return new Angle(m_totalDegree + 360);
                }
            }
        }
        /// <summary>
        /// 符号を反転させた角度を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle SignReverse() => new Angle(-m_totalDegree);
        /// <summary>
        /// 角度の絶対値を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle Absolute() => IsPositive ? this : SignReverse();

        /// <summary>
        /// 正規化していない角度値を取得します。
        /// </summary>
        public float TotalDegree => m_totalDegree;
        /// <summary>
        /// 正規化していない角度値をラジアンで取得します。
        /// </summary>
        public float TotalRadian => DegToRad(TotalDegree);
        /// <summary>
        /// 正規化された角度値(-180 &lt; angle &lt;= 180)を取得します。
        /// </summary>
        public float NormalizedDegree
        {
            get
            {
                float lapExcludedDegree = m_totalDegree - (Lap * 360);
                if (lapExcludedDegree > 180) return lapExcludedDegree - 360;
                if (lapExcludedDegree <= -180) return lapExcludedDegree + 360;
                return lapExcludedDegree;
            }
        }
        /// <summary>
        /// 正規化された角度値をラジアン(-π &lt; rad &lt; π)で取得します。
        /// </summary>
        public float NormalizedRadian => DegToRad(NormalizedDegree);
        /// <summary>
        /// 正規化された角度値(0 &lt;= angle &lt; 360)を取得します。
        /// </summary>
        public float PositiveNormalizedDegree
        {
            get
            {
                var normalized = NormalizedDegree;
                return normalized >= 0 ? normalized : normalized + 360;
            }
        }

        /// <summary>
        /// 正規化された角度値をラジアン(0 &lt;= rad &lt; 2π)で取得します。
        /// </summary>
        public float PositiveNormalizedRadian => DegToRad(PositiveNormalizedDegree);
        /// <summary>
        /// 角度が何周しているかを取得します。
        /// 例：370°→1周, -1085°→-3周
        /// </summary>
        public int Lap => ((int)m_totalDegree) / 360;
        /// <summary>
        /// 1周以上しているかどうか(360°以上、もしくは-360°以下かどうか)を取得します。
        /// </summary>
        public bool IsCircled => Lap != 0;
        /// <summary>
        /// 360の倍数の角度であるかどうかを取得します。
        /// </summary>
        public bool IsTrueCircle => IsCircled && m_totalDegree % 360 == 0;
        /// <summary>
        /// 正の角度かどうかを取得します。
        /// </summary>
        public bool IsPositive => m_totalDegree >= 0;

        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator +(Angle left, Angle right) => new Angle(ArithmeticCheck(() => left.m_totalDegree + right.m_totalDegree));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator -(Angle left, Angle right) => new Angle(ArithmeticCheck(() => left.m_totalDegree - right.m_totalDegree));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator *(Angle left, float right) => new Angle(ArithmeticCheck(() => left.m_totalDegree * right));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator /(Angle left, float right) => new Angle(ArithmeticCheck(() => left.m_totalDegree / right));
        public static bool operator ==(Angle left, Angle right) => left.m_totalDegree == right.m_totalDegree;
        public static bool operator !=(Angle left, Angle right) => left.m_totalDegree != right.m_totalDegree;
        public static bool operator >(Angle left, Angle right) => left.m_totalDegree > right.m_totalDegree;
        public static bool operator <(Angle left, Angle right) => left.m_totalDegree < right.m_totalDegree;
        public static bool operator >=(Angle left, Angle right) => left.m_totalDegree >= right.m_totalDegree;
        public static bool operator <=(Angle left, Angle right) => left.m_totalDegree <= right.m_totalDegree;

        /// <summary>
        /// 演算結果が数値であることを確かめる
        /// </summary>
        /// <param name="func"></param>
        /// <returns></returns>
        private static float ArithmeticCheck(Func<float> func)
        {
            var ans = func();
            if (float.IsInfinity(ans)) throw new NotFiniteNumberException("演算の結果、角度が正の無限大または負の無限大になりました");
            if (float.IsNaN(ans)) throw new NotFiniteNumberException("演算の結果、角度がNaNになりました");
            return ans;
        }
        private static float RadToDeg(float rad) => rad * 180 / Mathf.PI;
        private static float DegToRad(float deg) => deg * (Mathf.PI / 180);
    }
}


```

##単体テスト

`Angle`構造体はこれから長く使い続けられそうなので、きちんと単体テストを行いました。
次の記事を参考にしながら、__xUnit__ と __Chainning Assertion__を使ってテストコードを記述しました。
__[xUnit.net でユニットテストを始める](https://qiita.com/takutoy/items/84fa6498f0726418825d#chainning-assertion)__


以下がテストコードです。
`IComparable<Angle>`インターフェイスを実装したことにより、`OrderBy`による並べ替えも成功しています。

例外処理も可能な限りチェックしたかったのですが、`Angle`構造体は内部に単精度浮動小数点型(`float`)を使っているので、どうしても最大値・最小値の扱いが難しいです（例えば`float.MaxValue+1000`などとしても、`float`の有効数字は7桁なので1000が丸め込まれてしまう）。
そのため明確に`NaN`もしくは`Infinity`または`NegativeInfinity`になった場合のみ例外の発生をチェックしました。
実際、`3.40282347×10^38°`なんて角度を使うことはほぼありえないため目をつむります。__`float`の仕様に依存する__と言えば言い訳になるかも。



```csharp

using System;
using UnityUtility;
using UnityEngine;
using Xunit;
using System.Linq;
using System.Collections.Generic;

namespace AngleStructUnitTest
{
    public class UnitTest1
    {
        [Fact]
        public void CreateInstance()
        {
            Angle.FromDegree(180).Is(Angle.FromRadian(Mathf.PI));
            Angle.FromRadian(-4 * Mathf.PI).Is(Angle.FromDegree(-720));
            Angle.FromDegree(-1, -180).Is(Angle.FromDegree(-360 + -180));
            Angle.FromRadian(-10, Mathf.PI).Is(Angle.FromDegree(-3600 + 180));
            Angle.Zero.Is(Angle.FromRadian(0));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.NaN));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.NegativeInfinity));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.PositiveInfinity));
        }

        [Fact]
        public void Normalize()
        {
            Angle.Zero.Normalize().Is(Angle.Zero);
            Angle.FromDegree(180).Normalize().Is(Angle.FromDegree(180));
            Angle.FromDegree(270).Normalize().Is(Angle.FromDegree(-90));
            Angle.FromDegree(360).Normalize().Is(Angle.FromDegree(0));
            Angle.FromDegree(360 * 4 + 20).Normalize().Is(Angle.FromDegree(20));
            Angle.FromDegree(-360 * 80 + 20).Normalize().Is(Angle.FromDegree(20));
        }

        [Fact]
        public void PositiveNormalize()
        {
            Angle.FromDegree(0).PositiveNormalize().Is(Angle.Zero);
            Angle.FromDegree(180).PositiveNormalize().Is(Angle.FromDegree(180));
            Angle.FromDegree(270).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(360).PositiveNormalize().Is(Angle.FromDegree(0));
            Angle.FromDegree(380).PositiveNormalize().Is(Angle.FromDegree(20));
            Angle.FromDegree(-90).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(-360 - 90).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(-360 * 5 + 90).PositiveNormalize().Is(Angle.FromDegree(90));
        }

        [Fact]
        public void Reverse()
        {
            Angle.Zero.Reverse().Is(Angle.Zero);
            Angle.FromDegree(45).Reverse().Is(Angle.FromDegree(-315));
            Angle.FromDegree(-90).Reverse().Is(Angle.FromDegree(270));
            Angle.FromDegree(180).Reverse().Is(Angle.FromDegree(-180));
            Angle.FromDegree(360).Reverse().Is(Angle.FromDegree(-360));
            Angle.FromDegree(359).Reverse().Is(Angle.FromDegree(-1));
            Angle.FromDegree(361).Reverse().Is(Angle.FromDegree(-1, -359));
            Angle.FromDegree(-450).Reverse().Is(Angle.FromDegree(360 + 270));
            Angle.FromDegree(2, 90).Reverse().Is(Angle.FromDegree(-2, -270));
        }

        [Fact]
        public void SignReverse()
        {
            Angle.Zero.SignReverse().Is(Angle.Zero);
            Angle.FromDegree(60).SignReverse().Is(Angle.FromDegree(-60));
            Angle.FromDegree(-120).SignReverse().Is(Angle.FromDegree(120));
            Angle.FromDegree(-2, 60).SignReverse().Is(Angle.FromDegree(2, -60));
        }

        [Fact]
        public void Absolute()
        {
            Angle.Zero.Absolute().Is(Angle.Zero);
            Angle.FromDegree(60).Absolute().Is(Angle.FromDegree(60));
            Angle.FromDegree(-120).Absolute().Is(Angle.FromDegree(120));
            Angle.FromDegree(-4, 60).Absolute().Is(Angle.FromDegree(4, -60));
            Angle.FromDegree(4, -60).Absolute().Is(Angle.FromDegree(4, -60));
        }

        [Fact]
        public void StandardMethods()
        {
            Angle.FromDegree(3, 270).ToString().Is("3x + 270°");
            Angle.FromDegree(90).Equals(Angle.FromDegree(1, 90).Normalize()).IsTrue();
            Angle.FromDegree(45).Equals(45).IsFalse();

            object o = Angle.FromDegree(135);
            Angle.FromDegree(135).Equals(o).IsTrue();
            Angle.Round.Equals(null).IsFalse();
        }

        [Fact]
        public void Operator()
        {
            (Angle.FromDegree(45) + Angle.FromDegree(90)).Is(Angle.FromDegree(135));
            (Angle.FromDegree(30) - Angle.FromDegree(90)).Is(Angle.FromDegree(-60));
            (Angle.FromDegree(90) * 4.5f).Is(Angle.FromDegree(90 * 4.5f));
            (Angle.FromDegree(45) * -1).Reverse().Is(Angle.FromDegree(315));
            (Angle.FromDegree(90) * 0).Is(Angle.Zero);
            (Angle.FromDegree(4, 90) / 2).Is(Angle.FromDegree(2, 45));
            (Angle.FromDegree(5, 10).Normalize() == Angle.FromDegree(10)).IsTrue();
            (Angle.FromDegree(-5, 10).PositiveNormalize() == Angle.FromDegree(10)).IsTrue();
            (Angle.FromDegree(90).Reverse() != Angle.FromDegree(-90)).IsTrue();
            (Angle.FromDegree(45) > Angle.FromDegree(90)).IsFalse();
            (Angle.FromDegree(-1, 0) > Angle.FromDegree(-360)).IsFalse();
            (Angle.FromDegree(-1, 0) >= Angle.FromDegree(-360)).IsTrue();
            (Angle.FromDegree(-1, 20) < Angle.FromDegree(-360)).IsFalse();
            (Angle.FromDegree(1, 45).Normalize() <= Angle.FromDegree(45)).IsTrue();
            (Angle.FromDegree(1, 45).Normalize() <= Angle.FromDegree(90)).IsTrue();

            Assert.Throws<NotFiniteNumberException>(() => Angle.FromDegree(float.MaxValue) + Angle.FromDegree(float.MaxValue));
            Assert.Throws<NotFiniteNumberException>(() => Angle.Zero - Angle.FromDegree(float.MaxValue) * 2);
            Assert.Throws<NotFiniteNumberException>(() => Angle.Round / 0);

        }

        [Fact]
        private void Getter()
        {
            Angle.FromDegree(2, 90).TotalDegree.Is(810);
            Angle.FromDegree(2, 90).Normalize().TotalRadian.Is(Mathf.PI / 2);
            Angle.Zero.NormalizedDegree.Is(0);
            Angle.FromDegree(2, 90).NormalizedDegree.Is(90);
            Angle.FromDegree(-1, -90).NormalizedRadian.Is(-1 * Mathf.PI / 2);
            Angle.Zero.PositiveNormalizedDegree.Is(0);
            Angle.FromDegree(1, 90).Reverse().PositiveNormalizedDegree.Is(90);
            Angle.FromDegree(-2, 90).PositiveNormalizedRadian.Is(Mathf.PI / 2);
            Angle.FromDegree(3, 90).Lap.Is(3);
            Angle.FromDegree(360).Lap.Is(1);
            Angle.FromDegree(-180).Lap.Is(0);
            Angle.FromDegree(-750).Lap.Is(-2);
            Angle.FromDegree(-360).IsCircled.IsTrue();
            Angle.FromDegree(1, -1).IsCircled.IsFalse();
            Angle.FromDegree(1).IsPositive.IsTrue();
            (Angle.FromDegree(1) * -1).IsPositive.IsFalse();
            Angle.Round.IsTrueCircle.IsTrue();
            Angle.Zero.IsTrueCircle.IsFalse();
            Angle.FromDegree(180).IsTrueCircle.IsFalse();
            Angle.FromDegree(-720).IsTrueCircle.IsTrue();
        }

        [Fact]
        private void Compare()
        {
            var collection = new List<Angle>
            {
                Angle.FromRadian(MathF.PI / 2),
                Angle.FromDegree(45),
                Angle.FromDegree(-90),
                Angle.Zero,
                Angle.Round
            };

            collection.OrderBy(a => a).SequenceEqual(new List<Angle>
            {
                Angle.FromDegree(-90),
                Angle.Zero,
                Angle.FromDegree(45),
                Angle.FromRadian(MathF.PI / 2),
                Angle.Round
            }).IsTrue();
            collection.Max().Is(Angle.Round);
            collection.Min().Is(Angle.FromDegree(-90));
        }
    }
}

```

###テスト結果
すべて合格。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/c5edb573-e835-3cbd-4f96-2f61d4c0b9bc.png)


##最後に
角度の扱いは簡単なようで地味に厄介でした。
その厄介な部分を全部隠蔽してきれいな部分だけを外部に公開したこの`Angle`構造体、少し使ってみたのですが普通に便利です。（自画自賛）
みなさんも自己責任でよかったらどうぞ。

以下GitHubでも公開しています。

https://github.com/yutorisan/UnityUtility/blob/develop/UnityUnility/Structs/Angle.cs


##2020/2/7追記：DOTweenでTweenできるようにしました
DOTweenにAngle構造体を投入してTweenできるようにするDOTweenのPluginを作成しました。
下記記事を参照ください。

https://qiita.com/yutorisan/items/1eaff2322fbe28a9fbcb

Plugin本体は下記GitHubを参照ください。

https://github.com/yutorisan/DOTweenPlugin-For-AngleStruct