use std::f64::consts::PI;
use std::ops::{Add, Div, Mul, Sub};
use crate::astronomy::ops;
use crate::models::rounding::Rounding;
use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike};
pub trait Normalize {
fn normalized_to_scale(&self, max: f64) -> f64;
}
impl Normalize for f64 {
fn normalized_to_scale(&self, max: f64) -> f64 {
self - (max * (self / max).floor())
}
}
pub trait Stride {
fn tomorrow(&self) -> Self;
fn yesterday(&self) -> Self;
fn julian_day(&self) -> f64;
fn adjust_time(&self, minutes: i64) -> Self;
fn next_date(&self, fwd: bool) -> Self;
fn rounded_minute(&self, rounding: Rounding) -> Self;
}
impl<Tz: TimeZone> Stride for DateTime<Tz> {
fn tomorrow(&self) -> Self {
self.next_date(true)
}
fn yesterday(&self) -> Self {
self.next_date(false)
}
fn julian_day(&self) -> f64 {
ops::julian_day(
self.year() as i32,
self.month() as i32,
self.day() as i32,
0.0,
)
}
fn rounded_minute(&self, rounding: Rounding) -> Self {
let adjusted = self.clone();
let seconds = adjusted.second();
match rounding {
Rounding::Nearest => {
let rounded = ((seconds as f64) / 60.0).round() as i64;
let adjusted_seconds = seconds as i64;
if rounded == 1 {
adjusted + Duration::seconds(60 - adjusted_seconds)
} else {
adjusted + Duration::seconds(adjusted_seconds * -1)
}
}
Rounding::Up => {
let adjusted_seconds = seconds as i64;
adjusted + Duration::seconds(60 - adjusted_seconds)
}
Rounding::None => adjusted,
}
}
fn adjust_time(&self, minutes: i64) -> Self {
let some_date = self.clone();
some_date
.checked_add_signed(Duration::seconds(minutes * 60))
.unwrap()
}
fn next_date(&self, fwd: bool) -> Self {
let ordinal = if fwd {
self.ordinal() + 1
} else {
self.ordinal() - 1
};
match self.with_ordinal(ordinal) {
Some(dt) => dt,
None => {
if fwd {
self.with_year(self.year() + 1)
.unwrap()
.with_ordinal(1)
.unwrap()
} else {
self.with_year(self.year() - 1)
.unwrap()
.with_month(12)
.unwrap()
.with_day(31)
.unwrap()
}
}
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Angle {
pub degrees: f64,
}
impl Angle {
pub fn new(value: f64) -> Self {
Angle { degrees: value }
}
pub fn from_radians(value: f64) -> Self {
Angle {
degrees: (value * 180.0) / PI,
}
}
pub fn radians(&self) -> f64 {
(self.degrees * PI) / 180.0
}
pub fn unwound(&self) -> Angle {
Angle {
degrees: self.degrees.normalized_to_scale(360.0),
}
}
pub fn quadrant_shifted(&self) -> Angle {
let angle: Angle;
if self.degrees >= -180.0 && self.degrees <= 180.0 {
angle = self.clone();
} else {
let value = self.degrees - (360.0 * (self.degrees / 360.0).round());
angle = Angle { degrees: value };
}
angle
}
}
impl Add for Angle {
type Output = Angle;
fn add(self, rhs: Angle) -> Angle {
Angle {
degrees: self.degrees + rhs.degrees,
}
}
}
impl Sub for Angle {
type Output = Angle;
fn sub(self, rhs: Angle) -> Angle {
Angle {
degrees: self.degrees - rhs.degrees,
}
}
}
impl Mul for Angle {
type Output = Angle;
fn mul(self, rhs: Angle) -> Angle {
Angle {
degrees: self.degrees * rhs.degrees,
}
}
}
impl Div for Angle {
type Output = Angle;
fn div(self, rhs: Angle) -> Angle {
if rhs.degrees == 0.0 {
panic!("Cannot divide by zero.");
}
Angle {
degrees: self.degrees / rhs.degrees,
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Coordinates {
pub latitude: f64,
pub longitude: f64,
}
impl Coordinates {
pub fn new(latitude: f64, longitude: f64) -> Self {
Coordinates {
latitude: latitude,
longitude: longitude,
}
}
}
impl Coordinates {
pub fn latitude_angle(&self) -> Angle {
Angle::new(self.latitude)
}
pub fn longitude_angle(&self) -> Angle {
Angle::new(self.longitude)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::f64::consts::PI;
#[test]
fn angle_conversion_from_radians() {
assert_eq!(Angle::from_radians(PI).degrees, 180.0);
assert_eq!(Angle::from_radians(PI / 2.0).degrees, 90.0);
}
#[test]
fn angle_conversion_degrees_to_radians() {
assert_eq!(Angle::new(180.0).radians(), PI);
assert_eq!(Angle::new(90.0).radians(), PI / 2.0);
}
#[test]
fn normalize_value() {
assert_eq!(2.0_f64.normalized_to_scale(-5.0), -3.0);
assert_eq!((-4.0_f64).normalized_to_scale(-5.0), -4.0);
assert_eq!((-6.0_f64).normalized_to_scale(-5.0), -1.0);
assert_eq!((-1.0_f64).normalized_to_scale(24.0), 23.0);
assert_eq!(1.0_f64.normalized_to_scale(24.0), 1.0);
assert_eq!(49.0_f64.normalized_to_scale(24.0), 1.0);
assert_eq!(361.0_f64.normalized_to_scale(360.0), 1.0);
assert_eq!(360.0_f64.normalized_to_scale(360.0), 0.0);
assert_eq!(259.0_f64.normalized_to_scale(360.0), 259.0);
assert_eq!(2592.0_f64.normalized_to_scale(360.0), 72.0);
}
#[test]
fn angle_unwound() {
assert_eq!(Angle::new(-45.0).unwound().degrees, 315.0);
assert_eq!(Angle::new(361.0).unwound().degrees, 1.0);
assert_eq!(Angle::new(360.0).unwound().degrees, 0.0);
assert_eq!(Angle::new(259.0).unwound().degrees, 259.0);
assert_eq!(Angle::new(2592.0).unwound().degrees, 72.0);
}
#[test]
fn closest_angle() {
assert_eq!(Angle::new(360.0).quadrant_shifted().degrees, 0.0);
assert_eq!(Angle::new(361.0).quadrant_shifted().degrees, 1.0);
assert_eq!(Angle::new(1.0).quadrant_shifted().degrees, 1.0);
assert_eq!(Angle::new(-1.0).quadrant_shifted().degrees, -1.0);
assert_eq!(Angle::new(-181.0).quadrant_shifted().degrees, 179.0);
assert_eq!(Angle::new(180.0).quadrant_shifted().degrees, 180.0);
assert_eq!(Angle::new(359.0).quadrant_shifted().degrees, -1.0);
assert_eq!(Angle::new(-359.0).quadrant_shifted().degrees, 1.0);
assert_eq!(Angle::new(1261.0).quadrant_shifted().degrees, -179.0);
}
#[test]
fn adding_angles() {
let angle_a = Angle::new(45.0);
let angle_b = Angle::new(45.0);
assert_eq!((angle_a + angle_b).degrees, 90.0);
}
#[test]
fn calculate_rounding_nearest() {
let time_1 = Utc
.with_ymd_and_hms(2015, 7, 13, 4, 37, 30)
.single()
.expect("Invalid date and time.");
assert_eq!(
time_1.rounded_minute(Rounding::Nearest),
Utc.with_ymd_and_hms(2015, 7, 13, 4, 38, 00)
.single()
.unwrap()
);
}
#[test]
fn calculate_rounding_up() {
let time_1 = Utc
.with_ymd_and_hms(2015, 07, 13, 05, 59, 20)
.single()
.expect("Invalid date and time.");
assert_eq!(
time_1.rounded_minute(Rounding::Up),
Utc.with_ymd_and_hms(2015, 7, 13, 6, 00, 00)
.single()
.unwrap()
);
}
#[test]
fn calculate_rounding_none() {
let time_1 = Utc
.with_ymd_and_hms(2015, 07, 13, 05, 59, 20)
.single()
.expect("Invalid date and time.");
assert_eq!(
time_1.rounded_minute(Rounding::None),
Utc.with_ymd_and_hms(2015, 7, 13, 5, 59, 20)
.single()
.unwrap()
);
}
}