diff --git a/src/builtin/functions/meta.rs b/src/builtin/functions/meta.rs index d6e3fd9..7ad6e34 100644 --- a/src/builtin/functions/meta.rs +++ b/src/builtin/functions/meta.rs @@ -7,7 +7,6 @@ use crate::{ common::{Identifier, QuoteKind}, error::SassResult, parse::Parser, - unit::Unit, value::{SassFunction, Value}, }; @@ -73,12 +72,7 @@ pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; - #[allow(clippy::match_same_arms)] - Ok(match args.get_err(0, "number")? { - Value::Dimension(_, Unit::None, _) => Value::True, - Value::Dimension(..) => Value::False, - _ => Value::True, - }) + Ok(Value::bool(args.get_err(0, "number")?.unitless())) } pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { diff --git a/src/builtin/modules/math.rs b/src/builtin/modules/math.rs index 36f8019..533f6e9 100644 --- a/src/builtin/modules/math.rs +++ b/src/builtin/modules/math.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use crate::{ args::CallArgs, builtin::{ @@ -5,6 +7,7 @@ use crate::{ meta::{unit, unitless}, modules::Module, }, + common::Op, error::SassResult, parse::Parser, unit::Unit, @@ -16,7 +19,84 @@ use crate::builtin::math::random; fn clamp(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; - todo!() + let span = args.span(); + + let min = match args.get_err(0, "min")? { + v @ Value::Dimension(..) => v, + v => { + return Err(( + format!("$min: {} is not a number.", v.inspect(args.span())?), + span, + ) + .into()) + } + }; + + let number = match args.get_err(1, "number")? { + v @ Value::Dimension(..) => v, + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(span)?), + span, + ) + .into()) + } + }; + + let max = match args.get_err(2, "max")? { + v @ Value::Dimension(..) => v, + v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()), + }; + + // ensure that `min` and `max` are compatible + min.cmp(&max, span, Op::LessThan)?; + + let min_unit = match min { + Value::Dimension(_, ref u, _) => u, + _ => unreachable!(), + }; + let number_unit = match number { + Value::Dimension(_, ref u, _) => u, + _ => unreachable!(), + }; + let max_unit = match max { + Value::Dimension(_, ref u, _) => u, + _ => unreachable!(), + }; + + if min_unit == &Unit::None && number_unit != &Unit::None { + return Err(( + format!( + "$min is unitless but $number has unit {}. Arguments must all have units or all be unitless.", + number_unit + ), span).into()); + } else if min_unit != &Unit::None && number_unit == &Unit::None { + return Err(( + format!( + "$min has unit {} but $number is unitless. Arguments must all have units or all be unitless.", + min_unit + ), span).into()); + } else if min_unit != &Unit::None && max_unit == &Unit::None { + return Err(( + format!( + "$min has unit {} but $max is unitless. Arguments must all have units or all be unitless.", + min_unit + ), span).into()); + } + + match min.cmp(&number, span, Op::LessThan)? { + Ordering::Greater => return Ok(min), + Ordering::Equal => return Ok(number), + Ordering::Less => {} + } + + match max.cmp(&number, span, Op::GreaterThan)? { + Ordering::Less => return Ok(max), + Ordering::Equal => return Ok(number), + Ordering::Greater => {} + } + + Ok(number) } fn hypot(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { @@ -84,6 +164,7 @@ pub(crate) fn declare(f: &mut Module) { f.insert_builtin("is-unitless", unitless); f.insert_builtin("unit", unit); f.insert_builtin("percentage", percentage); + f.insert_builtin("clamp", clamp); #[cfg(feature = "random")] f.insert_builtin("random", random); diff --git a/src/parse/value/eval.rs b/src/parse/value/eval.rs index 94c5101..736eed9 100644 --- a/src/parse/value/eval.rs +++ b/src/parse/value/eval.rs @@ -735,53 +735,9 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { HigherIntermediateValue::Literal(v) => v, v => panic!("{:?}", v), }; - let ordering = match left { - Value::Dimension(num, unit, _) => match &right { - Value::Dimension(num2, unit2, _) => { - if !unit.comparable(unit2) { - return Err(( - format!("Incompatible units {} and {}.", unit2, unit), - self.span, - ) - .into()); - } - if &unit == unit2 || unit == Unit::None || unit2 == &Unit::None { - num.cmp(num2) - } else { - num.cmp( - &(num2.clone() - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone()), - ) - } - } - v => { - return Err(( - format!( - "Undefined operation \"{} {} {}\".", - v.inspect(self.span)?, - op, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }, - _ => { - return Err(( - format!( - "Undefined operation \"{} {} {}\".", - left.inspect(self.span)?, - op, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }; + + let ordering = left.cmp(&right, self.span, op)?; + Ok(match op { Op::GreaterThan => match ordering { Ordering::Greater => Value::True, diff --git a/src/value/mod.rs b/src/value/mod.rs index 5cf7cd4..7766b3f 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,10 +1,12 @@ +use std::cmp::Ordering; + use peekmore::PeekMore; use codemap::{Span, Spanned}; use crate::{ color::Color, - common::{Brackets, ListSeparator, QuoteKind}, + common::{Brackets, ListSeparator, Op, QuoteKind}, error::SassResult, parse::Parser, selector::Selector, @@ -322,6 +324,63 @@ impl Value { } } + pub fn cmp(&self, other: &Self, span: Span, op: Op) -> SassResult { + Ok(match self { + Value::Dimension(num, unit, _) => match &other { + Value::Dimension(num2, unit2, _) => { + if !unit.comparable(unit2) { + return Err( + (format!("Incompatible units {} and {}.", unit2, unit), span).into(), + ); + } + if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None { + num.cmp(num2) + } else { + num.cmp( + &(num2.clone() + * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] + [unit2.to_string().as_str()] + .clone()), + ) + } + } + v => { + return Err(( + format!( + "Undefined operation \"{} {} {}\".", + v.inspect(span)?, + op, + other.inspect(span)? + ), + span, + ) + .into()) + } + }, + _ => { + return Err(( + format!( + "Undefined operation \"{} {} {}\".", + self.inspect(span)?, + op, + other.inspect(span)? + ), + span, + ) + .into()) + } + }) + } + + pub fn unitless(&self) -> bool { + #[allow(clippy::match_same_arms)] + match self { + Value::Dimension(_, Unit::None, _) => true, + Value::Dimension(..) => false, + _ => true, + } + } + pub fn not_equals(&self, other: &Self) -> bool { match self { Value::String(s1, ..) => match other { diff --git a/src/value/number/mod.rs b/src/value/number/mod.rs index 00152b5..9cbb4ed 100644 --- a/src/value/number/mod.rs +++ b/src/value/number/mod.rs @@ -16,7 +16,7 @@ mod integer; const PRECISION: usize = 10; -#[derive(Clone, Eq, PartialEq, Ord)] +#[derive(Clone, Eq, PartialEq)] pub(crate) enum Number { Small(Rational64), Big(Box), @@ -321,6 +321,30 @@ impl PartialOrd for Number { } } +impl Ord for Number { + fn cmp(&self, other: &Self) -> Ordering { + match self { + Self::Small(val1) => match other { + Self::Small(val2) => val1.cmp(val2), + Self::Big(val2) => { + let tuple: (i64, i64) = (*val1).into(); + BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)).cmp(val2) + } + }, + Self::Big(val1) => match other { + Self::Small(val2) => { + let tuple: (i64, i64) = (*val2).into(); + (**val1).cmp(&BigRational::new_raw( + BigInt::from(tuple.0), + BigInt::from(tuple.1), + )) + } + Self::Big(val2) => val1.cmp(val2), + }, + } + } +} + impl Add for Number { type Output = Self; diff --git a/tests/math-module.rs b/tests/math-module.rs new file mode 100644 index 0000000..50030df --- /dev/null +++ b/tests/math-module.rs @@ -0,0 +1,40 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + clamp_in_the_middle, + "@use 'sass:math';\na {\n color: math.clamp(0, 1, 2);\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + clamp_first_is_bigger, + "@use 'sass:math';\na {\n color: math.clamp(2, 1, 0);\n}\n", + "a {\n color: 2;\n}\n" +); +test!( + clamp_all_same_unit, + "@use 'sass:math';\na {\n color: math.clamp(0px, 1px, 2px);\n}\n", + "a {\n color: 1px;\n}\n" +); +test!( + clamp_all_different_but_compatible_unit, + "@use 'sass:math';\na {\n color: math.clamp(0mm, 1cm, 2in);\n}\n", + "a {\n color: 1cm;\n}\n" +); +error!( + clamp_only_min_has_no_unit, + "@use 'sass:math';\na {\n color: math.clamp(0, 1cm, 2in);\n}\n", + "Error: $min is unitless but $number has unit cm. Arguments must all have units or all be unitless." +); +error!( + clamp_only_number_has_no_unit, + "@use 'sass:math';\na {\n color: math.clamp(0mm, 1, 2in);\n}\n", + "Error: $min has unit mm but $number is unitless. Arguments must all have units or all be unitless." +); +error!( + clamp_only_max_has_no_unit, + "@use 'sass:math';\na {\n color: math.clamp(0mm, 1cm, 2);\n}\n", + "Error: $min has unit mm but $max is unitless. Arguments must all have units or all be unitless." +); diff --git a/tests/ordering.rs b/tests/ordering.rs index ae00b88..5a2b644 100644 --- a/tests/ordering.rs +++ b/tests/ordering.rs @@ -63,3 +63,8 @@ test!( "a {\n color: 0 < 1;\n}\n", "a {\n color: true;\n}\n" ); +test!( + ord_the_same_as_partial_ord, + "a {\n color: 2in > 1cm;\n}\n", + "a {\n color: true;\n}\n" +);