whitespace

Post Image Description

Doing Math with Sass

CSS (Cascading Stylesheets) is language for styling HTML elements. It's what makes websites look sleek and user-friendly. For the longest time, developers used pure CSS in projects. However in recent years, the importance for websites to look and feel unique has grown, and so has the size and complexity of CSS documents. So much so that pure CSS is now for the most part unmanageable for several reasons:

  1. No variable declaration. The problem with this is if, for example, you have a colour scheme of yellow and purple and you want to change yellow to grey everywhere on the site, you would have to go through the entire document and manually change each instance of the colour.
  2. No nesting. The lack of nesting means a lot of code has to be repeated. Complex CSS selectors have to be rewritten.
  3. No polymorphism (abstraction). Oftentimes, elements such as buttons, inputs, etc. will have some shared styles with the sole difference being colour, size, etc... Without a simple way to define shared styles, a lot of repeated code appears in each variation of the element.

These all decreases maintainability, which is important in this fast-paced industry because things are always changing. There are a few other issues but these are the ones I've run into most often when dealing with pure CSS.

However, these problems are all addressed with the emergence of CSS preprocessors like LESS, Stylus, Sass (which we're going to take a look at in this post), and others. A preprocessor is a tool used to compile code written in one language into another language. In the case of CSS preprocessors, the output language is CSS. This preprocessing is necessary because web browsers will not render Sass, or any other CSS variation -- only pure CSS can be rendered.

I like using Sass over some o the other preprocessors because its syntax is very similar to pure CSS and even more similar with Sass's SCSS syntax (which I use for almost all my projects). I've been using Sass for a couple years now but I've never looked very deep into how powerful Sass is until now. So my goal with this exercise is to test the limits of Sass by doing some math with it. First we'll start with some simple evaluations and then work our way up to more complex algorithms. I often use Python for writing algorithms so I decided to use the Sass syntax (rather than SCSS) because it's similar to Python syntax.

I also hope, if you've never used Sass before, that going through these challenges will introduce you to some of the syntax. To get started with using Sass, you can learn how to set it up here: Sass Installation.

In this exercise I'll cover:

  • Factorials
  • Summation
  • Sorting (with insertion sort)
  • Polynomials
  • Finding Roots (with the bisection method)

If you're unfamiliar with any of these concepts, don't worry, I'll try to give a brief explanation where necessary. Also, just in case you're not familiar with CSS and Sass, I want to point out that what I'm attempting to do here is incredibly unconventional and it is NEVER EVER to be done in practice. This is only of an exercise in curiosity to see what is possible with Sass. If you want to do any of these calculations correctly, use a language like Python, MATLAB, or R, which meant for writing algorithms.

That's enough introduction, so let's get started!

Setting Up the View

I wanted to leave the HTML document as bare as possible and move as much of the content into Sass as I could. So I only included a head element and, to import the stylesheet, a link element. The body element what we will bind the data to. (The title element is just for aesthetics.)

sass-math.html

<!DOCTYPE html>
<html>
    <head>
        <title>Sass Math</title>
        <link rel="stylesheet" href="main.css">
    </head>
    <body></body>
</html>

Now before we start adding logic, I want to set up a main file, which will import the other modules. This will help organize the project and keep everything modular. The order in which these modules are imported matter because functions and variables in Sass must be defined before they are used.

main.sass

// Performs Calculations
@import 'functions'

// Specifies Data & Calculations to Use
@import 'input'

// Displays Results
@import 'view'

The input module is where we will define the operations we want to carry out and set the variables $input and $result which will later be bound to the view.

_input.sass

/*****************************************
 *  USER INPUT
 *****************************************/

$input: Input
$result: Result

//*****************************************

Now I want to give our HTML some styles (we are using stylesheets after all) and also bind the variables that will eventually contain the input and results of our math operations.

For the head, I decided to display it's content and bind the whitespace name to it just to add some extra fill to the view without adding to the HTML document.

I was able to bind my 'Input' and 'Result' strings to the body tag by using the shadow DOM's :before and :after selectors with have the option to add a style called content which can contain a string of a number value. This value is then appended to the top and bottom of the body element. Content can only be bound before and after elements though; binding to the root element is not allowed.

_view.sass

/*****************************************
 *  VIEW & DATA BINDING
 *****************************************/

// Color Scheme for the view
$primary: #61bd4f
$accent: #699
$dark: #61774f

// Styles for the head and title
head
    display: block      // Makes the head element visible

    title
        display: block  // Makes the title element visible
        margin-top: 20px

        color: white
        text-align: center
        font-family: Palatino
        font-style: italic
        font-size: 60px
        text-shadow: 2px 2px 5px #000

        &:after
            display: block
            margin-top: 30px

            font-family: menlo
            font-style: normal
            font-size: 0.4em
            font-weight: 100
            letter-spacing: 0px
            text-shadow: none

            content: "whitespace"   // binds "whitespace" after the title

// Define styles for the body
body 
    padding-top: 100px
    background: $primary

    color: white
    text-align: center
    font-family: menlo

    &:before
        display: block
        font-size: 30px

        content: "#{$input}"        // Binds the input before the body

    &:after
        display: inline-block
        min-width: 200px
        margin: 1em 0
        padding: 10px
        border-radius: 10px
        background: rgba(white, 0.35)
        box-shadow: 0 2px 5px $dark inset

        font-size: 20px

        content: "#{$result}"       // Binds the result after the body

//***************************************

... And here's the result:

Screenshot of the view setup.

Now that everything is setup, we can start doing some math with Sass. The basic operations (+, -, *, /) are already supported in CSS so I'll skip over testing these.

The math functions will be stored in the _functions.sass file. Each new function will just be appended to the bottom of this file after the previous ones.


Factorials

Factorials are fairly basic. The factorial of an integer, x, is the result of multiplying all integers x down to 1.

For example: 10! = 10*9*8*7*6*5*4*3*2*1 = 3628800

Let's try it in Sass!

_functions.sass

/*****************************************
 *  Factorials
 *****************************************/

// Performs a factorial of an integer and returns the result
@function factorial($n)
    $result: 1 
    @for $i from 1 through $n
        $result: $result * $i
    @return ($result)

// end factorial()

For the input, I redefined the variable to give the view context for the operation being performed. I do this later on for most of the examples as well.

_input.sass

/*****************************************
 *  USER INPUT
 *****************************************/

// Factorials
$input: 10
$result: factorial($input)

$input: "Evaluate #{$input}!"   // Give context for displaying input

//*****************************************

Screenshot of factorial result.


Summation

Summation is a pretty basic concept as well. We'll sum the numbers 1 though 10.

sum(1,2,3,4,5,6,7,8,9,10) = 55

And it works in Sass too:

_functions.sass

/*****************************************
 *  Summing Numbers
 *****************************************/

// Performs a summation of a set of numbers and returns the result
@function sum($list)
    $result: 0 
    @each $n in $list
        $result: $result + $n
    @return ($result)

// end sum()

This time we had to define a list for the input. A list is the same as an array in other languages.

_input.sass

/*****************************************
 *  USER INPUT
 *****************************************/

// Summations
$input: (1,2,3,4,5,6,7,8,9,10)
$result: sum($input)

$input: "sum(#{$input})"        // Give context for displaying input

//*****************************************

Screenshot of the view setup.


Polynomials

For polynomials to work we need an exponential operator. Exponentials are a bit more of a challenge since Sass doesn't natively support them. So now we have to define a function which will build the exponent operator for us. Once our function is built, we can call it to evaluate exponentials for us, giving it a base and a power.

_functions.sass

/*****************************************
 *  EVALUATING POLYNOMIALS
 *****************************************/

// Takes as parameters a base and an exponent and returns the exponential value
@function exp($base, $exponent)
    // reset value
    $value: $base
    // positive intergers get multiplied
    @if $exponent > 1
        @for $i from 2 through $exponent
            $value: $value * $base
    // negitive intergers get divided. A number divided by itself is 1
    @if $exponent < 1
        @for $i from 0 through -$exponent
            $value: $value / $base
        // return the last value written
    @return ($value)

// end exp()

The polynomial itself can be evaluated without using a function.

To test this I used the function f(x) = x^5 + x - 1, x=2

_input.sass

/*****************************************
 *  USER INPUT
 *****************************************/

// Polynomials
$x: 2
$result: exp($x, 5) + $x - 1

$input: "x^5 + x - 1, x=#{$x}"   // Reformatting the polynomial for display

//*****************************************

Screenshot of the view setup.


Sorting

This one was quite tricky... Sass lists are implemented very differently than in other languages. You can't access elements of a list as you would expect (like so: $list[$index]). Instead, Sass has functions for getting and setting list values. Although I was able to implement an insertion sort algorithm, it was very tiring.

I did not like this one... :/

More about insertion sort.

_functions.sass

/*****************************************
 *  Sorting
 *****************************************/

// Sorts list of numbers using insertion sort algorithm.
@function sort($list) 
    @for $i from 1 through length($list) 
        $curr: nth($list, $i)
        $j: $i - 1

        @while $j > 0 and nth($list, $j) > $curr
            $list: set-nth($list, $j + 1, nth($list, $j))
            $j: $j - 1

        $list: set-nth($list, $j + 1, $curr)

    @return $list
// end sort()

_input.sass

/*****************************************
 *  USER INPUT
 *****************************************/

// Sorting

$input: (3,8,4,4,-5,2,12,0,1,-12)
$result: sort($input)

$input: "sort[#{$input}]"        // Give context for displaying input

//*****************************************

Screenshot of the view setup.


Root of a Function

Now for the real test...

Finding the root of a function seems like a pretty simple task, but for a computer, it involves complex algorithms. The algorithm I will be implementing here is called the bisection method. It starts with an interval where f(x1) is less than 0 and f(x2) is greater than 0, leaving the root somewhere in between. The algorithm then finds the middle of these two points and checks if the result is 0 (the root). If it is not the root, the original interval is narrowed to include the midpoint and one of the two original points (x1 or x2, whichever one has the opposite sign of the midpoint). The process is then repeated until the root is reached or a precision level is reached.

Here's a flowchart that explains the bisection method:

Bisection method flowchart

I also implemented a boolean function to determine if two numbers had the same sign.

/*****************************************
 *  FINDING ROOTS WITH THE BISECTION METHOD
 *****************************************/

// Takes as parameters two signed values and returns a 
// boolean true if the signs are the same and false if not
@function samesign($a, $b)
    $val: $a * $b
    @if $val > 0
        $val: true
    @else
        $val: false
    @return ($val)
// end samesign()

// Takes as parameters a lower bound ($low) and a upper bound 
// ($high) between which exists a root and calculates the root 
// of func(), to a tolerance level ($tol), which must be defined 
// before calling this function.
@function bisect($low, $high, $tol)
    $midpoint: "No root found."
    // Check if root exists
    @if not samesign(func($low), func($high))
        $i: 1
        $err: 1
        // Bisect root until the tolerance level has been satisfied
        @while $err > $tol
            $i: $i + 1
            $err: ($high - $low)/2.0
            $midpoint: ($low + $high)/2.0
            @if samesign(func($low), func($midpoint))
                $low: $midpoint
            @else
                $high: $midpoint

    @return ($midpoint)
// end bisect()

This was also challenging since the bisection function had to use a function given by the user. The surprising thing was that the user function was not given as a parameter for the bisection but it still worked because I called bisect() after defining func().

Because CSS numbers are limited to 5 decimal places, I opted to give a precision of 10-3 as the stoping condition. So the result is not as accurate as I would like it to be, but it's still pretty close!

The function I tested is f(x) = x^5 + x - 1 and a starting interval of x = {0, 1}.

Result from Wolfram Alpha: x~~0.754877666246693

_input.sass

/*****************************************
 *  USER INPUT
 *****************************************/

// Finding Roots with the Bisection Method

// Function of which to find the root
@function func($x)
    @return (exp($x, 5) + $x - 1)

$l: 0           // lower bound
$u: 1           // upper bound
$p: exp(10, -3) // percision

$result: bisect($l, $u, $p)
$f: func($result)

// Formatting for display
$input: "Root of f(x)= x^5 + x - 1 within #{$p} percision."
$result: "(#{$result},#{$f})"

//*****************************************

Screenshot of the view setup.


Reflections

Wow, that was fun!

So what I learned from doing this exercise was that Sass is not meant to do math. It was clunky to use and difficult to write algorithms. I felt as if I was teaching calculus to a painter; and abstractly, I was. Sass is great at what it's meant to do, and that's organizing and modularizing UI elements. It's not so great for doing complex math.

Sass's major weakness was shown in the implementation of the sorting algorithm. Manipulating list elements is not intuitive at all, but in practice, lists generally should be static in Sass. I was going to try an example with solving a system of equations but that would have taken far more effort than I was willing to give.

Some takeaways from this exercise are that we became more familiar with Sass's logical statements, and that different languages are meant to solve different problems. If one language could do everything, then we would only have one to choose from. So keep that in mind when starting future coding projects. Do some research and find the best language for the task you want to accomplish.

Some things that I didn't cover in this post are mixins and media queries. If I were to revisit this challenge again I would try to incorporate those things into the code (maybe in another post).

I hope you learned something! Leave a comment and tell me what you think.

  • David

Additional Resources:


Share This Post:

recent posts