All the elements in a Craft site have contrived a clever plan to hide a secret from you. One of the elements in the site has stored the secret in its title field. The other elements have spawned millions of elements as detours to prevent you from finding out the secret. They have left a trail though, which you must follow to the end to reveal the secret.
Every element (entry, category, user, etc.) in a Craft site stores a value in a custom field called nextElement
, which contains an expression which, when evaluated, reveals the ID of the next element. This creates a trail that you can follow until you find the element that contains the secret in its title. You can determine when you have reached this element because it will have a nextElement
value of null
.
For the purposes of this challenge, we will work with objects rather than Craft elements. Below is an example that should help illustrate the problem.
{% set elements = {
1: {nextElement: 4, title: "I used to think I was indecisive, but now I'm not too sure."},
2: {nextElement: 2, title: "Doing nothing is hard, you never know when you're done."},
3: {nextElement: 5, title: "If two wrongs don't make a right, try three."},
4: {nextElement: 1, title: "I am not lazy, I am on energy saving mode."},
5: {nextElement: 7, title: "Life is short, smile while you still have teeth."},
6: {nextElement: null, title: "Sorry for the mean, awful, accurate things I said."},
7: {nextElement: 9, title: "People say nothing is impossible, but I do nothing every day."},
8: {nextElement: 6, title: "I’m sorry but if you were right, I’d agree with you."},
9: {nextElement: null, title: "The answer to the ultimate question of life is 42."},
} %}
Starting with an element ID of 3, the trail is as follows:
3 > 5 > 7 > 9
And the secret is therefore:
The answer to the ultimate question of life is 42.
The starting point of the trail is of course the key to being able to reveal the secret. You have inside information that the starting point is the element with an ID equal to 3
(the value of the currently installed version of Craft).
The challenge is to write a macro called “revealSecret” that accepts 2 parameters: startId
(the ID to start with) and elements
(a collection of objects indexed by their IDs). The macro should output the trail it follows by outputting the IDs of the elements it encounters, followed by the secret.
So for example calling:
{% set startId = 3 %}
{{ revealSecret(startId, elements) }}
Given the input above will output:
3 > 5 > 7 > 9 > The answer to the ultimate question of life is 42.
The value of nextElement
may contain an expression rather than an integer, that when evaluated reveals the ID of the next element. So for example, the input may be as follows.
{% set elements = {
1: {nextElement: "1+2", title: "I used to think I was indecisive, but now I'm not too sure."},
2: {nextElement: 2, title: "Doing nothing is hard, you never know when you're done."},
3: {nextElement: "2+3", title: "If two wrongs don't make a right, try three."},
4: {nextElement: "4/4", title: "I am not lazy, I am on energy saving mode."},
5: {nextElement: "21/3", title: "Life is short, smile while you still have teeth."},
6: {nextElement: null, title: "Sorry for the mean, awful, accurate things I said."},
7: {nextElement: "3*3", title: "People say nothing is impossible, but I do nothing every day."},
8: {nextElement: "12/2", title: "I’m sorry but if you were right, I’d agree with you."},
9: {nextElement: null, title: "The answer to the ultimate question of life is 42."},
} %}
The value of nextElement
will always be either an integer or a mathematical expression that uses one and only one of the operators +
, -
, *
, /
.
The macro must output the trail and the secret given the 2 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:
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.
Begin by solving the challenge with the assumption that the value of nextElement
is always an integer. Then solve it for expressions that can contain one of the operators, for example 2+3
.
To begin with, we will assume that the value of nextElement
is always an integer that is equal to the ID of the next element. So all we need to do is start with the element with an ID 3
and loop over each subsequent element until we reach an element with a nextElement
of null
. We do this as long as the secret
has not been revealed by adding a condition to the for
loop.
Since twig does not allow us to break out of a loop however, how do we know how many times we need to iterate over the loop? Well, we know that the trail will end at some stage and that it cannot be longer than the total number of elements in the set, so we will use elements|length
as the limit.
For each iteration, we output the current startId
and set the new startId
to the value of the current element’s nextElement
. If it is null
then we set secret
to the title of the element which will prevent the loop from being entered again. After the for
loop we simply output the secret.
{% macro revealSecret(startId, elements) %}
{% set secret = '' %}
{% for i in 1..elements|length if not secret %}
{{ startId }} >
{% if elements[startId].nextElement is null %}
{% set secret = elements[startId].title %}
{% else %}
{% set startId = elements[startId].nextElement %}
{% endif %}
{% endfor %}
{{ secret }}
{% endmacro %}
We could condense this down to less code if we wanted to (though perhaps less readable).
{% macro revealSecret(startId, elements) %}
{% for i in 1..elements|length if elements[startId].nextElement is not null %}
{{ startId }} >
{% set startId = elements[startId].nextElement %}
{% endfor %}
{{ startId }} > {{ elements[startId].title }}
{% endmacro %}
Since our macro consists almost entirely of a loop, using recursion could be a more elegant solution to this problem. Recursion is an approach in which a function (or macro in this case) solves one step of the problem and then calls itself to solve the next step.
To illustrate further, we use an iterative approach above to loop n
times (the length of the elements) over the code, regardless of when the solution is found. This could be seen as wasteful since the trail to the solution could be a single iteration and the for loop would nevertheless continue iterating until n
is reached.
A recursive approach would run only until the solution is revealed and would then immediately stop by simply not calling itself any more.
Note that in order for a macro to call itself, it must first import itself.
{% macro revealSecret(startId, elements) %}
{{ startId }} >
{% if elements[startId].nextElement is null %}
{{ elements[startId].title }}
{% else %}
{% from _self import revealSecret %}
{{ revealSecret(elements[startId].nextElement, elements) }}
{% endif %}
{% endmacro %}
The next challenge is to determine the value of nextElement
if it is a mathematical expression. There is no obvious function in twig to evaluate a string (although there is a workaround), but Craft gives us a few possibilities.
In Challenge #3’s solution, we made it possible to define an email notification template which we rendered using a variation of the following PHP code:
Craft::$app->getView()->renderTemplate($templateName, $variables);
The renderTemplate method allowed us to pass in the name of a template to render as well as some variables. There is also a renderString method which will parse a template as a string for us. Since the view
is available as a global variable to us in twig templates, we can make it work for our mathematical expression as follows:
{% set startId = view.renderString("{{" ~ elements[startId].nextElement ~ "}}") %}
We could also use string interpolation to evaluate the string and can also make it work using some of Craft’s other render methods on the View class.
{% set startId = view. renderObjectTemplate("{{ #{elements[startId].nextElement} }}", {}) %}
Similar solutions: Andrew Welch.
Craft’s View
class extends Yii’s View
class, which contains a method called evaluateDynamicContent that will evaluate PHP statements (beware!), so the following also works:
{% set startId = view.evaluateDynamicContent("return " ~ elements[id].nextElement ~ ";") %}
Similar solutions: Spenser Hannon.
Even twig provides a template_from_string function which can be used. This requires that the Twig_Extension_StringLoader
extension is added, which it is by default in Craft.
{% set startId = include(template_from_string("{{" ~ elements[startId].nextElement ~ "}}")) %}
Similar solutions: Rias Van der Veken, Berlin Craft Meetup, Christian Seelbach.
We could of course also evaluate the string by parsing it using twig alone. There are various ways we could do this, but for simplicity’s sake we will show a series of if/else
statements. Here we assume that one and only one operator can be used.
{% set nextElement = elements[startId].nextElement %}
{% if '+' in nextElement %}
{% set numbers = nextElement|split('+') %}
{% set startId = numbers[0] + numbers[1] %}
{% elseif '-' in nextElement %}
{% set numbers = nextElement|split('-') %}
{% set startId = numbers[0] - numbers[1] %}
{% elseif '*' in nextElement %}
{% set numbers = nextElement|split('*') %}
{% set startId = numbers[0] * numbers[1] %}
{% elseif '/' in nextElement %}
{% set numbers = nextElement|split('/') %}
{% set startId = numbers[0] / numbers[1] %}
{% endif %}
Similar solutions: Paul Verheul, Matt Stein, Cole Henley, John Wells, Alex Roper, Mark Smits.
Or if none of the above solutions take your fancy then you could follow Patrick Harrington’s example and outsource the problem by using Craft’s API service to call an external API to solve it for you so you can get on with the rest of your life!!
Solution submitted by Andrew Welch on 4 December 2018.
{#
# Challenge #4 – Elementary, my dear Watson
# https://craftcodingchallenge.com/challenge-4-elementary-my-dear-watson
# @author Andrew Welch, nystudio107
#}
{#
# Iterate through the passed in elements, starting at startId, outputting
# the id number of the element, and then recursively call the macro until
# an element is reach that has no nextElement.
# @param int startId
# @param array elements
#}
{%- macro revealSecret(startId, elements) -%}
{# Make sure we were passed in a value starting point #}
{%- set element = elements[startId] ?? null -%}
{%- if element | length -%}
{{- "#{startId} > " }}
{# If element.nextElement is null, we're done. Output the title and exit #}
{%- if element.nextElement == null -%}
{{- element.title }}
{%- else -%}
{# Let twig evaluate the expression for us #}
{%- set startId = renderObjectTemplate("{{ #{element.nextElement} }}", {}) -%}
{# Call ourselves recursively to continue iterating through the elements #}
{%- from _self import revealSecret -%}
{{- revealSecret(startId, elements) }}
{%- endif -%}
{% else %}
{{ "Error, startId #{startId} not found in elements" | t }}
{% endif %}
{%- endmacro -%}
{% from _self import revealSecret %}
{{ revealSecret(3, elements) }}
Solution submitted by Paul Verheul on 4 December 2018.
{#
# This recursive macro finds the secret and creates a papertrail.
#}
{% macro revealSecret(elements, startId = null, currentElementId = null, paperTrail = []) %}
{# For recursive purposes #}
{% import _self as macro %}
{#
# If the start Id is not given, we get it from Craft's version (https://docs.craftcms.com/api/v3/craft-models-info.html#public-properties)
#}
{% if startId is empty %}
{% set startId = craft.app.info.version |split('.') |first %}
{% endif %}
{#
# If no current key is given (only on 'entry' into the macro), we use the start id
#}
{% set currentKey = currentElementId ? currentElementId : startId %}
{#
# 'Loop' through our element with current key
#}
{% for id, element in elements if id == currentKey %}
{% if element.nextElement is null %}
{#
# No next element? We must've found the holy grail! Complete our paper trail and display it.
#}
{% set paperTrail = paperTrail |merge([id, element.title]) %}
{{ paperTrail |join(' > ') }}
{% else %}
{#
# Update our papertrail with the current id.
#}
{% set paperTrail = paperTrail |merge([id]) %}
{#
# I wish I'd found a better/nicer way to do some Twig expression calculation.
# Based on the assigment ('only one operator type per expression'), I assumed it's not possible
#}
{% set rawNextElement = element.nextElement %}
{% if '+' in rawNextElement %}
{% set rawNextElementParts = rawNextElement |split('+') %}
{% set nextElementId = rawNextElementParts |first %}
{% for rawNextElementPart in rawNextElementParts |slice(1, rawNextElementParts |length - 1) %}
{% set nextElementId = nextElementId + rawNextElementPart %}
{% endfor %}
{% elseif '-' in rawNextElement %}
{% set rawNextElementParts = rawNextElement |split('-') %}
{% set nextElementId = rawNextElementParts |first %}
{% for rawNextElementPart in rawNextElementParts |slice(1, rawNextElementParts |length - 1) %}
{% set nextElementId = nextElementId - rawNextElementPart %}
{% endfor %}
{% elseif '*' in rawNextElement %}
{% set rawNextElementParts = rawNextElement |split('*') %}
{% set nextElementId = rawNextElementParts |first %}
{% for rawNextElementPart in rawNextElementParts |slice(1, rawNextElementParts |length - 1) %}
{% set nextElementId = nextElementId * rawNextElementPart %}
{% endfor %}
{% elseif '/' in rawNextElement %}
{% set rawNextElementParts = rawNextElement |split('/') %}
{% set nextElementId = rawNextElementParts |first %}
{% for rawNextElementPart in rawNextElementParts |slice(1, rawNextElementParts |length - 1) %}
{% set nextElementId = nextElementId / rawNextElementPart %}
{% endfor %}
{% else %}
{% set nextElementId = rawNextElement %}
{% endif %}
{#
# Check if our search produced an integer ...
#}
{% if nextElementId is not empty and nextElementId matches '/^\\d+$/' %}
{#
# ... and recursively continue our quest.
#}
{{ macro.revealSecret(elements, startId, nextElementId, paperTrail) }}
{% else %}
{#
# ... or end our quest, knowing that we've done everything in our powers!
#}
{% set paperTrail = paperTrail |merge(['No next element found for (expression) ' ~ rawNextElement]) %}
{{ paperTrail |join(' > ') }}
{% endif %}
{% endif %}
{% else %}
{% set paperTrail = paperTrail |merge(['Element with ID ' ~ currentElementId ~ ' not found']) %}
{{ paperTrail |join(' > ') }}
{% endfor %}
{% endmacro %}
Solution submitted by Rias Van der Veken on 4 December 2018.
{% macro revealSecret(startId, elements) %}
{% import _self as self %}
{{ startId }} >
{% if elements[startId].nextElement is not null %}
{% set nextId = include(template_from_string('{{' ~ elements[startId].nextElement ~ '}}')) %}
{{ self.revealSecret(nextId, elements) }}
{% else %}
{{ elements[startId].title }}
{% endif %}
{% endmacro %}
{% from _self import revealSecret %}
{% set elements = {
1: {nextElement: "1+2", title: "I used to think I was indecisive, but now I'm not too sure."},
2: {nextElement: 2, title: "Doing nothing is hard, you never know when you're done."},
3: {nextElement: "2+3", title: "If two wrongs don't make a right, try three."},
4: {nextElement: "4/4", title: "I am not lazy, I am on energy saving mode."},
5: {nextElement: "21/3", title: "Life is short, smile while you still have teeth."},
6: {nextElement: null, title: "Sorry for the mean, awful, accurate things I said."},
7: {nextElement: "3*3", title: "People say nothing is impossible, but I do nothing every day."},
8: {nextElement: "12/2", title: "I’m sorry but if you were right, I’d agree with you."},
9: {nextElement: null, title: "The answer to the ultimate question of life is 42."},
} %}
{% set startId = 3 %}
{{ revealSecret(startId, elements) }}
Solution submitted by Matt Stein on 4 December 2018.
{% macro revealSecret(startId, elements) %}
{% set reachedSecret = false %}
{% set currentId = startId %}
{% set maxHops = (elements | length) ** 2 %}
{% for i in 0..maxHops if reachedSecret == false %}
{% set nextId = elements[currentId].nextElement %}
{% if nextId matches '~[+-/*//]~' %}
{# remove digits to isolate the operator #}
{% set operator = nextId | replace('/[0-9]/', '') %}
{# break at the operator to get the numbers #}
{% set numbers = nextId | split(operator) %}
{# do the arithmetic with a switch like some kind of animal #}
{% switch operator %}
{% case "+" %}
{% set nextId = numbers[0] + numbers[1] %}
{% case "-" %}
{% set nextId = numbers[0] - numbers[1] %}
{% case "*" %}
{% set nextId = numbers[0] * numbers[1] %}
{% case "/" %}
{% set nextId = numbers[0] / numbers[1] %}
{% endswitch %}
{% endif %}
{{ currentId ~ " > " }}
{% if nextId is null %}
{# reveal secret to Watson and audience #}
{{ elements[currentId].title }}
{% set reachedSecret = true %}
{% endif %}
{# get ready for the next loop #}
{% set currentId = nextId %}
{% endfor %}
{% endmacro %}
Solution submitted by Cole Henley on 4 December 2018.
{% macro revealSecret(startId, elements) %}
{% set outputInt = startId %}
{% set outputStr = startId %}
{% set end = false %}
{% for i in 0..elements|length if end == false %}
{% set el = elements[outputInt] %}
{% if '+' in el.nextElement %}
{% set arr = el.nextElement|split('+') %}
{% set output = arr[0] + arr[1] %}
{% elseif '*' in el.nextElement %}
{% set arr = el.nextElement|split('*') %}
{% set output = arr[0] * arr[1] %}
{% elseif '/' in el.nextElement %}
{% set arr = el.nextElement|split('/') %}
{% set output = arr[0] / arr[1] %}
{% elseif '-' in el.nextElement %}
{% set arr = el.nextElement|split('-') %}
{% set output = arr[0] - arr[1] %}
{% else %}
{% set output = el.nextElement %}
{% endif %}
{% if el.nextElement is null %}
{% set end = true %}
{% set outputStr = outputStr ~ ' > ' ~ el.title %}
{% else %}
{% set outputInt = output %}
{% set outputStr = outputStr ~ ' > ' ~ output %}
{% endif %}
{% endfor %}
{{ outputStr }}
{% endmacro %}
Solution submitted by Patrick Harrington on 4 December 2018.
{% macro revealSecret(startId, elements) %}{% spaceless %}
{% import _self as macros %}
{{- "#{startId} > " | raw -}}
{% if elements[startId].nextElement %}
{%- set nextElement = elements[startId].nextElement|url_encode %}
{%- set nextId = craft.app.api.client.get("http://api.mathjs.org/v4/?expr=#{nextElement}").getBody().__toString() %}
{{- macros.revealSecret(nextId, elements) }}
{% else %}
{{- elements[startId].title }}
{% endif %}
{% endspaceless %}{% endmacro %}
Solution submitted by John Wells on 4 December 2018.
{% macro add(string) %}
{% set n = string|split('+') %}
{% if n|length == 2 %}
{{ n[0] + n[1] }}
{% endif %}
{% endmacro %}
{% macro subtract(string) %}
{% set n = string|split('-') %}
{% if n|length == 2 %}
{{ n[0] - n[1] }}
{% endif %}
{% endmacro %}
{% macro multiply(string) %}
{% set n = string|split('*') %}
{% if n|length == 2 %}
{{ n[0] * n[1] }}
{% endif %}
{% endmacro %}
{% macro divide(string) %}
{% set n = string|split('/') %}
{% if n|length == 2 %}
{{ n[0] / n[1] }}
{% endif %}
{% endmacro %}
{% macro eval(string) %}
{% from _self import add, subtract, multiply, divide %}
{% set result %}
{{ add(string) }}
{{ subtract(string) }}
{{ multiply(string) }}
{{ divide(string) }}
{% endset %}
{{ result|trim ? result : string }}
{% endmacro %}
{% macro revealSecret(startId, elements) %}
{% from _self import eval %}
{% set nextElementIds, secret = [startId], null %}
{% for i in 0..elements|length if secret is null %}
{% for id, element in elements if id == nextElementIds|last %}
{% if element.nextElement is null %}
{% set secret = element.title %}
{% else %}
{% set nextElementIds = nextElementIds|merge([
eval(element.nextElement)|trim
]) %}
{% endif %}
{% endfor %}
{% endfor %}
{{ nextElementIds|merge([secret])|join(' > ')}}
{% endmacro %}
{% from _self import revealSecret %}
{% set elements = {
1: {nextElement: "1+2", title: "I used to think I was indecisive, but now I'm not too sure."},
2: {nextElement: 2, title: "Doing nothing is hard, you never know when you're done."},
3: {nextElement: "2+3", title: "If two wrongs don't make a right, try three."},
4: {nextElement: "4/4", title: "I am not lazy, I am on energy saving mode."},
5: {nextElement: "21/3", title: "Life is short, smile while you still have teeth."},
6: {nextElement: null, title: "Sorry for the mean, awful, accurate things I said."},
7: {nextElement: "3*3", title: "People say nothing is impossible, but I do nothing every day."},
8: {nextElement: "12/2", title: "I’m sorry but if you were right, I’d agree with you."},
9: {nextElement: null, title: "The answer to the ultimate question of life is 42."},
} %}
{% set startId = 3 %}
{{ revealSecret(startId, elements) }}
Solution submitted by Berlin Craft Meetup on 5 December 2018.
{% macro revealSecret(startId, elements) %}
{# Import self, because we're about to get recursive! #}
{% import _self as macros %}
{# Escape our output, since ">" is a tricky character in HTML. #}
{% autoescape %}
{# Evaluate our input ID as a potentially being a string with an equation in it. #}
{% set evaluateId = include(template_from_string("{{#{startId}}}")) %}
{# Grab out element from our input set #}
{% set element = elements[evaluateId] %}
{# Just in case we don't find it, we should probably bail. #}
{% if element %}
{#
First, output the current ID we're working with, regardless of what we're about to find.
If element.nextElement is something, then start over again by passing it back to the macro and keep searching.
Otherwise print out the answer we've now finally found.
#}
{{ evaluateId }} > {{ element.nextElement? macros.revealSecret(element.nextElement, elements): element.title }}
{% endif %}
{% endautoescape %}
{% endmacro %}
Solution submitted by Christian Seelbach on 5 December 2018.
{% macro revealSecret(startId, elements) %}
{%- from _self import revealSecret %}
{%- set nextId = elements[startId].nextElement %}
{%- if nextId is not same as(null) %}
{{- "#{startId} > " }}
{%- set nextId = include(template_from_string("{{ #{nextId} }}")) %}
{{- revealSecret(nextId, elements) }}
{%- else %}
{{- "#{startId}: #{elements[startId].title}" }}
{%- endif -%}
{% endmacro %}
Solution submitted by Spenser Hannon on 5 December 2018.
{% macro revealSecret(id, elements) %}
{% for n in 0..(elements|length) if elements[id].nextElement %}
{{ id }} >
{% set id = view.evaluateDynamicContent("return #{elements[id].nextElement};") %}
{% endfor %}
{{ id }} >{{ elements[id].title }}
{% endmacro %}
Solution submitted by Alex Roper on 6 December 2018.
{% macro revealSecret(startId, elements) %}
{% import _self as macros %}
{# Evaluate simple math.
# How is there not a native way to evaluate an expression inside a string?!
-#}
{% if '+' in startId %}
{% set nums = startId|split('+') %}
{% set startId = nums[0] + nums[1] %}
{% elseif '-' in startId %}
{% set nums = startId|split('-') %}
{% set startId = nums[0] - nums[1] %}
{% elseif '*' in startId %}
{% set nums = startId|split('*') %}
{% set startId = nums[0] * nums[1] %}
{% elseif '/' in startId %}
{% set nums = startId|split('/') %}
{% set startId = nums[0] / nums[1] %}
{% endif %}
{% set currentElement = elements[startId] %}
{% if currentElement.nextElement == null %}
{{ currentElement.title }}
{% else %}
{{ macros.revealSecret(currentElement.nextElement, elements) }}
{% endif %}
{% endmacro %}
Solution submitted by Mark Smits on 7 December 2018.
{% macro revealSecret(startId, elements) %}
{% set next = startId %}
{% set secretFound = false %}
{% set output = [next] %}
{% for i in 1..elements|length if secretFound == false %}
{% set currentElement = elements[next] %}
{% set next = elements[next].nextElement %}
{% if not next %}
{% set next = currentElement.title %}
{% set secretFound = true %}
{% elseif '/' in next %}
{% set values = next|split('/') %}
{% set next = (values[0] / values[1]) %}
{% elseif '*' in next %}
{% set values = next|split('*') %}
{% set next = (values[0] * values[1]) %}
{% elseif '+' in next %}
{% set values = next|split('+') %}
{% set next = (values[0] + values[1]) %}
{% elseif '-' in next %}
{% set values = nextElement|split('-') %}
{% set next = (values[0] - values[1]) %}
{% endif %}
{% set output = output|merge([next]) %}
{% endfor %}
{{ output|join(' > ')|raw }}
{% endmacro %}