Edit : A better version of the code can be found here.
Hey people! I am currently working on a JSON parser in Sass (yes, you read right) thus I faced an issue I thought unsolvable until now: casting a string into a number in Sass. Needless to say I found a solution! Even better, I found a solution to convert a string into a valid CSS length you can use as a CSS value, in calculations and stuff.
I have to say I am pretty proud with what I have come up with. Not only does it work, but it is also very simple and from what I can tell quite efficient. This may be a bit slower for very large numbers but even there I’m not sure we can feel the difference in compilation time. It also lacks of support for very scientific notation like e
but that’s no big deal for now.
Building the function
As I said, the function is actually simple. It relies on parsing the string character after character in order to map them to actual numbers. Then once you have numbers — well — you can do pretty much any thing. Let’s start with the skeleton, shall we?
@function number($string) {
// Matrices
$strings: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
$numbers: 0 1 2 3 4 5 6 7 8 9;
// Result
$result: 0;
// Looping through all characters
@for $i from 1 through str-length($string) {
// Do magic
}
@return $result;
}
I think you can see where this is going. Now let’s have a look at what happens inside the loop:
@for $i from 1 through str-length($string) {
$character: str-slice($string, $i, $i);
$index: index($strings, $character);
@if not $index {
@warn "Unknown character `#{$character}`.";
@return false;
}
$number: nth($numbers, $index);
$result: $result * 10 + $number;
}
And this is enough to cast any positive integer from a string. But wait! What about negative integers? Plus I told you number
, not integer
. Let’s continue the journey!
Dealing with negative numbers
Dealing with negative numbers is very easy: if we spot a dash (-
) as a first character, then it’s a negative number. Thus, all we have to do is to multiply $result
by -1
(as soon as $result
isn’t 0
).
@function number($string) {
// …
$result: 0;
$minus: false;
@for $i from 1 through str-length($string) {
// …
@if $character == '-' {
$minus: true;
}
@else {
// …
$result: $result * 10 + $number;
}
@return if($minus, $result * -1, $result);
}
As I said, it is pretty straight forward.
Dealing with decimal dot
Making sure we can convert floats and doubles took me a couple of minutes. I couldn’t find a way to deal with numbers once the decimal dot has been found. I always ended up with a completely wrong result until I find a tricky way.
@function number($string) {
// …
$result: 0;
$divider: 0;
@for $i from 1 through str-length($string) {
// …
@if $character == '-' {
// …
} @else if $character == '.' {
$divider: 1;
} @else {
// …
// Decimal dot hasn’t been found yet
@if $divider == 0 {
$result: $result * 10;
}
// Decimal dot has been found
@else {
// Move the decimal dot to the left
$divider: $divider * 10;
$number: $number / $divider;
}
$result: $result + $number;
}
}
@return if($minus, $result * -1, $result);
}
Since it can be a little tricky to understand, let’s try with a quick example. Here is what happen when we try to cast "13.37" to a number:
- We set
$divider
and$result
variables to0
"1"
gets found$divider
is0
so$result
gets multiplied by10
(still0
)1
gets added to$result
(now1
)
"3"
gets found$divider
is0
so$result
gets multiplied by10
(now10
)3
gets added to$result
(now13
)
"."
gets found$divider
is now set to1
"3"
gets found$divider
is greater than0
so it gets multiplied by10
(now10
)3
gets divided by$divider
(now0.3
)0.3
gets added to$result
(now13.3
)
"7"
gets found$divider
is greater than0
so it gets multiplied by10
(now100
)7
gets divided by$divider
(now0.07
)0.07
gets added to$result
(now13.37
)
Dealing with CSS lengths
All we have left is the ability to retrieve the correct unit from the string and returning the length. At first I thought it would be hard to do, but it turned out to be very easy. I moved this to a second function to keep things clean but you could probably merge both functions.
First we need to get the unit as a string. It’s basically the string starting from the first not-numeric character. In "42px"
, it would be "px"
. We only need to slightly tweak our function to get this.
@function number($string) {
// …
@for $i from 1 through str-length($string) {
// …
@if $char == '-' {
// …
} @else if $char == '.' {
// …
} @else {
@if not $index {
$result: if($minus, $result * -1, $result);
@return _length($result, str-slice($string, $i));
}
// …
}
}
// …
}
If we come to find a character that is neither -
, nor .
nor a number, it means we are moving onto the unit. Then we can return the result of the _length
function.
@function _length($number, $unit) {
$strings: 'px' 'cm' 'mm' '%' 'ch' 'pica' 'in' 'em' 'rem' 'pt' 'pc' 'ex' 'vw'
'vh' 'vmin' 'vmax';
$units: 1px 1cm 1mm 1% 1ch 1pica 1in 1em 1rem 1pt 1pc 1ex 1vw 1vh 1vmin 1vmax;
$index: index($strings, $unit);
@if not $index {
@warn "Unknown unit `#{$unit}`.";
@return false;
}
@return $number * nth($units, $index);
}
The idea is the same as for the number
function. We retrieve the string in the $strings
list in order to map it to an actual CSS length from the $units
list, then we return the product of $number
and the length. If the unit doesn’t exist, we simply return false.
Examples
If you want to play with the code or the function, you can check it on SassMeister. In any case, here are a couple of examples of our awesome little function:
sass {
cast: number('-15'); // -15
cast: number('-1'); // -1
cast: number('-.5'); // -.5
cast: number('-0'); // 0
cast: number('0'); // 0
case: number('.10'); // 0.1
cast: number('1'); // 1
cast: number('1.5'); // 1.5
cast: number('10.'); // 10
cast: number('12.380'); // 12.38
cast: number('42'); // 42
cast: number('1337'); // 1337
cast: number('-10px'); // -10px
cast: number('20em'); // 20em
cast: number('30ch'); // 30ch
cast: number('1fail'); // Error
cast: number('string'); // Error
}
Final words
So people, what do you think? Pretty cool isn’t it? I’d be glad to see what you could be using this for so if you ever come up with a usecase, be sure to share. ;)
Oh by the way if you need to cast a number into a string, it is nothing easier than $number + unquote("")
.