Ordinals in Python

A couple more things to say about ordinal numbers…

First, I mentioned yesterday that the strftime library doesn’t have a code for an ordinal date. But if you’re using JavaScript, and can add the Moment.js library to your project, you have access to formatting that does have codes for ordinal versions of day of month, day of year, week of year, month of year, and quarter of year. Of course, if you need ordinals in JavaScript for something other than dates, yesterday’s function will give you what Moment.js won’t.

Second, after playing around in JavaScript, I naturally wanted an ordinal function for Python. There’s a pretty clever one here from Thad Guidry (which I’ve renamed):

1:  def ordinaltg(n):
2:    return str(n) + {1: 'st', 2: 'nd', 3: 'rd'}.get(4 if 10 <= n % 100 < 20 else n % 10, "th")

The clever parts here are

  1. The use of a dictionary for the suffixes and get to provide a default value when the key isn’t present in the dictionary.
  2. The ternary expression

    4 if 10 <= n % 100 < 20 else n % 10

    to return a key that’s in the dictionary for the oddball suffixes and isn’t in the dictionary when we want “th.” (Note that the 4 at the beginning of the expression could be any number other than 1, 2, or 3, and the function would work just as well.)

Somehow, though, the cleverness here struck me as overdone. There’s something false about using a dictionary with integer keys. I know it’s legal, but indexing by integer is more naturally done with lists and tuples. And although speed is unlikely to be an issue in a function like this, it seemed especially wasteful to me to have two explicit comparisons in the ternary expression and then a third one implicitly to handle the default condition of the get.

I decided to be a little less clever and use more space with fewer computations:

1:  def ordinal(n):
2:    s = ('th', 'st', 'nd', 'rd') + ('th',)*10
3:    v = n%100
4:    if v > 13:
5:      return f'{n}{s[v%10]}'
6:    else:
7:      return f'{n}{s[v]}'

The extra space is the tuple with 14 items, indexed 0 through 13. If v—which is just n stripped down to the tens and one places—is bigger than 13, we get its last digit via v%10 and get the suffix from one of the first ten items in s. If v is 13 or less, we use v itself as the index to get the suffix.

Now there’s just one comparison, and retrieving a value from a tuple is pretty fast. We do have to spend time (and space) building the tuple, but my timing tests show that the tradeoff is worth it. Depending on the computer and the version of Python,1 ordinal is between 1.5 and 2.0 times the speed of ordinaltg. And I think it’s easier to read and understand.

As written, ordinal is limited to Python 3.6 or later because of the f-strings in Lines 5 an 7. If you’re stuck with a pre-3.6 Python, you’ll have to use some older and lesser technique, like

5:      return '{}{}'.format(n,s[v%10])


5:      return '%d%s' % (n,s[v%10])

or even

5:      return str(n) + s[v%10]

none of which are as fast as f-strings.

I promised myself that this wouldn’t turn into a multiplicity of posts like my ROT13 odyssey of this past winter. So you won’t see me doing ordinal conversions in Perl, Ruby, Awk, Shortcuts, etc. You’re welcome.

  1. I’m using Python 3.7 on my Macs, 3.6 in Pythonista on my iPads, and 3.8 in Pyto on my iPads.