implement builtin function math.atan2

This commit is contained in:
Connor Skees 2020-08-02 00:42:24 -04:00
parent 28701b2707
commit a9e4d5cba5
3 changed files with 225 additions and 1 deletions

View File

@ -464,7 +464,125 @@ fn atan(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
fn atan2(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
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);

View File

@ -137,6 +137,16 @@ impl Number {
)?)))
}
pub fn pi() -> Self {
Number::from(std::f64::consts::PI)
}
pub fn atan2(self, other: Self) -> Option<Self> {
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()

View File

@ -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"
);