Challenge #1 – Putting the Fizz back in your Buzz

5 November 2018 Solved Twig Intermediate

A variation of FizzBuzz, this challenge tests your ability to output or style things in different ways based on a recurring indexing pattern using Twig.

In FizzBuzz, counting from 1 to 100, you replace the number each time you encounter a multiple of 3 with "Fizz", a multiple of 5 with "Buzz" and a multiple of both 3 and 5 with "FizzBuzz". So the result is:

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

Challenge

The challenge is to write a twig macro called "fizzBuzz" that will accept 3 parameters: entries (an array of entries), fizz (an array of integers) and buzz (also an array of integers). The macro should output the titles of each of the provided entries as header tags with a class of "fizz" each time a multiple of one or more of the integers in the fizz array is encountered, "buzz" each time a multiple of or more one of the integers in the buzz array is encountered, and "fizzbuzz" each time a multiple 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 output:

<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 output the tags with the appropriate classes given the 3 parameters as described above. It should not rely on any plugins and the code will be evaluated based on the following criteria in order of priority:

  1. Readability
  2. Brevity
  3. Performance

Therefore the code should be readable and easy to understand. It should be concise and non-repetative, but not at the expense of readability. It should be performant, but not at the expense of readability or brevity.

Tips

  • It may help to start by solving the challenge with the fizz and buzz parameters as integers.
  • You can link to this CSS file to distinguish the header tags in the output.
  • To test with large data sets, you can create an array of a single repeating 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 challenge, I was astounded by the number and quality of solutions that were submitted. Thank you to everyone who took part in the challenge, regardless of whether you submitted a solution or not. Hopefully you had fun and learned something along the way!!

To get us started, let's look at how we might solve the challenge 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 %}

Similar solutions: Josh Magness.

We've looped through the entries and for each loop index i, we check if i is divisible by fizz and buzz, updating the appropriate class if it is. Finally, we output the header class by concatenating fizzClass and buzzClass.

We use the divisible by operator to check if the integer i is divisible by the integer fizz. This can also be done using the modulo operator % to check if the remainder equals 0, but is perhaps less human readable than the method used above.

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

Now let's tackle the challenge of fizz and buzz being passed in as arrays. We'll need to loop through each of them in order to determine whether the loop index i is divisible 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 %}

Similar solutions: John Wells, Christian Seelbach, Craft CMS Berlin Meetup, Quentin Delcourt, Otto Radics.

This solution works well and is readable, but there is a potential performance hit as we loop through the entire fizz and buzz arrays regardless of whether the fizzClass is empty or has been set. So for example, if i = 3 and fizz = [3, 7, 22] then the following loop will be iterated over 3 times, unnecessarily checking if i is divisible 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 possible with the Twig Perversion plugin). So instead, we can avoid this by adding a condition to the for loop, which will prevent 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 %}

Resulting in a slightly more performant 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 %}

Similar solutions: Spenser Hannon.


This can also be achieved using a single array variable rather than 2 string variables to represent 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 %}

Similar solutions: Pierre Stoffe.


This can be further simplified to use a single string variable to represent 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 %}

Similar solutions: Trevor Plassman, Jason Sawyer.


This can be condensed by combining the set statements and the if statements, however 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 %}

Similar solutions: Alex Roper, Henry Bley-Vroman.


You've probably noticed that the for loop over the elements in fizz and buzz are almost identical. We can avoid repeating ourselves by adding the fizz and buzz parameters to an associative array tests and looping over that array, running 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 %}

Similar solutions: Paul Verheul, Steve Rowling.


Another way to avoid repeating ourselves is by adding a helper macro that will handle the logic for us and output the provided class only if the number is divisible 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 spaceless tag to remove whitespace within the block, as well as the whitespace control modifier to remove the leading (-%}) and trailing ({%-) whitespace between the macro tags.

We can then call our new getClassIfDivisible macro as many times as we need to from within our fizzBuzz macro, providing we first import it from _self (assuming 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 %}

Similar solutions: Andrew Welch, Doug St. John, John F Morton, Patrick Harrington, Lindsey DiLoreto.

The solution above is analogous to creating a helper function in PHP which returns the result of a calculation that has to be performed multiple times.


There are of course many more possible solutions to the challenge. Oliver Stark, the evil genius, submitted this solution, 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 acknowledge the team effort and solution submitted by the Craft CMS Berlin Meetup team (Oliver, Mike, Kristian and Tom). While working on the solution, they stumbled upon a potential bug in Twig, which can be demonstrated as follows.

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

The code above should output 1-1, 1-2, 1-3, and so on, but instead it throws an error with devMode enabled and outputs a blank string otherwise. Referencing loop.index in the outer loop, however, 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 discovered and explained by Brad Bell, is as follows:

When the twig node visitor is parsing a for loop, they set the with_loop to false by default to determine if loop should be included in the context (source). But it looks like they're only checking for the current loop, not for any nested loops (source), which would explain why it would work if you reference loop in the outer.

Thanks Brad, for putting out the fire that erupted in the slack channel. That was a blast!!

Putting out the fire

Submitted Solutions

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