Skip to content

Commit 244a1d9

Browse files
author
Awjin Ahn
authored
Merge pull request #930 from sass/math-functions
More Math functions
2 parents 5e29ea6 + 4c0c6b4 commit 244a1d9

File tree

4 files changed

+312
-33
lines changed

4 files changed

+312
-33
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
## 1.25.0
2+
3+
* Add functions to the built-in "sass:math" module.
4+
5+
* `clamp($min, $number, $max)`. Clamps `$number` in between `$min` and `$max`.
6+
7+
* `hypot($numbers...)`. Given *n* numbers, outputs the length of the
8+
*n*-dimensional vector that has components equal to each of the inputs.
9+
10+
* Exponential. All inputs must be unitless.
11+
* `log($number)` or `log($number, $base)`. If no base is provided, performs
12+
a natural log.
13+
* `pow($base, $exponent)`
14+
* `sqrt($number)`
15+
16+
* Trigonometric. The input must be an angle. If no unit is given, the input is
17+
assumed to be in `rad`.
18+
* `cos($number)`
19+
* `sin($number)`
20+
* `tan($number)`
21+
22+
* Inverse trigonometric. The output is in `deg`.
23+
* `acos($number)`. Input must be unitless.
24+
* `asin($number)`. Input must be unitless.
25+
* `atan($number)`. Input must be unitless.
26+
* `atan2($y, $x)`. `$y` and `$x` must have compatible units or be unitless.
27+
28+
* Add the variables `$pi` and `$e` to the built-in "sass:math" module.
29+
130
## 1.24.5
231

332
* Highlight contextually-relevant sections of the stylesheet in error messages,

lib/src/functions/math.dart

Lines changed: 272 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,51 @@ import '../module/built_in.dart';
1313
import '../util/number.dart';
1414
import '../value.dart';
1515

16-
/// A random number generator.
17-
final _random = math.Random();
18-
1916
/// The global definitions of Sass math functions.
2017
final global = UnmodifiableListView([
21-
_round, _ceil, _floor, _abs, _max, _min, _randomFunction, _unit, //
22-
_percentage,
18+
_abs, _ceil, _floor, _max, _min, _percentage, _randomFunction, _round,
19+
_unit, //
20+
_compatible.withName("comparable"),
2321
_isUnitless.withName("unitless"),
24-
_compatible.withName("comparable")
2522
]);
2623

2724
/// The Sass math module.
2825
final module = BuiltInModule("math", functions: [
29-
_round, _ceil, _floor, _abs, _max, _min, _randomFunction, _unit,
30-
_isUnitless, //
31-
_percentage, _compatible
32-
]);
33-
34-
final _percentage = _function("percentage", r"$number", (arguments) {
35-
var number = arguments[0].assertNumber("number");
36-
number.assertNoUnits("number");
37-
return SassNumber(number.value * 100, '%');
26+
_abs, _acos, _asin, _atan, _atan2, _ceil, _clamp, _cos, _compatible, //
27+
_floor, _hypot, _isUnitless, _log, _max, _min, _percentage, _pow, //
28+
_randomFunction, _round, _sin, _sqrt, _tan, _unit,
29+
], variables: {
30+
"e": SassNumber(math.e),
31+
"pi": SassNumber(math.pi),
3832
});
3933

40-
final _round = _numberFunction("round", fuzzyRound);
34+
///
35+
/// Bounding functions
36+
///
37+
4138
final _ceil = _numberFunction("ceil", (value) => value.ceil());
39+
40+
final _clamp = _function("clamp", r"$min, $number, $max", (arguments) {
41+
var min = arguments[0].assertNumber("min");
42+
var number = arguments[1].assertNumber("number");
43+
var max = arguments[2].assertNumber("max");
44+
if (min.hasUnits == number.hasUnits && number.hasUnits == max.hasUnits) {
45+
if (min.greaterThanOrEquals(max).isTruthy) return min;
46+
if (min.greaterThanOrEquals(number).isTruthy) return min;
47+
if (number.greaterThanOrEquals(max).isTruthy) return max;
48+
return number;
49+
}
50+
51+
var arg2 = min.hasUnits != number.hasUnits ? number : max;
52+
var arg2Name = min.hasUnits != number.hasUnits ? "\$number" : "\$max";
53+
var unit1 = min.hasUnits ? "has unit ${min.unitString}" : "is unitless";
54+
var unit2 = arg2.hasUnits ? "has unit ${arg2.unitString}" : "is unitless";
55+
throw SassScriptException(
56+
"\$min $unit1 but $arg2Name $unit2. Arguments must all have units or all "
57+
"be unitless.");
58+
});
59+
4260
final _floor = _numberFunction("floor", (value) => value.floor());
43-
final _abs = _numberFunction("abs", (value) => value.abs());
4461

4562
final _max = _function("max", r"$numbers...", (arguments) {
4663
SassNumber max;
@@ -62,31 +79,258 @@ final _min = _function("min", r"$numbers...", (arguments) {
6279
throw SassScriptException("At least one argument must be passed.");
6380
});
6481

65-
final _randomFunction = _function("random", r"$limit: null", (arguments) {
66-
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
67-
var limit = arguments[0].assertNumber("limit").assertInt("limit");
68-
if (limit < 1) {
69-
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
82+
final _round = _numberFunction("round", fuzzyRound);
83+
84+
///
85+
/// Distance functions
86+
///
87+
88+
final _abs = _numberFunction("abs", (value) => value.abs());
89+
90+
final _hypot = _function("hypot", r"$numbers...", (arguments) {
91+
var numbers =
92+
arguments[0].asList.map((argument) => argument.assertNumber()).toList();
93+
if (numbers.isEmpty) {
94+
throw SassScriptException("At least one argument must be passed.");
7095
}
71-
return SassNumber(_random.nextInt(limit) + 1);
96+
97+
var numeratorUnits = numbers[0].numeratorUnits;
98+
var denominatorUnits = numbers[0].denominatorUnits;
99+
var subtotal = 0.0;
100+
for (var i = 0; i < numbers.length; i++) {
101+
var number = numbers[i];
102+
if (number.hasUnits == numbers[0].hasUnits) {
103+
number = number.coerce(numeratorUnits, denominatorUnits);
104+
subtotal += math.pow(number.value, 2);
105+
} else {
106+
var unit1 = numbers[0].hasUnits
107+
? "has unit ${numbers[0].unitString}"
108+
: "is unitless";
109+
var unit2 =
110+
number.hasUnits ? "has unit ${number.unitString}" : "is unitless";
111+
throw SassScriptException(
112+
"Argument 1 $unit1 but argument ${i + 1} $unit2. Arguments must all "
113+
"have units or all be unitless.");
114+
}
115+
}
116+
return SassNumber.withUnits(math.sqrt(subtotal),
117+
numeratorUnits: numeratorUnits, denominatorUnits: denominatorUnits);
72118
});
73119

74-
final _unit = _function("unit", r"$number", (arguments) {
120+
///
121+
/// Exponential functions
122+
///
123+
124+
final _log = _function("log", r"$number, $base: null", (arguments) {
75125
var number = arguments[0].assertNumber("number");
76-
return SassString(number.unitString, quotes: true);
126+
if (number.hasUnits) {
127+
throw SassScriptException("\$number: Expected $number to have no units.");
128+
}
129+
130+
var numberValue = _fuzzyRoundIfZero(number.value);
131+
if (arguments[1] == sassNull) return SassNumber(math.log(numberValue));
132+
133+
var base = arguments[1].assertNumber("base");
134+
if (base.hasUnits) {
135+
throw SassScriptException("\$base: Expected $base to have no units.");
136+
}
137+
138+
var baseValue = fuzzyEquals(base.value, 1)
139+
? fuzzyRound(base.value)
140+
: _fuzzyRoundIfZero(base.value);
141+
return SassNumber(math.log(numberValue) / math.log(baseValue));
77142
});
78143

79-
final _isUnitless = _function("is-unitless", r"$number", (arguments) {
144+
final _pow = _function("pow", r"$base, $exponent", (arguments) {
145+
var base = arguments[0].assertNumber("base");
146+
var exponent = arguments[1].assertNumber("exponent");
147+
if (base.hasUnits) {
148+
throw SassScriptException("\$base: Expected $base to have no units.");
149+
} else if (exponent.hasUnits) {
150+
throw SassScriptException(
151+
"\$exponent: Expected $exponent to have no units.");
152+
}
153+
154+
// Exponentiating certain real numbers leads to special behaviors. Ensure that
155+
// these behaviors are consistent for numbers within the precision limit.
156+
var baseValue = _fuzzyRoundIfZero(base.value);
157+
var exponentValue = _fuzzyRoundIfZero(exponent.value);
158+
if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
159+
return SassNumber(double.nan);
160+
} else if (fuzzyEquals(baseValue, 0)) {
161+
if (exponentValue.isFinite &&
162+
fuzzyIsInt(exponentValue) &&
163+
fuzzyAsInt(exponentValue) % 2 == 1) {
164+
exponentValue = fuzzyRound(exponentValue);
165+
}
166+
} else if (baseValue.isFinite &&
167+
fuzzyLessThan(baseValue, 0) &&
168+
exponentValue.isFinite &&
169+
fuzzyIsInt(exponentValue)) {
170+
exponentValue = fuzzyRound(exponentValue);
171+
} else if (baseValue.isInfinite &&
172+
fuzzyLessThan(baseValue, 0) &&
173+
exponentValue.isFinite &&
174+
fuzzyIsInt(exponentValue) &&
175+
fuzzyAsInt(exponentValue) % 2 == 1) {
176+
exponentValue = fuzzyRound(exponentValue);
177+
}
178+
return SassNumber(math.pow(baseValue, exponentValue));
179+
});
180+
181+
final _sqrt = _function("sqrt", r"$number", (arguments) {
80182
var number = arguments[0].assertNumber("number");
81-
return SassBoolean(!number.hasUnits);
183+
if (number.hasUnits) {
184+
throw SassScriptException("\$number: Expected $number to have no units.");
185+
}
186+
187+
var numberValue = _fuzzyRoundIfZero(number.value);
188+
return SassNumber(math.sqrt(numberValue));
189+
});
190+
191+
///
192+
/// Trigonometric functions
193+
///
194+
195+
final _acos = _function("acos", r"$number", (arguments) {
196+
var number = arguments[0].assertNumber("number");
197+
if (number.hasUnits) {
198+
throw SassScriptException("\$number: Expected $number to have no units.");
199+
}
200+
201+
var numberValue = fuzzyEquals(number.value.abs(), 1)
202+
? fuzzyRound(number.value)
203+
: number.value;
204+
var acos = math.acos(numberValue) * 180 / math.pi;
205+
return SassNumber.withUnits(acos, numeratorUnits: ['deg']);
206+
});
207+
208+
final _asin = _function("asin", r"$number", (arguments) {
209+
var number = arguments[0].assertNumber("number");
210+
if (number.hasUnits) {
211+
throw SassScriptException("\$number: Expected $number to have no units.");
212+
}
213+
214+
var numberValue = fuzzyEquals(number.value.abs(), 1)
215+
? fuzzyRound(number.value)
216+
: _fuzzyRoundIfZero(number.value);
217+
var asin = math.asin(numberValue) * 180 / math.pi;
218+
return SassNumber.withUnits(asin, numeratorUnits: ['deg']);
219+
});
220+
221+
final _atan = _function("atan", r"$number", (arguments) {
222+
var number = arguments[0].assertNumber("number");
223+
if (number.hasUnits) {
224+
throw SassScriptException("\$number: Expected $number to have no units.");
225+
}
226+
227+
var numberValue = _fuzzyRoundIfZero(number.value);
228+
var atan = math.atan(numberValue) * 180 / math.pi;
229+
return SassNumber.withUnits(atan, numeratorUnits: ['deg']);
230+
});
231+
232+
final _atan2 = _function("atan2", r"$y, $x", (arguments) {
233+
var y = arguments[0].assertNumber("y");
234+
var x = arguments[1].assertNumber("x");
235+
if (y.hasUnits != x.hasUnits) {
236+
var unit1 = y.hasUnits ? "has unit ${y.unitString}" : "is unitless";
237+
var unit2 = x.hasUnits ? "has unit ${x.unitString}" : "is unitless";
238+
throw SassScriptException(
239+
"\$y $unit1 but \$x $unit2. Arguments must all have units or all be "
240+
"unitless.");
241+
}
242+
243+
x = x.coerce(y.numeratorUnits, y.denominatorUnits);
244+
var xValue = _fuzzyRoundIfZero(x.value);
245+
var yValue = _fuzzyRoundIfZero(y.value);
246+
var atan2 = math.atan2(yValue, xValue) * 180 / math.pi;
247+
return SassNumber.withUnits(atan2, numeratorUnits: ['deg']);
248+
});
249+
250+
final _cos = _function("cos", r"$number", (arguments) {
251+
var number = _coerceToRad(arguments[0].assertNumber("number"));
252+
return SassNumber(math.cos(number.value));
82253
});
83254

255+
final _sin = _function("sin", r"$number", (arguments) {
256+
var number = _coerceToRad(arguments[0].assertNumber("number"));
257+
var numberValue = _fuzzyRoundIfZero(number.value);
258+
return SassNumber(math.sin(numberValue));
259+
});
260+
261+
final _tan = _function("tan", r"$number", (arguments) {
262+
var number = _coerceToRad(arguments[0].assertNumber("number"));
263+
var asymptoteInterval = 0.5 * math.pi;
264+
var tanPeriod = 2 * math.pi;
265+
if (fuzzyEquals((number.value - asymptoteInterval) % tanPeriod, 0)) {
266+
return SassNumber(double.infinity);
267+
} else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) {
268+
return SassNumber(double.negativeInfinity);
269+
} else {
270+
var numberValue = _fuzzyRoundIfZero(number.value);
271+
return SassNumber(math.tan(numberValue));
272+
}
273+
});
274+
275+
///
276+
/// Unit functions
277+
///
278+
84279
final _compatible = _function("compatible", r"$number1, $number2", (arguments) {
85280
var number1 = arguments[0].assertNumber("number1");
86281
var number2 = arguments[1].assertNumber("number2");
87282
return SassBoolean(number1.isComparableTo(number2));
88283
});
89284

285+
final _isUnitless = _function("is-unitless", r"$number", (arguments) {
286+
var number = arguments[0].assertNumber("number");
287+
return SassBoolean(!number.hasUnits);
288+
});
289+
290+
final _unit = _function("unit", r"$number", (arguments) {
291+
var number = arguments[0].assertNumber("number");
292+
return SassString(number.unitString, quotes: true);
293+
});
294+
295+
///
296+
/// Other functions
297+
///
298+
299+
final _percentage = _function("percentage", r"$number", (arguments) {
300+
var number = arguments[0].assertNumber("number");
301+
number.assertNoUnits("number");
302+
return SassNumber(number.value * 100, '%');
303+
});
304+
305+
final _random = math.Random();
306+
307+
final _randomFunction = _function("random", r"$limit: null", (arguments) {
308+
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
309+
var limit = arguments[0].assertNumber("limit").assertInt("limit");
310+
if (limit < 1) {
311+
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
312+
}
313+
return SassNumber(_random.nextInt(limit) + 1);
314+
});
315+
316+
///
317+
/// Helpers
318+
///
319+
320+
num _fuzzyRoundIfZero(num number) {
321+
if (!fuzzyEquals(number, 0)) return number;
322+
return number.isNegative ? -0.0 : 0;
323+
}
324+
325+
SassNumber _coerceToRad(SassNumber number) {
326+
try {
327+
return number.coerce(['rad'], []);
328+
} on SassScriptException catch (error) {
329+
if (!error.message.startsWith('Incompatible units')) rethrow;
330+
throw SassScriptException('\$number: Expected ${number} to be an angle.');
331+
}
332+
}
333+
90334
/// Returns a [Callable] named [name] that transforms a number's value
91335
/// using [transform] and preserves its units.
92336
BuiltInCallable _numberFunction(String name, num transform(num value)) {
@@ -98,7 +342,7 @@ BuiltInCallable _numberFunction(String name, num transform(num value)) {
98342
});
99343
}
100344

101-
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:math`.
345+
/// Like [new _function.function], but always sets the URL to `sass:math`.
102346
BuiltInCallable _function(
103347
String name, String arguments, Value callback(List<Value> arguments)) =>
104348
BuiltInCallable.function(name, arguments, callback, url: "sass:math");

0 commit comments

Comments
 (0)