@@ -13,34 +13,51 @@ import '../module/built_in.dart';
13
13
import '../util/number.dart' ;
14
14
import '../value.dart' ;
15
15
16
- /// A random number generator.
17
- final _random = math.Random ();
18
-
19
16
/// The global definitions of Sass math functions.
20
17
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" ),
23
21
_isUnitless.withName ("unitless" ),
24
- _compatible.withName ("comparable" )
25
22
]);
26
23
27
24
/// The Sass math module.
28
25
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),
38
32
});
39
33
40
- final _round = _numberFunction ("round" , fuzzyRound);
34
+ ///
35
+ /// Bounding functions
36
+ ///
37
+
41
38
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
+
42
60
final _floor = _numberFunction ("floor" , (value) => value.floor ());
43
- final _abs = _numberFunction ("abs" , (value) => value.abs ());
44
61
45
62
final _max = _function ("max" , r"$numbers..." , (arguments) {
46
63
SassNumber max;
@@ -62,31 +79,258 @@ final _min = _function("min", r"$numbers...", (arguments) {
62
79
throw SassScriptException ("At least one argument must be passed." );
63
80
});
64
81
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." );
70
95
}
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);
72
118
});
73
119
74
- final _unit = _function ("unit" , r"$number" , (arguments) {
120
+ ///
121
+ /// Exponential functions
122
+ ///
123
+
124
+ final _log = _function ("log" , r"$number, $base: null" , (arguments) {
75
125
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));
77
142
});
78
143
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) {
80
182
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));
82
253
});
83
254
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
+
84
279
final _compatible = _function ("compatible" , r"$number1, $number2" , (arguments) {
85
280
var number1 = arguments[0 ].assertNumber ("number1" );
86
281
var number2 = arguments[1 ].assertNumber ("number2" );
87
282
return SassBoolean (number1.isComparableTo (number2));
88
283
});
89
284
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
+
90
334
/// Returns a [Callable] named [name] that transforms a number's value
91
335
/// using [transform] and preserves its units.
92
336
BuiltInCallable _numberFunction (String name, num transform (num value)) {
@@ -98,7 +342,7 @@ BuiltInCallable _numberFunction(String name, num transform(num value)) {
98
342
});
99
343
}
100
344
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` .
102
346
BuiltInCallable _function (
103
347
String name, String arguments, Value callback (List <Value > arguments)) =>
104
348
BuiltInCallable .function (name, arguments, callback, url: "sass:math" );
0 commit comments