Challenge #1 – Putting the Fizz back in your Buzz

5 November 2018 Solved Twig Intermediate

A vari­ation of FizzBuzz, this chal­lenge tests your abil­ity to out­put or style things in dif­fer­ent ways based on a recur­ring index­ing pat­tern using Twig.

In FizzBuzz, count­ing from 1 to 100, you replace the num­ber each time you encounter a mul­tiple of 3 with Fizz”, a mul­tiple of 5 with Buzz” and a mul­tiple of both 3 and 5 with FizzBuzz”. So the res­ult is:

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, ...

Chal­lenge

The chal­lenge is to write a twig macro called fizzBuzz” that will accept 3 para­met­ers: entries (an array of entries), fizz (an array of integers) and buzz (also an array of integers). The macro should out­put the titles of each of the provided entries as head­er tags with a class of fizz” each time a mul­tiple of one or more of the integers in the fizz array is encountered, buzz” each time a mul­tiple of or more one of the integers in the buzz array is encountered, and fizzbuzz” each time a mul­tiple of one or more of the integers in both the fizz and buzz arrays is encountered. So for example, calling:

{% set entries = craft.entries.limit(22).all() %}

{{ fizzBuzz(entries, [3, 7], [5, 17]) }}

Should out­put:

<h4 class="">Entry Title 1</h4>
<h4 class="">Entry Title 2</h4>
<h4 class="fizz">Entry Title 3</h4>
<h4 class="">Entry Title 4</h4>
<h4 class="buzz">Entry Title 5</h4>
<h4 class="fizz">Entry Title 6</h4>
<h4 class="fizz">Entry Title 7</h4>
<h4 class="">Entry Title 8</h4>
<h4 class="fizz">Entry Title 9</h4>
<h4 class="buzz">Entry Title 10</h4>
<h4 class="">Entry Title 11</h4>
<h4 class="fizz">Entry Title 12</h4>
<h4 class="">Entry Title 13</h4>
<h4 class="fizz">Entry Title 14</h4>
<h4 class="fizzbuzz">Entry Title 15</h4>
<h4 class="">Entry Title 16</h4>
<h4 class="buzz">Entry Title 17</h4>
<h4 class="fizz">Entry Title 18</h4>
<h4 class="">Entry Title 19</h4>
<h4 class="buzz">Entry Title 20</h4>
<h4 class="fizz">Entry Title 21</h4>
<h4 class="">Entry Title 22</h4>

Rules

The macro must out­put the tags with the appro­pri­ate classes giv­en the 3 para­met­ers as described above. It should not rely on any plu­gins and the code will be eval­u­ated based on the fol­low­ing cri­ter­ia in order of priority:

  1. Read­ab­il­ity
  2. Brev­ity
  3. Per­form­ance

There­fore the code should be read­able and easy to under­stand. It should be con­cise and non-repet­at­ive, but not at the expense of read­ab­il­ity. It should be per­form­ant, but not at the expense of read­ab­il­ity or brevity.

Tips

  • It may help to start by solv­ing the chal­lenge with the fizz and buzz para­met­ers as integers. 
  • You can link to this CSS file to dis­tin­guish the head­er tags in the output.
  • To test with large data sets, you can cre­ate an array of a single repeat­ing entry as follows:
{% set entries = [] %}
{% set entry = craft.entries.one() %}
{% for i in 1..100 %}
    {% set entries = entries|merge([entry]) %}
{% endfor %}

Solution

Being the first chal­lenge, I was astoun­ded by the num­ber and qual­ity of solu­tions that were sub­mit­ted. Thank you to every­one who took part in the chal­lenge, regard­less of wheth­er you sub­mit­ted a solu­tion or not. Hope­fully you had fun and learned some­thing along the way!!

To get us star­ted, let’s look at how we might solve the chal­lenge with fizz and buzz provided as integers rather than arrays.

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set fizzClass = '' %}
        {% set buzzClass = '' %}

        {% if i is divisible by(fizz) %}
            {% set fizzClass = 'fizz' %}
        {% endfor %}

        {% if i is divisible by(buzz) %}
            {% set buzzClass = 'buzz' %}
        {% endfor %}
        
        <h4 class="{{ fizzClass ~ buzzClass }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Josh Mag­ness.

We’ve looped through the entries and for each loop index i, we check if i is divis­ible by fizz and buzz, updat­ing the appro­pri­ate class if it is. Finally, we out­put the head­er class by con­cat­en­at­ing fizzClass and buzzClass.

We use the divis­ible by oper­at­or to check if the integer i is divis­ible by the integer fizz. This can also be done using the mod­ulo oper­at­or % to check if the remainder equals 0, but is per­haps less human read­able than the meth­od used above. 

{% if i % fizz == 0 %}
    {% set fizzClass = 'fizz' %}
{% endfor %}

Now let’s tackle the chal­lenge of fizz and buzz being passed in as arrays. We’ll need to loop through each of them in order to determ­ine wheth­er the loop index i is divis­ible by one or more of the values.

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set fizzClass = '' %}
        {% set buzzClass = '' %}

        {% for n in fizz %}
            {% if i is divisible by(n) %}
                {% set fizzClass = 'fizz' %}
            {% endif %}
        {% endfor %}

        {% for n in buzz %}
            {% if i is divisible by(n) %}
                {% set buzzClass = 'buzz' %}
            {% endif %}
        {% endfor %}

        <h4 class="{{ fizzClass ~ buzzClass }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: John Wells, Chris­ti­an Seel­bach, Craft CMS Ber­lin Meetup, Quentin Del­court, Otto Radics.

This solu­tion works well and is read­able, but there is a poten­tial per­form­ance hit as we loop through the entire fizz and buzz arrays regard­less of wheth­er the fizzClass is empty or has been set. So for example, if i = 3 and fizz = [3, 7, 22] then the fol­low­ing loop will be iter­ated over 3 times, unne­ces­sar­ily check­ing if i is divis­ible by n each and every time:

{% for n in fizz %}
    {% if i is divisible by(n) %}
        {% set fizzClass = 'fizz' %}
    {% endif %}
{% endfor %}

We could avoid this if there was a way to break out of a loop in Twig like in PHP, but there isn’t (it is pos­sible with the Twig Per­ver­sion plu­gin). So instead, we can avoid this by adding a con­di­tion to the for loop, which will pre­vent the loop being entered as soon as fizzClass has been set:

{% for n in fizz if not fizzClass %}
    {% if i is divisible by(n) %}
        {% set fizzClass = 'fizz' %}
    {% endif %}
{% endfor %}

Res­ult­ing in a slightly more per­form­ant macro. 

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set fizzClass = '' %}
        {% set buzzClass = '' %}

        {% for n in fizz if not fizzClass %}
            {% if i is divisible by(n) %}
                {% set fizzClass = 'fizz' %}
            {% endif %}
        {% endfor %}

        {% for n in buzz if not buzzClass %}
            {% if i is divisible by(n) %}
                {% set buzzClass = 'buzz' %}
            {% endif %}
        {% endfor %}

        <h4 class="{{ fizzClass ~ buzzClass }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Spenser Han­non.


This can also be achieved using a single array vari­able rather than 2 string vari­ables to rep­res­ent the classes.

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set classes = [] %}

        {% for n in fizz if 'fizz' not in classes %}
            {% if i is divisible by(n) %}
                {% set classes = ['fizz'] %}
            {% endif %}
        {% endfor %}

        {% for n in buzz if 'buzz' not in classes %}
            {% if i is divisible by(n) %}
                {% set classes = classes|merge(['buzz']) %}
            {% endif %}
        {% endfor %}

        <h4 class="{{ classes|join('') }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Pierre Stoffe.


This can be fur­ther sim­pli­fied to use a single string vari­able to rep­res­ent the class.

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set class = '' %}

        {% for n in fizz if 'fizz' not in class %}
            {% if i is divisible by(n) %}
                {% set class = 'fizz' %}
            {% endif %}
        {% endfor %}

        {% for n in buzz if 'buzz' not in class %}
            {% if i is divisible by(n) %}
                {% set class = class ~ 'buzz' %}
            {% endif %}
        {% endfor %}

        <h4 class="{{ class }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Tre­vor Plass­man, Jason Saw­yer.


This can be con­densed by com­bin­ing the set state­ments and the if state­ments, how­ever this comes at the expense of readability.

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i, class = loop.index, '' %}

        {% for n in fizz if 'fizz' not in class and i is divisible by(n) %}
            {% set class = 'fizz' %}
        {% endfor %}

        {% for n in buzz if 'buzz' not in class and i is divisible by(n) %}
            {% set class = class ~ 'buzz' %}
        {% endfor %}

        <h4 class="{{ class }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Alex Rop­er, Henry Bley-Vro­man.


You’ve prob­ably noticed that the for loop over the ele­ments in fizz and buzz are almost identic­al. We can avoid repeat­ing ourselves by adding the fizz and buzz para­met­ers to an asso­ci­at­ive array tests and loop­ing over that array, run­ning our logic on each set of values and using the key as the className.

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set class = '' %}
        {% set tests = {'fizz': fizz, 'buzz': buzz} %}

        {% for className, values in tests %}
            {% for n in values if className not in class %}
                {% if i is divisible by(n) %}
                    {% set class = class ~ className %}
                {% endif %}
            {% endfor %}
        {% endfor %}

        <h4 class="{{ class }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Paul Ver­heul, Steve Rowl­ing.


Anoth­er way to avoid repeat­ing ourselves is by adding a help­er macro that will handle the logic for us and out­put the provided class only if the number is divis­ible by one or more of the integers in values.

{% macro getClassIfDivisible(className, number, values) -%}

    {% spaceless %}
        {% set result = '' %}

        {% for value in values if not result %}
            {% if number is divisible by(value) %}
                {% set result = className %}
            {% endif %}
        {% endfor %}

        {{ result }}
    {% endspaceless %}

{%- endmacro %}

Notice how we use the space­less tag to remove whitespace with­in the block, as well as the whitespace con­trol mod­i­fi­er to remove the lead­ing (-%}) and trail­ing ({%-) whitespace between the macro tags.

We can then call our new getClassIfDivisible macro as many times as we need to from with­in our fizzBuzz macro, provid­ing we first import it from _self (assum­ing it exists in the same file).

{% macro fizzBuzz(entries, fizz, buzz) %}

    {% from _self import getClassIfDivisible %}

    {% for entry in entries %}
        {% set i = loop.index %}
        {% set class = '' %}
        
        {% set class = class ~ getClassIfDivisible('fizz', i, fizz) %}
        {% set class = class ~ getClassIfDivisible('buzz', i, buzz) %}
        
        <h4 class="{{ class }}">{{ entry.title }} #{{ loop.index }}</h4>
    {% endfor %}

{% endmacro %}

Sim­il­ar solu­tions: Andrew Welch, Doug St. John, John F Mor­ton, Patrick Har­ring­ton, Lind­sey DiLoreto.

The solu­tion above is ana­log­ous to cre­at­ing a help­er func­tion in PHP which returns the res­ult of a cal­cu­la­tion that has to be per­formed mul­tiple times.


There are of course many more pos­sible solu­tions to the chal­lenge. Oliv­er Stark, the evil geni­us, sub­mit­ted this solu­tion, which made me almost fall out of my chair with laughter when I saw it. 

{% macro fizzBuzz(not, so, important) %}

    <h4 class="">Entry Title 1</h4>
    <h4 class="">Entry Title 2</h4>
    <h4 class="fizz">Entry Title 3</h4>
    <h4 class="">Entry Title 4</h4>
    <h4 class="buzz">Entry Title 5</h4>
    <h4 class="fizz">Entry Title 6</h4>
    <h4 class="fizz">Entry Title 7</h4>
    <h4 class="">Entry Title 8</h4>
    <h4 class="fizz">Entry Title 9</h4>
    <h4 class="buzz">Entry Title 10</h4>
    <h4 class="">Entry Title 11>/h4>
    <h4 class="fizz">Entry Title 12</h4>
    <h4 class="">Entry Title 13</h4>
    <h4 class="fizz">Entry Title 14</h4>
    <h4 class="fizzbuzz">Entry Title 15</h4>
    <h4 class="">Entry Title 16</h4>
    <h4 class="buzz">Entry Title 17</h4>
    <h4 class="fizz">Entry Title 18</h4>
    <h4 class="">Entry Title 19</h4>
    <h4 class="buzz">Entry Title 20</h4>
    <h4 class="fizz">Entry Title 21</h4>
    <h4 class="">Entry Title 22</h4>
    
{% endmacro %}


Craft CMS Berlin Meetup

Finally, I would like to acknow­ledge the team effort and solu­tion sub­mit­ted by the Craft CMS Ber­lin Meetup team (Oliv­er, Mike, Kris­ti­an and Tom). While work­ing on the solu­tion, they stumbled upon a poten­tial bug in Twig, which can be demon­strated as follows.

{% for entry in entries %}
    {% for n in fizz %}
        {{ loop.parent.loop.index }}-{{ loop.index }},
    {% endfor %}
{% endfor %}

The code above should out­put 1-1, 1-2, 1-3, and so on, but instead it throws an error with devMode enabled and out­puts a blank string oth­er­wise. Ref­er­en­cing loop.index in the out­er loop, how­ever, makes it work as expected.

{% for entry in entries %}
    {% set i = loop.index %}
    
    {% for n in fizz %}
        {{ loop.parent.loop.index }}-{{ loop.index }},
    {% endfor %}
{% endfor %}

The issue, as dis­covered and explained by Brad Bell, is as follows:

When the twig node vis­it­or is pars­ing a for loop, they set the with_loop to false by default to determ­ine if loop should be included in the con­text (source). But it looks like they’re only check­ing for the cur­rent loop, not for any nes­ted loops (source), which would explain why it would work if you ref­er­ence loop in the outer.

Thanks Brad, for put­ting out the fire that erup­ted in the slack chan­nel. That was a blast!!

Putting out the fire

Submitted Solutions

  • Jason Sawyer
  • Otto Radics
  • Josh Magness
  • Quentin Delcourt
  • Henry Bley-Vroman
  • Alex Roper
  • Craft CMS Berlin Meetup
  • Pierre Stoffe
  • Lindsey DiLoreto
  • Andrew Welch
  • Patrick Harrington
  • John F Morton
  • Spenser Hannon
  • Doug St. John
  • Trevor Plassman
  • Oliver Stark
  • Steve Rowling
  • Christian Seelbach
  • Paul Verheul
  • John Wells