[Rust] 角度の表記: 弧度法, 度数法, 時間表記

これは半年ほど前に書きかけて中断した下書きですが, クレートとしては永遠に完成しなさそうですし, これだけでも多少は有用な気がするので放流しておきます. 

# 角度の表現

まず最初に角度の数値表現について確認しておきます. 良く知られている表現として度数法と弧度法がありますが, 天文学においては時間表記が現代でも残っています.

* 弧度法: 半円がなす角を π = 3.14... とする単位です. 単位は radian.
    * 地球の赤道上の経度差 1.568E-4 rad が 1 メートルに対応します.
    * 京都御所は緯度 +0.61129 rad, 経度 2.369496 rad です.
* 度数法: 半円がなす角を 180° とする単位です. 単位は度 (degree).
    * 小数以下を表すために分 (1' = 1°/60) と秒 (1'' = 1'/60) を用いることがあります.
    * 地球の赤道上の経度差 0°0'32.34'' が 1 メートルに対応します.
    * 京都御所は緯度 +35°1'26.''83, 経度 135°45'43.''67 です.
* 時間表記: 半円がなす角を 12<sup>h</sup> とする単位です. 単位は時間 (hour).
    * 小数以下を表すために分 (1<sup>h</sup> = 60<sup>m</sup>) と秒 (1<sup>m</sup> = 60<sup>s</sup>) を用いることがあります.
    * 地球の赤道上の経度差 0<sup>h</sup>0<sup>m</sup>2.<sup>s</sup>16 が 1 メートルに対応します.
    * 京都御所は緯度 +2<sup>h</sup>20<sup>m</sup>5.<sup>s</sup>79, 経度 9<sup>h</sup>3<sup>m</sup>2.<sup>s</sup>91 です. このように経度を時間表記するとちょうど時差 (つまり地球が自転するのに要する時間) を与えるため, 経度に相当する量を表記するのに便利なのです (逆に緯度には用いません).

度数法と時間表記で分と秒の呼び方が同じですがその大きさは 15 倍違う (1<sup>h</sup> = 15°, 1<sup>m</sup> = 15' 1<sup>s</sup> = 15'') ので注意が必要です. この区別を明確にするため, 度数法は 分角 (minute of arc, moa) と 秒角 (second of arc, soa), 時間表記は 分 (minute, min) と 秒 (second, sec) と呼ぶことにします.

参考文献: [角度表示 - 天文学辞典 (日本天文学会)](http://astro-dic.jp/expression-of-angle/)

# Rust での扱い

角度を扱うプログラムを書くときには, 時間表記で扱っていたつもりなのに度数法の値だった, などのミスが頻繁に起こり得ます. そこで完全に別の型にしてしまえば, そのようなミスをコンパイル時に検出することができるでしょう. 弧度法は普通に `f64` で良いので, 度数法と時間表記のために struct `Degrees`, `Hours` を用意します. おまけに初期化用に `new` メソッドもつけておきます.

```rust
#[derive(Debug, Clone, Copy)]
pub struct Degrees {
    deg: i16,
    moa: u8,
    soa: f32,
}

#[derive(Debug, Clone, Copy)]
pub struct Hours {
    hour: i8,
    min: u8,
    sec: f32,
}

impl Degrees {
    pub fn new(deg: i16, moa: u8, soa: f32) -> Self {
        Degrees { deg, moa, soa }
    }
}
impl Hours {
    pub fn new(hour: i8, min: u8, sec: f32) -> Self {
        Hours { hour, min, sec }
    }
}
```

なおフィールドアクセスしたければ各フィールドに `pub` が必要です.

文字列にフォーマットするためには `std::fmt::Display` トレイトを実装します.

```rust
use std::fmt;
impl fmt::Display for Degrees {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let decimals = f.precision().unwrap_or(3);
        write!(f, "{}°{}′{:.*}′′", self.deg, self.moa, decimals, self.soa)
    }
}
impl fmt::Display for Hours {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let decimals = f.precision().unwrap_or(3);
        write!(f, "{}h{}m{:.*}s", self.hour, self.min, decimals, self.sec)
    }
}
```

最後に, 弧度法, 度数法, 時間表記の間で自由に変換ができるように `std::convert::{From, Into}` を実装します.

```rust
use std::f64::consts::PI;

use std::convert::From;
impl From<f64> for Degrees {
    fn from(rad: f64) -> Degrees {
        let (sign, rad) = (rad.signum() as i16, rad.abs()%(2.*PI));
        let deg: f64 = rad*180./PI;
        let moa: f64 = deg.fract() * 60.;
        let soa: f64 = moa.fract() * 60.;

        Degrees::new(
            sign * deg.floor() as i16, moa.floor() as u8, soa as f32
        )
    }
}
impl From<f64> for Hours {
    fn from(rad: f64) -> Hours {
        let (sign, rad) = (rad.signum() as i8, rad.abs()%(2.*PI));
        let hour: f64 = rad*12./PI;
        let min: f64 = hour.fract() * 60.;
        let sec: f64 = min.fract() * 60.;

        Hours::new(
            sign * hour.floor() as i8, min.floor() as u8, sec as f32
        )
    }
}

use std::convert::Into;
impl Into<f64> for Degrees {
    fn into(self) -> f64 {
        ( self.deg as f64 + self.deg.signum() as f64 * ( self.moa as f64/60. + self.soa as f64/3600. ) ) * PI/180. 
    }
}
impl Into<f64> for Hours {
    fn into(self) -> f64 {
        ( self.hour as f64 + self.hour.signum() as f64 * ( self.min as f64/60. + self.sec as f64/3600. ) ) * PI/12. 
    }
}

impl From<Hours> for Degrees {
    fn from(hour: Hours) -> Degrees {
        let rad: f64 = hour.into();
        rad.into()
    }
}
impl From<Degrees> for Hours {
    fn from(deg: Degrees) -> Hours {
        let rad: f64 = deg.into();
        rad.into()
    }
}
```

これで自由に角度が扱えるようになりました. コード例として簡単なテストを載せておきます.

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_rad() {
        let rad = 2.369496;
        let deg: Degrees = rad.into();
        let hour: Hours = rad.into();
        println!("[FromRad] {:.3}={:.2}={:.2}", rad, deg, hour)
    }

    #[test]
    fn from_deg() {
        let deg = Degrees::new( 135, 45, 43.67 );
        let rad: f64 = deg.into();
        let hour: Hours = deg.into();
        println!("[FromDeg] {:.2}={}={:.2}", deg, rad, hour);
    }

    #[test]
    fn from_hour() {
        let hour = Hours::new( 9, 3, 2.91 );
        let rad: f64 = hour.into();
        let deg: Degrees = hour.into();
        println!("[FromHour] {:.2}={}={:.2}", hour, rad, deg);
    }
}
```

あとは `FromStr` の実装と, 変換を明示的に `.to_hours()` みたいな形で呼べるようにしたら便利になると思います.