Laravel str_ordinal Helper Function

I recently submitted a PR to the Laravel framework to add a str_ordinal helper function. The function adds an ordinal indicator to a numeric value passed to its first argument. Simply put, it adds “st”, “nd”, “rd” and “th” to numbers. The PR was closed by Taylor within the same minute in which I submitted it. He was kind enough to let me know that he typically doesn’t add helpers that aren’t used by the framework itself. Fair enough. However, I have use for it and Rails thoughtfully includes it so I thought you might find it handy as well. While we’re at it, we’ll go ahead and run through the process of implementing a helpers file in Laravel as well as a unit test to make sure everything works as intended.

But PHP Already…

Yes, of course PHP has this functionality baked in.


$value = 2;
$number = new (NumberFormatter('en_US', NumberFormatter::ORDINAL))->format($value);

echo $number;
// 2nd

Unfortunately, NumberFormatter is in the php_intl module which is not installed on all hosts by default. Even if you do have it installed, you’d still want to wrap it in a helper to make working with it in views more convenient as we’ll see later.

Create the Helper File

Now, under your app directory, create a Helpers folder. Within that folder, create a file named StrOrdinal.php and include the following:

<?php

if (! function_exists('str_ordinal')) {
    /**
     * Append an ordinal indicator to a numeric value.
     *
     * @param  string|int  $value
     * @param  bool  $superscript
     * @return string
     */
    function str_ordinal($value, $superscript = false)
    {
        $number = abs($value);

        $indicators = ['th','st','nd','rd','th','th','th','th','th','th'];

        $suffix = $superscript ? '<sup>' . $indicators[$number % 10] . '</sup>' : $indicators[$number % 10];
        if ($number % 100 >= 11 && $number % 100 <= 13) {
            $suffix = $superscript ? '<sup>th</sup>' : 'th';
        }

        return number_format($number) . $suffix;
    }
}

You’ll notice the function takes a second, optional argument to determine whether you want the string returned with HTML superscript tags for the indicator. If you do want the indicator in superscript, remember that you’ll have to use {!! !!} instead of {{ }} to prevent your output from being escaped when it is passed through html_entities.

Create a Service Provider

While you could include the path(s) your helpers in your composer.json files array, it is more in keeping with the Laravel way of doing things to create a service provider. IN doing so, we can get the added benefit of adding new files without the need for additional configuration. First, let’s create the provider:

$ php artisan make:provider HelperServiceProvider

Now, navigate to your app/Providers directory, open your newly created provider and configure the register method as follows:

    public function register()
    {
        foreach (glob(app_path().'/Helpers/*.php') as $filename) {
            require_once($filename);
        }
    }

Finally, add your service provider to the providers array found in app/config.app

    'providers' => [
        /*
         * Application Service Providers...
         */
        App\Provicers\HelperServiceProvider::class,

The register method of our service provider gets an array of files from the Helpers directory then requires each file once if it is found. Therefore, when we want to add a new helper in the future we can either add it to an existing helper file or create a new, dedicated file as we did for our str_ordinal function. No further configuration is necessary.

Create a Unit Test

Sorry, this isn’t a post about TDD so we’re going to cheat and create our test after the fact. Let’s create a unit test to ensure the helper works as intended. In your tests/Unit directory create a file called HelpersTest.php and include the following test case:

<?php

namespace Tests\Unit;

use Tests\TestCase;

class HelpersTest extends TestCase
{
    /**
     * Test to ensure the proper ordinal indicator is applied to values passed
     * to the str_ordinal helper function.
     *
     * @return void
     */
    public function testStrOrdinal()
    {
        $this->assertEquals('0th', str_ordinal('foo'));
        $this->assertEquals('0th', str_ordinal(0));
        $this->assertEquals('0th', str_ordinal('0'));
        $this->assertEquals('1st', str_ordinal('1'));
        $this->assertEquals('2nd', str_ordinal('2'));
        $this->assertEquals('3rd', str_ordinal('3'));
        $this->assertEquals('4th', str_ordinal('4'));
        $this->assertEquals('11th', str_ordinal('11'));
        $this->assertEquals('112th', str_ordinal('112'));
        $this->assertEquals('1,113th', str_ordinal('1113'));
        $this->assertEquals('1,000<sup>th</sup>', str_ordinal('1000', true));
    }
}

Go ahead and run the test to ensure it passes:

$ vendor/bin/phpunit tests/Unit/HelpersTest

Calling Our Function from Blade

Finally, use it in a blade template:

@for ($i = 1; $i <= 4; $i++)
   {{ str_ordinal($i) }} <br>
@endfor

You will then have the following output:
1st
2nd
3rd
4th

If you want the indicator in superscript, pass true to the second argument remembering to output it raw (as well as any risks with doing so):

@for ($i = 1; $i <= 4; $i++)
   {!! str_ordinal($i, true) !!} <br>
@endfor

You will then have the indicator in superscript:
1st
2nd
3rd
4th

Revisiting NumberFormatter

I noted above that you could wrap NumberFormatter in a helper for more convenient use. In fact you could do two things. First you could check to see if NumberFormatter exists and use it or fall back to the function our original code provides. Let’s see what that might look like:

<?php

if (!function_exists('str_ordinal')) {
    /**
     * Append an ordinal indicator to a numeric value.
     *
     * @param  string|int $value
     * @param  bool $superscript
     * @return string
     */
    function str_ordinal($value, $superscript = false)
    {
        $number = abs($value);

        if (class_exists('NumberFormatter')) {
            $nf = new \NumberFormatter('en_US', \NumberFormatter::ORDINAL);
            $ordinalized = $superscript ?
                number_format($number) .
                '<sup>' .
                substr($nf->format($number), -2) .
                '</sup>' :
                $nf->format($number);

            return $ordinalized;
        }


        $indicators = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th'];

        $suffix = $superscript ? '<sup>' . $indicators[$number % 10] . '</sup>' : $indicators[$number % 10];
        if ($number % 100 >= 11 && $number % 100 <= 13) {
            $suffix = $superscript ? '<sup>th</sup>' : 'th';
        }

        return number_format($number) . $suffix;
    }
}

The only practical reason for doing it like this is to ensure it works on hosts whether or not php_intl is installed. It also serves to illustrate how you might wrap NumberFormatter for more convenient use as we discussed earlier. Further, if you have php_intl installed, you can strip this down and to use only NumberFormatter. It’s up to you whether you want the option of having the indicator in superscript, but the benefit of going with NumberFormatter alone is having the ability to pass in the locale of your choosing.

Drawbacks

My implementation has two minor drawbacks. First, since we always get the absolute value of the value passed to the function, we don’t respect (or return) negatives. NumberFormatter does preserve the sign. Personally, I can’t even think of a use for -1st place so I think that’s forgivable. Second, it only addresses ordinals in English. Again, if you have php_intl, just wrap NumberFormatter, take locale as an argument (or hard code it) and be on your merry way.

Wrapping Up

I hope you not only find the str_ordinal function useful, but more broadly the practice of implementing a helpers file and testing it as well. Since the framework likely won’t be adding useful helpers like this anytime soon, I’d love to see some other helpers you use in your own projects. Post or link to them in the comments below!

Leave a Comment

Your email address will not be published. Required fields are marked *