diff --git a/src/builtin/modules/math.rs b/src/builtin/modules/math.rs index 07d64af..d9f9b11 100644 --- a/src/builtin/modules/math.rs +++ b/src/builtin/modules/math.rs @@ -102,7 +102,74 @@ fn clamp(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } fn hypot(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { - todo!() + args.min_args(1)?; + + let span = args.span(); + + let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> { + match v.node { + Value::Dimension(n, u, ..) => Ok((n, u)), + v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), + } + }); + + let first: (Number, Unit) = match numbers.next().unwrap()? { + (Some(n), u) => (n.clone() * n, u), + (None, u) => return Ok(Value::Dimension(None, u, true)), + }; + + let rest = numbers + .enumerate() + .map(|(idx, val)| -> SassResult> { + let (number, unit) = val?; + if first.1 == Unit::None { + if unit == Unit::None { + Ok(number.map(|n| n.clone() * n)) + } else { + Err(( + format!( + "Argument 1 is unitless but argument {} has unit {}. \ + Arguments must all have units or all be unitless.", + idx + 2, + unit + ), + span, + ) + .into()) + } + } else if unit == Unit::None { + Err(( + format!( + "Argument 1 has unit {} but argument {} is unitless. \ + Arguments must all have units or all be unitless.", + first.1, + idx + 2, + ), + span, + ) + .into()) + } else if first.1.comparable(&unit) { + Ok(number + .map(|n| n.convert(&unit, &first.1)) + .map(|n| n.clone() * n)) + } else { + Err(( + format!("Incompatible units {} and {}.", first.1, unit), + span, + ) + .into()) + } + }) + .collect::>>>()?; + + let rest = match rest { + Some(v) => v, + None => return Ok(Value::Dimension(None, first.1, true)), + }; + + let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b); + + Ok(Value::Dimension(sum.sqrt(), first.1, true)) } fn log(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { @@ -421,6 +488,7 @@ pub(crate) fn declare(f: &mut Module) { f.insert_builtin("atan", atan); f.insert_builtin("log", log); f.insert_builtin("pow", pow); + f.insert_builtin("hypot", hypot); #[cfg(feature = "random")] f.insert_builtin("random", random); diff --git a/src/value/number/mod.rs b/src/value/number/mod.rs index a95a81a..cff80a5 100644 --- a/src/value/number/mod.rs +++ b/src/value/number/mod.rs @@ -12,6 +12,8 @@ use num_traits::{ CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Num, One, Signed, ToPrimitive, Zero, }; +use crate::unit::{Unit, UNIT_CONVERSION_TABLE}; + use integer::Integer; mod integer; @@ -134,6 +136,11 @@ impl Number { self.as_float()?.powf(exponent.as_float()?), )?))) } + + /// Invariants: `from.comparable(&to)` must be true + pub fn convert(self, from: &Unit, to: &Unit) -> Self { + self * UNIT_CONVERSION_TABLE[to.to_string().as_str()][from.to_string().as_str()].clone() + } } macro_rules! trig_fn( diff --git a/tests/math-module.rs b/tests/math-module.rs index 2915c28..f1addb4 100644 --- a/tests/math-module.rs +++ b/tests/math-module.rs @@ -446,3 +446,52 @@ test!( "@use 'sass:math';\na {\n color: math.pow(2, 0);\n}\n", "a {\n color: 1;\n}\n" ); +test!( + hypot_all_same_unit, + "@use 'sass:math';\na {\n color: math.hypot(1px, 2px, 3px, 4px, 5px);\n}\n", + "a {\n color: 7.4161984871px;\n}\n" +); +test!( + hypot_negative, + "@use 'sass:math';\na {\n color: math.hypot(1px, 2px, 3px, 4px, 5px, -20px);\n}\n", + "a {\n color: 21.3307290077px;\n}\n" +); +test!( + hypot_all_different_but_comparable_unit, + "@use 'sass:math';\na {\n color: math.hypot(1in, 2cm, 3mm, 4pt, 5pc);\n}\n", + "a {\n color: 1.5269191636in;\n}\n" +); +test!( + hypot_all_no_unit, + "@use 'sass:math';\na {\n color: math.hypot(1, 2, 3);\n}\n", + "a {\n color: 3.7416573868;\n}\n" +); +test!( + hypot_nan_has_comparable_unit, + "@use 'sass:math';\na {\n color: math.hypot(1deg, 2deg, math.acos(2));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +error!( + hypot_no_args, + "@use 'sass:math';\na {\n color: math.hypot();\n}\n", + "Error: At least one argument must be passed." +); +error!( + hypot_first_has_no_unit_third_has_unit, + "@use 'sass:math';\na {\n color: math.hypot(1, 2, 3px);\n}\n", + "Error: Argument 1 is unitless but argument 3 has unit px. Arguments must all have units or all be unitless." +); +error!( + hypot_non_numeric_argument, + "@use 'sass:math';\na {\n color: math.hypot(1, red, 3);\n}\n", "Error: red is not a number." +); +error!( + hypot_units_not_comparable, + "@use 'sass:math';\na {\n color: math.hypot(1px, 2in, 3rem);\n}\n", + "Error: Incompatible units px and rem." +); +error!( + hypot_nan_has_no_unit_but_first_has_unit, + "@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." +);