From a9e4d5cba5aeb2fad92feffde18ca64dfeb06749 Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Sun, 2 Aug 2020 00:42:24 -0400 Subject: [PATCH] implement builtin function `math.atan2` --- src/builtin/modules/math.rs | 121 +++++++++++++++++++++++++++++++++++- src/value/number/mod.rs | 10 +++ tests/math-module.rs | 95 ++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) diff --git a/src/builtin/modules/math.rs b/src/builtin/modules/math.rs index d9f9b11..c49a52b 100644 --- a/src/builtin/modules/math.rs +++ b/src/builtin/modules/math.rs @@ -464,7 +464,125 @@ fn atan(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { fn atan2(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; - todo!() + let (y_num, y_unit) = match args.get_err(0, "y")? { + Value::Dimension(n, u, ..) => (n, u), + v => { + return Err(( + format!("$y: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let (x_num, x_unit) = match args.get_err(1, "x")? { + Value::Dimension(n, u, ..) => (n, u), + v => { + return Err(( + format!("$x: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None { + let x = match x_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + let y = match y_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + (x, y) + } else if y_unit == Unit::None { + return Err(( + format!( + "$y is unitless but $x has unit {}. \ + Arguments must all have units or all be unitless.", + x_unit + ), + args.span(), + ) + .into()); + } else if x_unit == Unit::None { + return Err(( + format!( + "$y has unit {} but $x is unitless. \ + Arguments must all have units or all be unitless.", + y_unit + ), + args.span(), + ) + .into()); + } else if x_unit.comparable(&y_unit) { + let x = match x_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + let y = match y_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + (x, y.convert(&y_unit, &x_unit)) + } else { + return Err(( + format!("Incompatible units {} and {}.", y_unit, x_unit), + args.span(), + ) + .into()); + }; + + Ok( + match ( + NumberState::from_number(&x_num), + NumberState::from_number(&y_num), + ) { + (NumberState::Zero, NumberState::FiniteNegative) => { + Value::Dimension(Some(Number::from(-90)), Unit::Deg, true) + } + (NumberState::Zero, NumberState::Zero) | (NumberState::Finite, NumberState::Zero) => { + Value::Dimension(Some(Number::zero()), Unit::Deg, true) + } + (NumberState::Zero, NumberState::Finite) => { + Value::Dimension(Some(Number::from(90)), Unit::Deg, true) + } + (NumberState::Finite, NumberState::Finite) + | (NumberState::FiniteNegative, NumberState::Finite) + | (NumberState::Finite, NumberState::FiniteNegative) + | (NumberState::FiniteNegative, NumberState::FiniteNegative) => Value::Dimension( + y_num + .atan2(x_num) + .map(|n| (n * Number::from(180)) / Number::pi()), + Unit::Deg, + true, + ), + (NumberState::FiniteNegative, NumberState::Zero) => { + Value::Dimension(Some(Number::from(180)), Unit::Deg, true) + } + }, + ) +} + +enum NumberState { + Zero, + Finite, + FiniteNegative, +} + +impl NumberState { + fn from_number(num: &Number) -> Self { + match (num.is_zero(), num.is_positive()) { + (true, _) => NumberState::Zero, + (false, true) => NumberState::Finite, + (false, false) => NumberState::FiniteNegative, + } + } } pub(crate) fn declare(f: &mut Module) { @@ -489,6 +607,7 @@ pub(crate) fn declare(f: &mut Module) { f.insert_builtin("log", log); f.insert_builtin("pow", pow); f.insert_builtin("hypot", hypot); + f.insert_builtin("atan2", atan2); #[cfg(feature = "random")] f.insert_builtin("random", random); diff --git a/src/value/number/mod.rs b/src/value/number/mod.rs index c8d552d..522f915 100644 --- a/src/value/number/mod.rs +++ b/src/value/number/mod.rs @@ -137,6 +137,16 @@ impl Number { )?))) } + pub fn pi() -> Self { + Number::from(std::f64::consts::PI) + } + + pub fn atan2(self, other: Self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.atan2(other.as_float()?), + )?))) + } + /// Invariants: `from.comparable(&to)` must be true pub fn convert(self, from: &Unit, to: &Unit) -> Self { self * UNIT_CONVERSION_TABLE[to][from].clone() diff --git a/tests/math-module.rs b/tests/math-module.rs index f1addb4..4f83a4d 100644 --- a/tests/math-module.rs +++ b/tests/math-module.rs @@ -495,3 +495,98 @@ error!( "@use 'sass:math';\na {\n color: math.hypot(1deg, 2deg, (0 / 0));\n}\n", "Error: Argument 1 has unit deg but argument 3 is unitless. Arguments must all have units or all be unitless." ); +test!( + atan2_both_positive, + "@use 'sass:math';\na {\n color: math.atan2(3, 4);\n}\n", + "a {\n color: 36.8698976458deg;\n}\n" +); +test!( + atan2_first_negative, + "@use 'sass:math';\na {\n color: math.atan2(-3, 4);\n}\n", + "a {\n color: -36.8698976458deg;\n}\n" +); +test!( + atan2_second_negative, + "@use 'sass:math';\na {\n color: math.atan2(3, -4);\n}\n", + "a {\n color: 143.1301023542deg;\n}\n" +); +test!( + atan2_both_negative, + "@use 'sass:math';\na {\n color: math.atan2(-3, -4);\n}\n", + "a {\n color: -143.1301023542deg;\n}\n" +); +test!( + atan2_first_positive_second_zero, + "@use 'sass:math';\na {\n color: math.atan2(3, 0);\n}\n", + "a {\n color: 90deg;\n}\n" +); +test!( + atan2_first_negative_second_zero, + "@use 'sass:math';\na {\n color: math.atan2(-3, 0);\n}\n", + "a {\n color: -90deg;\n}\n" +); +test!( + atan2_first_zero_second_positive, + "@use 'sass:math';\na {\n color: math.atan2(0, 4);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + atan2_first_zero_second_negative, + "@use 'sass:math';\na {\n color: math.atan2(0, -4);\n}\n", + "a {\n color: 180deg;\n}\n" +); +test!( + atan2_both_zero, + "@use 'sass:math';\na {\n color: math.atan2(0, 0);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + atan2_both_same_unit, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4px);\n}\n", + "a {\n color: 36.8698976458deg;\n}\n" +); +test!( + atan2_both_different_but_comparable_unit, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4in);\n}\n", + "a {\n color: 0.4476141709deg;\n}\n" +); +error!( + atan2_first_unitless_second_unit, + "@use 'sass:math';\na {\n color: math.atan2(3, 4rem);\n}\n", + "Error: $y is unitless but $x has unit rem. Arguments must all have units or all be unitless." +); +error!( + atan2_first_unit_second_unitless, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4);\n}\n", + "Error: $y has unit px but $x is unitless. Arguments must all have units or all be unitless." +); +error!( + atan2_incompatible_units, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4rem);\n}\n", + "Error: Incompatible units px and rem." +); +error!( + atan2_nan_incompatible_units, + "@use 'sass:math';\na {\n color: math.atan2(math.acos(2), 3);\n}\n", + "Error: $y has unit deg but $x is unitless. Arguments must all have units or all be unitless." +); +test!( + atan2_first_nan, + "@use 'sass:math';\na {\n color: math.atan2((0/0), 0);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan2_second_nan, + "@use 'sass:math';\na {\n color: math.atan2(0, (0/0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan2_both_nan, + "@use 'sass:math';\na {\n color: math.atan2((0/0), (0/0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan2_nan_with_same_units, + "@use 'sass:math';\na {\n color: math.atan2(math.acos(2), 3deg);\n}\n", + "a {\n color: NaNdeg;\n}\n" +);