Using the Spaceship Operator in Twig

This three-way comparison operator has the nickname spaceship operator. It is available as of PHP 7.

Image

In ver­sion 2.12 of Twig, the devel­op­ers added a space­ship oper­a­tor. Craft added sup­port for Twig 2.12 in Craft CMS 3.3.8.

The Space­ship Oper­a­tor doesn’t allow you to send an array to Mars, sad­ly, but it does allow you to make mul­ti­ple com­par­isons at the same time. And by mul­ti­ple I mean three.

It kind of looks like a space­ship, I guess:

<==>

Or maybe a fly­ing saucer?

This is also called a three-way com­par­i­son oper­a­tor. It has the nick­name space­ship oper­a­tor in the PHP doc­u­men­ta­tion and it’s of the com­par­i­son oper­a­tor type. It is avail­able as of PHP 7.

The result of the com­par­i­son isn’t a boolean; instead, it’s an inte­ger of 0, 1, or ‑1 depend­ing on how the com­par­i­son evaluates. 

Let’s look at a sim­ple exam­ple by com­par­ing two integers:

23 <=> 23
20 <=> 20

What hap­pens when this is run by Twig or PHP — or in any one of the sev­er­al lan­guages that sup­port three-way com­par­i­son oper­a­tors — is that it first 

This does three checks:

  1. Is the operand on the right greater than the operand on the left? If so, it returns ‑1.
  2. Is the operand on left is greater than that on the right? If so, it returns 1.
  3. Are the operands equal? If so, it returns a 0.

We can do this in Twig by using the out­put tag and a space­ship comparison. 

{{ 23 <=> 23 }}
{# returns 0 #}

{{ 23 <=> 6 }}
{# returns 1 #}

{{ 23 <=> 50 }}
{# returns -1 #}

Using the Space­ship Oper­a­tor for Sorting

That’s a nice the­o­ret­i­cal exer­cise but how do we apply this in a real usage scenario?

A typ­i­cal exam­ple of using the space­ship oper­a­tor is for sort­ing. In Twig we have the sort fil­ter, which uses PHP’s asort func­tion for sort­ing arrays. 

asort is based on Quick­Sort, which is a divide and con­quers sort­ing algo­rithm. It splits the array at a piv­ot point and then swaps array ele­ments based on val­ue comparisons.

Here’s the data we’ll use for this exam­ple. It’s an array of run­ners and their marathon time in seconds. 

{%  set runners = [
    { name: 'Wilson Kipsang', time: 7403 },
    { name: 'Eliud Kipchoge', time: 7299 },
    { name: 'Patrick Makau', time: 7418 },
    { name: 'Dennis Kimetto', time: 7377 },
    { name: 'Haile Gebrselassie', time: 7439 },
] %}

Let’s loop through them to get a list of runners:

{% for runner in runners %}
    <li>{{ runner.name  }} - {{ runner.time }}</li>
{% endfor %}

But if I want to order them by time (stored in sec­onds), how would I do that? We use the arrow func­tion (a short­hand way of cre­at­ing an anony­mous func­tion) in Twig and the sort fil­ter and the space­ship operator.

{% for runner in runners | sort((a,b) => a.time <=> b.time) | column('name') %}
    <li>{{ runner }}</li>
{% endfor %}

This out­puts the run­ners sort­ed by time, small­est to biggest. When the num­bers are the same the algo­rithm leaves the orig­i­nal order in place.

Eliud Kipchoge
Dennis Kimetto
Wilson Kipsang
Patrick Makau
Ryan Irelan
Meb Keflezki
Haile Gebrselassie

In the for-loop, we pass the run­ners through the sort fil­ter using an arrow func­tion. We tell sort to com­pare two times at once (a and b) and use the space­ship com­par­i­son to sort them by time. If any of the times are the same then it doesn’t do any­thing but does sort the oth­ers based on time. 

If we didn’t use the space­ship oper­a­tor and instead used >= then we’d get the same results for every­thing except those that have the same time value.

{% for runner in runners | sort((a,b) => a.time >= b.time) | column('name') %}
    <li>{{ runner }}</li>
{% endfor %}

Those items with the same time val­ue would sort by the val­ue of their name. You can see that in the output.

Eliud Kipchoge
Dennis Kimetto
Wilson Kipsang
Patrick Makau
Haile Gebrselassie
Meb Keflezki
Ryan Irelan

When we change it back to the space­ship oper­a­tor than we get the array order of the items with the val­ue time val­ue left intact.

Eliud Kipchoge
Dennis Kimetto
Wilson Kipsang
Patrick Makau
Ryan Irelan
Meb Keflezki
Haile Gebrselassie