これは半年ほど前に書きかけて中断した下書きですが, クレートとしては永遠に完成しなさそうですし, これだけでも多少は有用な気がするので放流しておきます.
角度の表現
まず最初に角度の数値表現について確認しておきます. 良く知られている表現として度数法と弧度法がありますが, 天文学においては時間表記が現代でも残っています.
- 弧度法: 半円がなす角を π = 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 です.
- 時間表記: 半円がなす角を 12h とする単位です. 単位は時間 (hour).
- 小数以下を表すために分 (1h = 60m) と秒 (1m = 60s) を用いることがあります.
- 地球の赤道上の経度差 0h0m2.s16 が 1 メートルに対応します.
- 京都御所は緯度 +2h20m5.s79, 経度 9h3m2.s91 です. このように経度を時間表記するとちょうど時差 (つまり地球が自転するのに要する時間) を与えるため, 経度に相当する量を表記するのに便利なのです (逆に緯度には用いません).
度数法と時間表記で分と秒の呼び方が同じですがその大きさは 15 倍違う (1h = 15°, 1m = 15' 1s = 15'') ので注意が必要です. この区別を明確にするため, 度数法は 分角 (minute of arc, moa) と 秒角 (second of arc, soa), 時間表記は 分 (minute, min) と 秒 (second, sec) と呼ぶことにします.
参考文献: 角度表示 - 天文学辞典 (日本天文学会)
Rust での扱い
角度を扱うプログラムを書くときには, 時間表記で扱っていたつもりなのに度数法の値だった, などのミスが頻繁に起こり得ます. そこで完全に別の型にしてしまえば, そのようなミスをコンパイル時に検出することができるでしょう. 弧度法は普通に f64
で良いので, 度数法と時間表記のために struct Degrees
, Hours
を用意します. おまけに初期化用に new
メソッドもつけておきます.
#[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
トレイトを実装します.
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}
を実装します.
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()
}
}
これで自由に角度が扱えるようになりました. コード例として簡単なテストを載せておきます.
#[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()
みたいな形で呼べるようにしたら便利になると思います.