diff --git a/CHANGELOG.md b/CHANGELOG.md index 8205048b..51c5aeca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,28 @@ ## 1.32.0 +* Improve error messages about incompatible units. + * Properly mark some warnings emitted by `sass:color` functions as deprecation warnings. + +### Dart API + +* Rename `SassNumber.valueInUnits()` to `SassNumber.coerceValue()`. The old name + remains, but is now deprecated. + +* Rename `SassNumber.coerceValueToUnit()`, a shorthand for + `SassNumber.coerceValue()` that takes a single numerator unit. + +* Add `SassNumber.coerceToMatch()` and `SassNumber.coerceValueToMatch()`, which + work like `SassNumber.coerce()` and `SassNumber.coerceValue()` but take a + `SassNumber` whose units should be matched rather than taking the units + explicitly. These generate better error messages than `SassNumber.coerce()` + and `SassNumber.coerceValue()`. + +* Add `SassNumber.convertToMatch()` and `SassNumber.convertValueToMatch()`, + which work like `SassNumber.coerceToMatch()` and + `SassNumber.coerceValueToMatch()` except they throw exceptions when converting + unitless values to or from units. ## 1.31.0 diff --git a/lib/src/functions/math.dart b/lib/src/functions/math.dart index ffe8ea10..c87083d3 100644 --- a/lib/src/functions/math.dart +++ b/lib/src/functions/math.dart @@ -41,20 +41,17 @@ final _clamp = _function("clamp", r"$min, $number, $max", (arguments) { var min = arguments[0].assertNumber("min"); var number = arguments[1].assertNumber("number"); var max = arguments[2].assertNumber("max"); - if (min.hasUnits == number.hasUnits && number.hasUnits == max.hasUnits) { - if (min.greaterThanOrEquals(max).isTruthy) return min; - if (min.greaterThanOrEquals(number).isTruthy) return min; - if (number.greaterThanOrEquals(max).isTruthy) return max; - return number; - } - var arg2 = min.hasUnits != number.hasUnits ? number : max; - var arg2Name = min.hasUnits != number.hasUnits ? "\$number" : "\$max"; - var unit1 = min.hasUnits ? "has unit ${min.unitString}" : "is unitless"; - var unit2 = arg2.hasUnits ? "has unit ${arg2.unitString}" : "is unitless"; - throw SassScriptException( - "\$min $unit1 but $arg2Name $unit2. Arguments must all have units or all " - "be unitless."); + // Even though we don't use the resulting values, `convertValueToMatch` + // generates more user-friendly exceptions than [greaterThanOrEquals] since it + // has more context about parameter names. + number.convertValueToMatch(min, "number", "min"); + max.convertValueToMatch(min, "max", "min"); + + if (min.greaterThanOrEquals(max).isTruthy) return min; + if (min.greaterThanOrEquals(number).isTruthy) return min; + if (number.greaterThanOrEquals(max).isTruthy) return max; + return number; }); final _floor = _numberFunction("floor", (value) => value.floor()); @@ -94,27 +91,16 @@ final _hypot = _function("hypot", r"$numbers...", (arguments) { throw SassScriptException("At least one argument must be passed."); } - var numeratorUnits = numbers[0].numeratorUnits; - var denominatorUnits = numbers[0].denominatorUnits; var subtotal = 0.0; for (var i = 0; i < numbers.length; i++) { var number = numbers[i]; - if (number.hasUnits == numbers[0].hasUnits) { - number = number.coerce(numeratorUnits, denominatorUnits); - subtotal += math.pow(number.value, 2); - } else { - var unit1 = numbers[0].hasUnits - ? "has unit ${numbers[0].unitString}" - : "is unitless"; - var unit2 = - number.hasUnits ? "has unit ${number.unitString}" : "is unitless"; - throw SassScriptException( - "Argument 1 $unit1 but argument ${i + 1} $unit2. Arguments must all " - "have units or all be unitless."); - } + var value = number.convertValueToMatch( + numbers[0], "numbers[${i + 1}]", "numbers[1]"); + subtotal += math.pow(value, 2); } return SassNumber.withUnits(math.sqrt(subtotal), - numeratorUnits: numeratorUnits, denominatorUnits: denominatorUnits); + numeratorUnits: numbers[0].numeratorUnits, + denominatorUnits: numbers[0].denominatorUnits); }); /// @@ -232,42 +218,36 @@ final _atan = _function("atan", r"$number", (arguments) { final _atan2 = _function("atan2", r"$y, $x", (arguments) { var y = arguments[0].assertNumber("y"); var x = arguments[1].assertNumber("x"); - if (y.hasUnits != x.hasUnits) { - var unit1 = y.hasUnits ? "has unit ${y.unitString}" : "is unitless"; - var unit2 = x.hasUnits ? "has unit ${x.unitString}" : "is unitless"; - throw SassScriptException( - "\$y $unit1 but \$x $unit2. Arguments must all have units or all be " - "unitless."); - } - x = x.coerce(y.numeratorUnits, y.denominatorUnits); - var xValue = _fuzzyRoundIfZero(x.value); + var xValue = _fuzzyRoundIfZero(x.convertValueToMatch(y, 'x', 'y')); var yValue = _fuzzyRoundIfZero(y.value); var atan2 = math.atan2(yValue, xValue) * 180 / math.pi; return SassNumber.withUnits(atan2, numeratorUnits: ['deg']); }); final _cos = _function("cos", r"$number", (arguments) { - var number = _coerceToRad(arguments[0].assertNumber("number")); - return SassNumber(math.cos(number.value)); + var value = + arguments[0].assertNumber("number").coerceValueToUnit("rad", "number"); + return SassNumber(math.cos(value)); }); final _sin = _function("sin", r"$number", (arguments) { - var number = _coerceToRad(arguments[0].assertNumber("number")); - var numberValue = _fuzzyRoundIfZero(number.value); - return SassNumber(math.sin(numberValue)); + var value = _fuzzyRoundIfZero( + arguments[0].assertNumber("number").coerceValueToUnit("rad", "number")); + return SassNumber(math.sin(value)); }); final _tan = _function("tan", r"$number", (arguments) { - var number = _coerceToRad(arguments[0].assertNumber("number")); + var value = + arguments[0].assertNumber("number").coerceValueToUnit("rad", "number"); var asymptoteInterval = 0.5 * math.pi; var tanPeriod = 2 * math.pi; - if (fuzzyEquals((number.value - asymptoteInterval) % tanPeriod, 0)) { + if (fuzzyEquals((value - asymptoteInterval) % tanPeriod, 0)) { return SassNumber(double.infinity); - } else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) { + } else if (fuzzyEquals((value + asymptoteInterval) % tanPeriod, 0)) { return SassNumber(double.negativeInfinity); } else { - var numberValue = _fuzzyRoundIfZero(number.value); + var numberValue = _fuzzyRoundIfZero(value); return SassNumber(math.tan(numberValue)); } }); @@ -322,15 +302,6 @@ num _fuzzyRoundIfZero(num number) { return number.isNegative ? -0.0 : 0; } -SassNumber _coerceToRad(SassNumber number) { - try { - return number.coerce(['rad'], []); - } on SassScriptException catch (error) { - if (!error.message.startsWith('Incompatible units')) rethrow; - throw SassScriptException('\$number: Expected ${number} to be an angle.'); - } -} - /// Returns a [Callable] named [name] that transforms a number's value /// using [transform] and preserves its units. BuiltInCallable _numberFunction(String name, num transform(num value)) { diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 69b96a50..cb3a3447 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -37,6 +37,11 @@ String pluralize(String name, int number, {String plural}) { return '${name}s'; } +/// Returns `a $word` or `an $word` depending on whether [word] starts with a +/// vowel. +String a(String word) => + [$a, $e, $i, $o, $u].contains(word.codeUnitAt(0)) ? "an $word" : "a $word"; + /// Returns a bulleted list of items in [bullets]. String bulletedList(Iterable bullets) { return bullets.map((element) { diff --git a/lib/src/value/external/number.dart b/lib/src/value/external/number.dart index 16accf24..ec7f29a7 100644 --- a/lib/src/value/external/number.dart +++ b/lib/src/value/external/number.dart @@ -96,20 +96,105 @@ abstract class SassNumber extends Value { /// (without the `$`). It's used for error reporting. void assertNoUnits([String name]); + /// Returns a copy of this number, converted to the same units as [other]. + /// + /// Unlike [convertToMatch], this does *not* throw an error if this number is + /// unitless and [other] is not, or vice versa. Instead, it treats all + /// unitless numbers as convertable to and from all units without changing the + /// value. + /// + /// Note that [coerceValueToMatch] is generally more efficient if the value is + /// going to be accessed directly. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + SassNumber coerceToMatch(SassNumber other, [String name, String otherName]); + + /// Returns [value], converted to the same units as [other]. + /// + /// Unlike [convertValueToMatch], this does *not* throw an error if this + /// number is unitless and [other] is not, or vice versa. Instead, it treats + /// all unitless numbers as convertable to and from all units without changing + /// the value. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + num coerceValueToMatch(SassNumber other, [String name, String otherName]); + + /// Returns a copy of this number, converted to the same units as [other]. + /// + /// Note that [convertValueToMatch] is generally more efficient if the value + /// is going to be accessed directly. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units, or if either number is unitless but the other is + /// not. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + SassNumber convertToMatch(SassNumber other, [String name, String otherName]); + + /// Returns [value], converted to the same units as [other]. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units, or if either number is unitless but the other is + /// not. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + num convertValueToMatch(SassNumber other, [String name, String otherName]); + /// Returns a copy of this number, converted to the units represented by /// [newNumerators] and [newDenominators]. /// - /// Note that [valueInUnits] is generally more efficient if the value is going + /// This does *not* throw an error if this number is unitless and + /// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead, + /// it treats all unitless numbers as convertable to and from all units + /// without changing the value. + /// + /// Note that [coerceValue] is generally more efficient if the value is going /// to be accessed directly. /// /// Throws a [SassScriptException] if this number's units aren't compatible /// with [newNumerators] and [newDenominators]. - SassNumber coerce(List newNumerators, List newDenominators); + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + SassNumber coerce(List newNumerators, List newDenominators, + [String name]); /// Returns [value], converted to the units represented by [newNumerators] and /// [newDenominators]. /// + /// This does *not* throw an error if this number is unitless and + /// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead, + /// it treats all unitless numbers as convertable to and from all units + /// without changing the value. + /// /// Throws a [SassScriptException] if this number's units aren't compatible /// with [newNumerators] and [newDenominators]. - num valueInUnits(List newNumerators, List newDenominators); + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + num coerceValue(List newNumerators, List newDenominators, + [String name]); + + /// This has been renamed [coerceValue] for consistency with [coerceToMatch], + /// [coerceValueToMatch], [convertToMatch], and [convertValueToMatch]. + @deprecated + num valueInUnits(List newNumerators, List newDenominators, + [String name]); + + /// A shorthand for [coerceValue] with only one numerator unit. + num coerceValueToUnit(String unit, [String name]); } diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 818be238..10cbb06c 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -4,6 +4,7 @@ import 'dart:math'; +import 'package:meta/meta.dart'; import 'package:tuple/tuple.dart'; import '../exception.dart'; @@ -16,7 +17,7 @@ import 'external/value.dart' as ext; /// A nested map containing unit conversion rates. /// /// `1unit1 * _conversions[unit1][unit2] = 1unit2`. -final _conversions = { +const _conversions = { // Length "in": { "in": 1, @@ -140,6 +141,22 @@ final _conversions = { }, }; +/// A map from human-readable names of unit types to the units that fall into +/// those types. +const _unitsByType = { + "length": ["in", "cm", "pc", "mm", "q", "pt", "px"], + "angle": ["deg", "grad", "rad", "turn"], + "time": ["s", "ms"], + "frequency": ["Hz", "kHz"], + "pixel density": ["dpi", "dpcm", "dppx"] +}; + +/// A map from units to the human-readable names of those unit types. +final _typesByUnit = { + for (var entry in _unitsByType.entries) + for (var unit in entry.value) unit: entry.key +}; + // TODO(nweiz): If it's faster, add subclasses specifically for unitless numbers // and numbers with only a single numerator unit. These should be opaque to // users of SassNumber. @@ -228,18 +245,113 @@ class SassNumber extends Value implements ext.SassNumber { throw _exception('Expected $this to have no units.', name); } - SassNumber coerce(List newNumerators, List newDenominators) => - SassNumber.withUnits(valueInUnits(newNumerators, newDenominators), + SassNumber coerceToMatch(ext.SassNumber other, + [String name, String otherName]) => + SassNumber.withUnits(coerceValueToMatch(other, name, otherName), + numeratorUnits: other.numeratorUnits, + denominatorUnits: other.denominatorUnits); + + num coerceValueToMatch(ext.SassNumber other, + [String name, String otherName]) => + _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, + coerceUnitless: true, name: name, other: other, otherName: otherName); + + SassNumber convertToMatch(ext.SassNumber other, + [String name, String otherName]) => + SassNumber.withUnits(convertValueToMatch(other, name, otherName), + numeratorUnits: other.numeratorUnits, + denominatorUnits: other.denominatorUnits); + + num convertValueToMatch(ext.SassNumber other, + [String name, String otherName]) => + _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, + coerceUnitless: false, + name: name, + other: other, + otherName: otherName); + + SassNumber coerce(List newNumerators, List newDenominators, + [String name]) => + SassNumber.withUnits(coerceValue(newNumerators, newDenominators, name), numeratorUnits: newNumerators, denominatorUnits: newDenominators); - num valueInUnits(List newNumerators, List newDenominators) { - if ((newNumerators.isEmpty && newDenominators.isEmpty) || - (numeratorUnits.isEmpty && denominatorUnits.isEmpty) || - (listEquals(numeratorUnits, newNumerators) && - listEquals(denominatorUnits, newDenominators))) { + num coerceValue(List newNumerators, List newDenominators, + [String name]) => + _coerceOrConvertValue(newNumerators, newDenominators, + coerceUnitless: true, name: name); + + num coerceValueToUnit(String unit, [String name]) => + coerceValue([unit], [], name); + + @deprecated + num valueInUnits(List newNumerators, List newDenominators, + [String name]) => + coerceValue(newNumerators, newDenominators, name); + + /// Converts [value] to [newNumerators] and [newDenominators]. + /// + /// If [coerceUnitless] is `true`, this considers unitless numbers convertable + /// to and from any unit. Otherwise, it will throw a [SassScriptException] for + /// such a conversion. + /// + /// If [other] is passed, it should be the number from which [newNumerators] + /// and [newDenominators] are derived. The [name] and [otherName] are the Sass + /// function parameter names of [this] and [other], respectively, used for + /// error reporting. + num _coerceOrConvertValue( + List newNumerators, List newDenominators, + {@required bool coerceUnitless, + String name, + ext.SassNumber other, + String otherName}) { + assert( + other == null || + (listEquals(other.numeratorUnits, newNumerators) && + listEquals(other.denominatorUnits, newDenominators)), + "Expected $other to have units " + "${_unitString(newNumerators, newDenominators)}."); + + if (listEquals(numeratorUnits, newNumerators) && + listEquals(denominatorUnits, newDenominators)) { return this.value; } + var otherHasUnits = newNumerators.isNotEmpty || newDenominators.isNotEmpty; + if (coerceUnitless && (!hasUnits || !otherHasUnits)) return this.value; + + SassScriptException _compatibilityException() { + if (other != null) { + var message = StringBuffer("$this and"); + if (otherName != null) message.write(" \$$otherName:"); + message.write(" $other have incompatible units"); + if (!hasUnits || !otherHasUnits) { + message.write(" (one has units and the other doesn't)"); + } + return _exception("$message.", name); + } else if (!otherHasUnits) { + return _exception("Expected $this to have no units.", name); + } else { + if (newNumerators.length == 1 && newDenominators.isEmpty) { + var type = _typesByUnit[newNumerators.first]; + if (type != null) { + // If we're converting to a unit of a named type, use that type name + // and make it clear exactly which units are convertible. + return _exception( + "Expected $this to have ${a(type)} unit " + "(${_unitsByType[type].join(', ')}).", + name); + } + } + + var unit = + pluralize('unit', newNumerators.length + newDenominators.length); + return _exception( + "Expected $this to have $unit " + "${_unitString(newNumerators, newDenominators)}.", + name); + } + } + var value = this.value; var oldNumerators = numeratorUnits.toList(); for (var newNumerator in newNumerators) { @@ -248,11 +360,7 @@ class SassNumber extends Value implements ext.SassNumber { if (factor == null) return false; value *= factor; return true; - }, orElse: () { - throw SassScriptException("Incompatible units " - "${_unitString(numeratorUnits, denominatorUnits)} and " - "${_unitString(newNumerators, newDenominators)}."); - }); + }, orElse: () => throw _compatibilityException()); } var oldDenominators = denominatorUnits.toList(); @@ -262,17 +370,11 @@ class SassNumber extends Value implements ext.SassNumber { if (factor == null) return false; value /= factor; return true; - }, orElse: () { - throw SassScriptException("Incompatible units " - "${_unitString(numeratorUnits, denominatorUnits)} and " - "${_unitString(newNumerators, newDenominators)}."); - }); + }, orElse: () => throw _compatibilityException()); } if (oldNumerators.isNotEmpty || oldDenominators.isNotEmpty) { - throw SassScriptException("Incompatible units " - "${_unitString(numeratorUnits, denominatorUnits)} and " - "${_unitString(newNumerators, newDenominators)}."); + throw _compatibilityException(); } return value; @@ -394,9 +496,9 @@ class SassNumber extends Value implements ext.SassNumber { num num2; if (hasUnits) { num1 = value; - num2 = other.valueInUnits(numeratorUnits, denominatorUnits); + num2 = other.coerceValueToMatch(this); } else { - num1 = valueInUnits(other.numeratorUnits, other.denominatorUnits); + num1 = coerceValueToMatch(other); num2 = other.value; } diff --git a/test/dart_api/value/number_test.dart b/test/dart_api/value/number_test.dart index f0640245..9d7e347c 100644 --- a/test/dart_api/value/number_test.dart +++ b/test/dart_api/value/number_test.dart @@ -37,6 +37,10 @@ void main() { expect(value.assertInt(), equals(123)); }); + test("can be coerced to unitless", () { + expect(value.coerce([], []), equals(SassNumber.withUnits(123))); + }); + test("can be coerced to any units", () { expect( value.coerce(["abc"], ["def"]), @@ -44,8 +48,31 @@ void main() { numeratorUnits: ["abc"], denominatorUnits: ["def"]))); }); - test("can return its value in any units", () { - expect(value.valueInUnits(["abc"], ["def"]), equals(123)); + test("can be converted to unitless", () { + expect(value.convertToMatch(SassNumber(456)), + equals(SassNumber.withUnits(123))); + }); + + test("can't be converted to a unit", () { + expect(() => value.convertToMatch(SassNumber(456, "px")), + throwsSassScriptException); + }); + + test("can coerce its value to unitless", () { + expect(value.coerceValue([], []), equals(123)); + }); + + test("can coerce its value to any units", () { + expect(value.coerceValue(["abc"], ["def"]), equals(123)); + }); + + test("can convert its value to unitless", () { + expect(value.convertValueToMatch(SassNumber(456)), equals(123)); + }); + + test("can't convert its value to any units", () { + expect(() => value.convertValueToMatch(SassNumber(456, "px")), + throwsSassScriptException); }); group("valueInRange()", () { @@ -183,14 +210,61 @@ void main() { expect(() => value.assertUnit("in"), throwsSassScriptException); }); + test("can be coerced to unitless", () { + expect(value.coerce([], []), equals(SassNumber(123))); + }); + test("can be coerced to compatible units", () { expect(value.coerce(["px"], []), equals(value)); expect(value.coerce(["in"], []), equals(SassNumber(1.28125, "in"))); }); - test("can return its value in compatible units", () { - expect(value.valueInUnits(["px"], []), equals(123)); - expect(value.valueInUnits(["in"], []), equals(1.28125)); + test("can't be coerced to incompatible units", () { + expect(() => value.coerce(["abc"], []), throwsSassScriptException); + }); + + test("can't be converted to unitless", () { + expect(() => value.convertToMatch(SassNumber(456)), + throwsSassScriptException); + }); + + test("can be converted to compatible units", () { + expect(value.convertToMatch(SassNumber(456, "px")), equals(value)); + expect(value.convertToMatch(SassNumber(456, "in")), + equals(SassNumber(1.28125, "in"))); + }); + + test("can't be converted to incompatible units", () { + expect(() => value.convertToMatch(SassNumber(456, "abc")), + throwsSassScriptException); + }); + + test("can coerce its value to unitless", () { + expect(value.coerceValue([], []), equals(123)); + }); + + test("can coerce its value to compatible units", () { + expect(value.coerceValue(["px"], []), equals(123)); + expect(value.coerceValue(["in"], []), equals(1.28125)); + }); + + test("can't coerce its value to incompatible units", () { + expect(() => value.coerceValue(["abc"], []), throwsSassScriptException); + }); + + test("can't convert its value to unitless", () { + expect(() => value.convertValueToMatch(SassNumber(456)), + throwsSassScriptException); + }); + + test("can convert its value to compatible units", () { + expect(value.convertValueToMatch(SassNumber(456, "px")), equals(123)); + expect(value.convertValueToMatch(SassNumber(456, "in")), equals(1.28125)); + }); + + test("can't convert its value to incompatible units", () { + expect(() => value.convertValueToMatch(SassNumber(456, "abc")), + throwsSassScriptException); }); test("equals the same number", () { @@ -238,6 +312,10 @@ void main() { expect(() => value.assertUnit("px"), throwsSassScriptException); }); + test("can be coerced to unitless", () { + expect(value.coerce([], []), equals(SassNumber(24.6))); + }); + test("can be coerced to compatible units", () { expect(value.coerce(["px"], ["ms"]), equals(value)); expect( @@ -246,9 +324,67 @@ void main() { numeratorUnits: ["in"], denominatorUnits: ["s"]))); }); - test("can return its value in compatible units", () { - expect(value.valueInUnits(["px"], ["ms"]), equals(24.6)); - expect(value.valueInUnits(["in"], ["s"]), equals(256.25)); + test("can coerce to match another number", () { + expect( + value.coerceToMatch(SassNumber.withUnits(456, + numeratorUnits: ["in"], denominatorUnits: ["s"])), + equals(SassNumber.withUnits(256.25, + numeratorUnits: ["in"], denominatorUnits: ["s"]))); + }); + + test("can't be coerced to incompatible units", () { + expect(() => value.coerce(["abc"], []), throwsSassScriptException); + }); + + test("can't be converted to unitless", () { + expect(() => value.convertToMatch(SassNumber(456)), + throwsSassScriptException); + }); + + test("can be converted to compatible units", () { + expect( + value.convertToMatch(SassNumber.withUnits(456, + numeratorUnits: ["px"], denominatorUnits: ["ms"])), + equals(value)); + expect( + value.convertToMatch(SassNumber.withUnits(456, + numeratorUnits: ["in"], denominatorUnits: ["s"])), + equals(SassNumber.withUnits(256.25, + numeratorUnits: ["in"], denominatorUnits: ["s"]))); + }); + + test("can coerce its value to unitless", () { + expect(value.coerceValue([], []), equals(24.6)); + }); + + test("can coerce its value to compatible units", () { + expect(value.coerceValue(["px"], ["ms"]), equals(24.6)); + expect(value.coerceValue(["in"], ["s"]), equals(256.25)); + }); + + test("can't coerce its value to incompatible units", () { + expect(() => value.coerceValue(["abc"], []), throwsSassScriptException); + }); + + test("can't convert its value to unitless", () { + expect(() => value.convertValueToMatch(SassNumber(456)), + throwsSassScriptException); + }); + + test("can convert its value to compatible units", () { + expect( + value.convertValueToMatch(SassNumber.withUnits(456, + numeratorUnits: ["px"], denominatorUnits: ["ms"])), + equals(24.6)); + expect( + value.convertValueToMatch(SassNumber.withUnits(456, + numeratorUnits: ["in"], denominatorUnits: ["s"])), + equals(256.25)); + }); + + test("can't convert its value to incompatible units", () { + expect(() => value.convertValueToMatch(SassNumber(456, "abc")), + throwsSassScriptException); }); test("equals the same number", () {