use std::fmt::Display;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::math::FloatNumber;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hue<T = f64>(T)
where
T: FloatNumber;
impl<T> Hue<T>
where
T: FloatNumber,
{
#[inline]
pub fn to_degrees(&self) -> T {
self.0
}
#[inline]
pub fn to_radians(&self) -> T {
self.0.to_radians()
}
#[must_use]
pub fn from_degrees(degrees: T) -> Self {
Self(normalize(degrees))
}
#[must_use]
pub fn from_radians(radians: T) -> Self {
Self(normalize(radians.to_degrees()))
}
}
#[cfg(feature = "wasm")]
impl<T> Serialize for Hue<T>
where
T: FloatNumber + Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "wasm")]
impl<'de, T> Deserialize<'de> for Hue<T>
where
T: FloatNumber + Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = T::deserialize(deserializer)?;
Ok(Hue::from_degrees(value))
}
}
impl<T> Display for Hue<T>
where
T: FloatNumber,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.2}", self.0)
}
}
#[inline]
#[must_use]
fn normalize<T>(value: T) -> T
where
T: FloatNumber,
{
let max = T::from_f32(360.0);
let value = value % max;
if value.is_sign_negative() {
value + max
} else {
value
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use rstest::rstest;
#[cfg(feature = "wasm")]
use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
use super::*;
#[test]
fn test_to_degrees() {
let hue = Hue::from_degrees(45.0);
let actual = hue.to_degrees();
assert_eq!(actual, 45.0);
}
#[test]
fn test_to_radians() {
let hue = Hue::from_degrees(45.0);
let actual = hue.to_radians();
assert_eq!(actual, 45.0 / 180.0 * PI);
}
#[rstest]
#[case(45.0, 45.0)]
#[case(360.0, 0.0)]
#[case(720.0, 0.0)]
#[case(-90.0, 270.0)]
fn test_from_degrees(#[case] degrees: f64, #[case] expected: f64) {
let actual = Hue::from_degrees(degrees);
assert_eq!(actual.to_degrees(), expected);
}
#[rstest]
#[case(0.0 * PI, 0.0)]
#[case(PI, 180.0)]
#[case(-PI, 180.0)]
#[case(2.0 * PI, 0.0)]
fn test_from_radians(#[case] radians: f64, #[case] expected: f64) {
let actual = Hue::from_radians(radians);
assert_eq!(actual.to_degrees(), expected);
}
#[test]
#[cfg(feature = "wasm")]
fn test_serialize() {
let hue = Hue::from_degrees(45.0);
assert_ser_tokens(&hue, &[Token::F64(45.0)]);
}
#[test]
#[cfg(feature = "wasm")]
fn test_deserialize() {
let hue = Hue::from_degrees(60.0);
assert_de_tokens(&hue, &[Token::F64(60.0)]);
}
#[test]
fn test_fmt() {
let degree = Hue::from_degrees(45.0);
let actual = format!("{}", degree);
assert_eq!(actual, "45.00");
}
}