Ordinal numbers replacement
Asked Answered
P

16

93

I am currently looking for the way to replace words like first, second, third,...with appropriate ordinal number representation (1st, 2nd, 3rd). I have been googling for the last week and I didn't find any useful standard tool or any function from NLTK.

So is there any or should I write some regular expressions manually?

Thanks for any advice

Prado answered 10/3, 2012 at 14:27 Comment(3)
If you can't find one it shouldn't be too hard to roll your own, because the number format is very strict. Something like pyparsing would make it easier, too!Smelly
wow, most answers don’t answer the question ('first''1st') but another problem (11st)Illnatured
@törzsmókus aside from that, there is a lot of duplication here.Kipkipling
S
161

The package number-parser can parse ordinal words ("first", "second", etc) to integers.

from number_parser import parse_ordinal
n = parse_ordinal("first")

To convert an integer to "1st", "2nd", etc, you can use the following:

def ordinal(n: int):
    if 11 <= (n % 100) <= 13:
        suffix = 'th'
    else:
        suffix = ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]
    return str(n) + suffix

Here is a more terse but less readable version (taken from Gareth on codegolf):

ordinal = lambda n: "%d%s" % (n,"tsnrhtdd"[(n//10%10!=1)*(n%10<4)*n%10::4])

This works on any number:

print([ordinal(n) for n in range(1,32)])

['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th',
 '11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th',
 '20th', '21st', '22nd', '23rd', '24th', '25th', '26th', '27th', '28th',
 '29th', '30th', '31st']
Score answered 15/11, 2013 at 18:7 Comment(11)
This seems to no longer work in python3.4 e.g. ordinal(13) = '13rd'. I'm not sure why. str(n) + {1: 'st', 2: 'nd', 3: 'rd'}.get(4 if 10 <= n % 100 < 20 else n % 10, "th") works.Joanniejoao
@BrettDiDonato n/10 requires / to be integer division, which changed between Python 2 and 3Diabetes
cute, but c'mon that's just uglyBibliomancy
you can use // for integer division in python3: ordinal = lambda n: "%d%s" % (n,"tsnrhtdd"[(n//10%10!=1)*(n%10<4)*n%10::4])Thirteenth
I am fighting the overwhelming urge to use this.Conscientious
I'm on Python 3.6 and the original solution (without math.floor) works. Is that version still necessary?Fortuitism
Using f-strings: ordinal = lambda n: f'{n}{"tsnrhtdd"[(n//10%10!=1)*(n%10<4)*n%10::4]}'Playground
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
I don't get why this was marked as an answer. The problem is 'first' -> '1st' not 1 -> '1st'Humboldt
@törzsmókus: I've updated the answer to address OP's question.Score
No codebase is complete without the tsnrhtddBulger
L
57

If you don't want to pull in an additional dependency on an external library (as suggested by luckydonald) but also don't want the future maintainer of the code to haunt you down and kill you (because you used golfed code in production) then here's a short-but-maintainable variant:

def make_ordinal(n):
    '''
    Convert an integer into its ordinal representation::

        make_ordinal(0)   => '0th'
        make_ordinal(3)   => '3rd'
        make_ordinal(122) => '122nd'
        make_ordinal(213) => '213th'
    '''
    n = int(n)
    if 11 <= (n % 100) <= 13:
        suffix = 'th'
    else:
        suffix = ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]
    return str(n) + suffix
Lait answered 22/6, 2018 at 16:58 Comment(7)
this is brilliant :)Moneymaker
> Perfect added the oneliner lambda function lambda n: "".join([str(n), ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)] if not 11 <= n <= 13 else "th"]) e.g. dt_tr_fn = lambda n: "".join([str(n), ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)] if not 11 <= n <= 13 else "th"]) [dt for dt in map(dt_tr_fn, range(1,32))] @ye-lin-aungKarlie
@Karlie I think your code should read 11 <= (n % 100) <= 13 instead of just n, otherwise it will fail for, e.g. 112.Lait
@FlorianBrucker My answer is specific to dates which can be between 1 to 31, i wrote it for something that works for 1 to 31. Django a python web framework humanize app has better solutions. Yes you are correct above code will fail for 112 and 1012 and so on...Karlie
Why not move the first suffix assignment to an else block so it isn't evaluated unnecessarily?Chara
@TaylorVance: Good idea, updated the code.Lait
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
U
20

How about this:

suf = lambda n: "%d%s"%(n,{1:"st",2:"nd",3:"rd"}.get(n%100 if (n%100)<20 else n%10,"th"))
print [suf(n) for n in xrange(1,32)]

['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th',
 '11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th',
 '20th', '21st', '22nd', '23rd', '24th', '25th', '26th', '27th', '28th',
 '29th', '30th', '31st']
Uniformize answered 2/5, 2016 at 7:51 Comment(4)
I like this one, way more readable. But does it work for n > 100?Thirteenth
@Thirteenth I think adding a n%100 < 20 is sufficient, right? "%d%s"%(n,{1:"st",2:"nd",3:"rd"}.get(n if (n % 100)<20 else n%10,"th"))Illogic
Works with a small correction: suf = lambda n: "%d%s"%(n,{1:"st",2:"nd",3:"rd"}.get(n%100 if n%100<20 else n%10,"th"))Khorma
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
S
14

Another solution to format numbers to 1th, 2nd, 3rd, ... is the num2words library (pip | github). It especially offers different languages, so localization/internationalization (aka. l10n/i18n) is a no-brainer.

Usage is easy after you installed it with pip install num2words:

from num2words import num2words
# english is default
num2words(4458, to="ordinal_num")
'4458th'

# examples for other languages
num2words(4458, lang="en", to="ordinal_num")
'4458th'

num2words(4458, lang="es", to="ordinal_num")
'4458º'

num2words(4458, lang="de", to="ordinal_num")
'4458.'

num2words(4458, lang="id", to="ordinal_num")
'ke-4458'

Bonus:

num2words(4458, lang="en", to="ordinal")
'four thousand, four hundred and fifty-eighth'

If you need to parse the words "first", "second", "third", ... to numbers 1, 2, 3 first (as asked in the question, too), you can use the number-parser library (pip | github) to do that:

from number_parser import parse_ordinal
parse_ordinal("twenty third")
23

Note that it only supports English, Hindi, Spanish, Ukrainian and Russian at the time of writing this answer.

Scissors answered 4/1, 2018 at 22:5 Comment(4)
Why does it show '4458rd' in the first example? Shouldn't it be '4458th'?Romany
@Romany (username checks out) you're right. That's the output it gives, too. I have no idea how it ended up as 4458rd in my answer..Scissors
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
While that's probably not the usecase of most people coming from google.com – in fact it took 4 years for someone to notice – I also included that part now.Scissors
D
10

The accepted answer to a previous question has an algorithm for half of this: it turns "first" into 1. To go from there to "1st", do something like:

suffixes = ["th", "st", "nd", "rd", ] + ["th"] * 16
suffixed_num = str(num) + suffixes[num % 100]

This only works for numbers 0-19.

Deltoid answered 10/3, 2012 at 14:48 Comment(3)
so if I am right then I need to write all the values to the dict like {'first':'1', 'second':'2', 'third':'3', 'fourth':'4',...} anyway?Prado
@Prado yes. There is, in general, no way to do this without having a dict like that. But with enough Googling, you will probably find someone has already done the menial labour bit for you. Otherwise, if you do end up having to do it yourself, you could avoid the suffixes list by setting up your dict as {'first': '1st'}, etc.Deltoid
one of the very few correct answers! most others don’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
D
9

I found myself doing something similar, needing to convert addresses with ordinal numbers ('Third St') to a format that a geocoder could comprehend ('3rd St'). While this isn't very elegant, one quick and dirty solution is to use the inflect.py to generate a dictionary for translation.

inflect.py has a number_to_words() function, that will turn a number (e.g. 2) to its word form (e.g. 'two'). Additionally, there is an ordinal() function that will take any number (numeral or word form) and turn it into its ordinal form (e.g. 4 -> fourth, six -> sixth). Neither of those, on their own, do what you're looking for, but together you can use them to generate a dictionary to translate any supplied ordinal-number-word (within a reasonable range) to its respective numeral ordinal. Take a look:

>>> import inflect
>>> p = inflect.engine()
>>> word_to_number_mapping = {}
>>>
>>> for i in range(1, 100):
...     word_form = p.number_to_words(i)  # 1 -> 'one'
...     ordinal_word = p.ordinal(word_form)  # 'one' -> 'first'
...     ordinal_number = p.ordinal(i)  # 1 -> '1st'
...     word_to_number_mapping[ordinal_word] = ordinal_number  # 'first': '1st'
...
>>> print word_to_number_mapping['sixth']
6th
>>> print word_to_number_mapping['eleventh']
11th
>>> print word_to_number_mapping['forty-third']
43rd

If you're willing to commit some time, it might be possible to examine inflect.py's inner-workings in both of those functions and build your own code to do this dynamically (I haven't tried to do this).

Donar answered 10/2, 2014 at 4:23 Comment(1)
one of the very few correct answers! most others don’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
J
6

I wanted to use ordinals for a project of mine and after a few prototypes I think this method although not small will work for any positive integer, yes any integer.

It works by determiniting if the number is above or below 20, if the number is below 20 it will turn the int 1 into the string 1st , 2 , 2nd; 3, 3rd; and the rest will have "st" added to it.

For numbers over 20 it will take the last and second to last digits, which I have called the tens and unit respectively and test them to see what to add to the number.

This is in python by the way, so I'm not sure if other languages will be able to find the last or second to last digit on a string if they do it should translate pretty easily.

def o(numb):
    if numb < 20: #determining suffix for < 20
        if numb == 1: 
            suffix = 'st'
        elif numb == 2:
            suffix = 'nd'
        elif numb == 3:
            suffix = 'rd'
        else:
            suffix = 'th'  
    else:   #determining suffix for > 20
        tens = str(numb)
        tens = tens[-2]
        unit = str(numb)
        unit = unit[-1]
        if tens == "1":
           suffix = "th"
        else:
            if unit == "1": 
                suffix = 'st'
            elif unit == "2":
                suffix = 'nd'
            elif unit == "3":
                suffix = 'rd'
            else:
                suffix = 'th'
    return str(numb)+ suffix

I called the function "o" for ease of use and can be called by importing the file name which I called "ordinal" by import ordinal then ordinal.o(number).

Let me know what you think :D

Jeremy answered 7/9, 2013 at 6:58 Comment(0)
R
5

If using django, you could do:

from django.contrib.humanize.templatetags.humanize import ordinal
var = ordinal(number)

(or use ordinal in a django template as the template filter it was intended to be, though calling it like this from python code works as well)

If not using django you could steal their implementation which is very neat.

Ratable answered 21/6, 2017 at 17:2 Comment(1)
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
H
4

There's an ordinal function in humanize

pip install humanize

>>> [(x, humanize.ordinal(x)) for x in (1, 2, 3, 4, 20, 21, 22, 23, 24, 100, 101,
...                                     102, 103, 113, -1, 0, 1.2, 13.6)]
[(1, '1st'), (2, '2nd'), (3, '3rd'), (4, '4th'), (20, '20th'), (21, '21st'),
 (22, '22nd'), (23, '23rd'), (24, '24th'), (100, '100th'), (101, '101st'),
 (102, '102nd'), (103, '103rd'), (113, '113th'), (-1, '-1th'), (0, '0th'),
 (1.2, '1st'), (13.6, '13th')]

Hildebrandt answered 5/2, 2019 at 9:59 Comment(1)
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
M
3

this function works well for each number n. If n is negative, it is converted to positive. If n is not integer, it is converted to integer.

def ordinal( n ):

    suffix = ['th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th']

    if n < 0:
        n *= -1

    n = int(n)

    if n % 100 in (11,12,13):
        s = 'th'
    else:
        s = suffix[n % 10]

    return str(n) + s
Misha answered 20/9, 2016 at 14:14 Comment(2)
This is necromancing in that the topic had been dead a while, but ... it's good necromancing. No cute code, very legible, and easy to understand. Nice!Yevette
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
H
2

Here is a more complicated solution I just wrote that takes into account compounded ordinals. So it works from first all the way to nine hundred and ninety ninth. I needed it to convert string street names to the number ordinals:

import re
from collections import OrderedDict

ONETHS = {
    'first': '1ST', 'second': '2ND', 'third': '3RD', 'fourth': '4TH', 'fifth': '5TH', 'sixth': '6TH', 'seventh': '7TH',
    'eighth': '8TH', 'ninth': '9TH'
}

TEENTHS = {
    'tenth': '10TH', 'eleventh': '11TH', 'twelfth': '12TH', 'thirteenth': '13TH',
    'fourteenth': '14TH', 'fifteenth': '15TH', 'sixteenth': '16TH', 'seventeenth': '17TH', 'eighteenth': '18TH',
    'nineteenth': '19TH'
}

TENTHS = {
    'twentieth': '20TH', 'thirtieth': '30TH', 'fortieth': '40TH', 'fiftieth': '50TH', 'sixtieth': '60TH',
    'seventieth': '70TH', 'eightieth': '80TH', 'ninetieth': '90TH',
}

HUNDREDTH = {'hundredth': '100TH'}  # HUNDREDTH not s

ONES = {'one': '1', 'two': '2', 'three': '3', 'four': '4', 'five': '5', 'six': '6', 'seven': '7', 'eight': '8',
        'nine': '9'}

TENS = {'twenty': '20', 'thirty': '30', 'forty': '40', 'fifty': '50', 'sixty': '60', 'seventy': '70', 'eighty': '80',
        'ninety': '90'}

HUNDRED = {'hundred': '100'}

# Used below for ALL_ORDINALS
ALL_THS = {}
ALL_THS.update(ONETHS)
ALL_THS.update(TEENTHS)
ALL_THS.update(TENTHS)
ALL_THS.update(HUNDREDTH)

ALL_ORDINALS = OrderedDict()
ALL_ORDINALS.update(ALL_THS)
ALL_ORDINALS.update(TENS)
ALL_ORDINALS.update(HUNDRED)
ALL_ORDINALS.update(ONES)


def split_ordinal_word(word):
    ordinals = []
    if not word:
        return ordinals 

    for key, value in ALL_ORDINALS.items():
        if word.startswith(key):
            ordinals.append(key)
            ordinals += split_ordinal_word(word[len(key):])
            break
    return ordinals

def get_ordinals(s):
    ordinals, start, end = [], [], []
    s = s.strip().replace('-', ' ').replace('and', '').lower()
    s = re.sub(' +',' ', s)  # Replace multiple spaces with a single space
    s = s.split(' ')

    for word in s:
        found_ordinals = split_ordinal_word(word)
        if found_ordinals:
            ordinals += found_ordinals
        else:  # else if word, for covering blanks
            if ordinals:  # Already have some ordinals
                end.append(word)
            else:
                start.append(word)
    return start, ordinals, end


def detect_ordinal_pattern(ordinals):
    ordinal_length = len(ordinals)
    ordinal_string = '' # ' '.join(ordinals)
    if ordinal_length == 1:
        ordinal_string = ALL_ORDINALS[ordinals[0]]
    elif ordinal_length == 2:
        if ordinals[0] in ONES.keys() and ordinals[1] in HUNDREDTH.keys():
            ordinal_string = ONES[ordinals[0]] + '00TH'
        elif ordinals[0] in HUNDRED.keys() and ordinals[1] in ONETHS.keys():
            ordinal_string = HUNDRED[ordinals[0]][:-1] + ONETHS[ordinals[1]]
        elif ordinals[0] in TENS.keys() and ordinals[1] in ONETHS.keys():
            ordinal_string = TENS[ordinals[0]][0] + ONETHS[ordinals[1]]
    elif ordinal_length == 3:
        if ordinals[0] in HUNDRED.keys() and ordinals[1] in TENS.keys() and ordinals[2] in ONETHS.keys():
            ordinal_string = HUNDRED[ordinals[0]][0] + TENS[ordinals[1]][0] + ONETHS[ordinals[2]]
        elif ordinals[0] in ONES.keys() and ordinals[1] in HUNDRED.keys() and ordinals[2] in ALL_THS.keys():
            ordinal_string =  ONES[ordinals[0]] + ALL_THS[ordinals[2]]
    elif ordinal_length == 4:
        if ordinals[0] in ONES.keys() and ordinals[1] in HUNDRED.keys() and ordinals[2] in TENS.keys() and \
           ordinals[3] in ONETHS.keys():
                ordinal_string = ONES[ordinals[0]] + TENS[ordinals[2]][0] + ONETHS[ordinals[3]]

    return ordinal_string

And here is some sample usage:

# s = '32 one   hundred and forty-third st toronto, on'
#s = '32 forty-third st toronto, on'
#s = '32 one-hundredth st toronto, on'
#s = '32 hundred and third st toronto, on'
#s = '32 hundred and thirty first st toronto, on'
# s = '32 nine hundred and twenty third st toronto, on'
#s = '32 nine hundred and ninety ninth st toronto, on'
s = '32 sixty sixth toronto, on'

st, ords, en = get_ordinals(s)
print st, detect_ordinal_pattern(ords), en
Handsel answered 7/5, 2016 at 22:45 Comment(1)
one of the very few correct answers! most others don’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
H
1

Gareth's code expressed using the modern .format()

ordinal = lambda n: "{}{}".format(n,"tsnrhtdd"[(n/10%10!=1)*(n%10<4)*n%10::4])
Hooten answered 18/7, 2017 at 0:5 Comment(2)
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
Nice one-liner. Does just what I was looking for. Thanks! I made it even shorter with f-string format. ordinal = lambda n: f"{n}{"tsnrhtdd"[(n/10%10!=1)*(n%10<4)*n%10::4]}"Augie
M
1

If you don't want to import an external module and prefer a one-line solution, then the following is probably (slightly) more readable than the accepted answer:

def suffix(i):
    return {1:"st", 2:"nd", 3:"rd"}.get(i%10*(i%100 not in [11,12,13]), "th"))

It uses dictionary .get, as suggested by https://codereview.stackexchange.com/a/41300/90593 and https://mcmap.net/q/223923/-ordinal-numbers-replacement.

I made use of multiplication with a boolean to handle the special cases (11,12,13) without having to start an if-block. If the condition (i%100 not in [11,12,13]) evaluates to False, the whole number is 0 and we get the default 'th' case.

Miamiami answered 31/7, 2017 at 12:34 Comment(1)
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
R
0

This can handle any length number, the exceptions for ...#11 to ...#13 and negative integers.

def ith(i):return(('th'*(10<(abs(i)%100)<14))+['st','nd','rd',*['th']*7][(abs(i)-1)%10])[0:2]

I suggest using ith() as a name to avoid overriding the builtin ord().

# test routine
for i in range(-200,200):
    print(i,ith(i))

Note: Tested with Python 3.6; The abs() function was available without explicitly including a math module.

Rotgut answered 15/5, 2018 at 19:36 Comment(1)
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured
W
0

Try this

import sys

a = int(sys.argv[1])

for i in range(1,a+1):

j = i
if(j%100 == 11 or j%100 == 12 or j%100 == 13):
    print("%dth Hello"%(j))
    continue            
i %= 10
if ((j%10 == 1) and ((i%10 != 0) or (i%10 != 1))):
    print("%dst Hello"%(j))
elif ((j%10 == 2) and ((i%10 != 0) or (i%10 != 1))):
    print("%dnd Hello"%(j))
elif ((j%10 == 3) and ((i%10 != 0) or (i%10 != 1))):
    print("%drd Hello"%(j))
else:
    print("%dth Hello"%(j))
Westernize answered 16/9, 2018 at 14:1 Comment(0)
H
-1

I salute Gareth's lambda code. So elegant. I only half-understand how it works though. So I tried to deconstruct it and came up with this:

def ordinal(integer):

    int_to_string = str(integer)

    if int_to_string == '1' or int_to_string == '-1':
        print int_to_string+'st'
        return int_to_string+'st';
    elif int_to_string == '2' or int_to_string == '-2':
        print int_to_string+'nd'
        return int_to_string+'nd';
    elif int_to_string == '3' or int_to_string == '-3':
        print int_to_string+'rd'
        return int_to_string+'rd';

    elif int_to_string[-1] == '1' and int_to_string[-2] != '1':
        print int_to_string+'st'
        return int_to_string+'st';
    elif int_to_string[-1] == '2' and int_to_string[-2] != '1':
        print int_to_string+'nd'
        return int_to_string+'nd';
    elif int_to_string[-1] == '3' and int_to_string[-2] != '1':
        print int_to_string+'rd'
        return int_to_string+'rd';

    else:
        print int_to_string+'th'
        return int_to_string+'th';


>>> print [ordinal(n) for n in range(1,25)]
1st
2nd
3rd
4th
5th
6th
7th
8th
9th
10th
11th
12th
13th
14th
15th
16th
17th
18th
19th
20th
21st
22nd
23rd
24th
['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th',             
'11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th', 
'20th', '21st', '22nd', '23rd', '24th']
Hooten answered 16/7, 2017 at 16:28 Comment(1)
nice, but it doesn’t answer the question ('first''1st') but another problem (1'1st') insteadIllnatured

© 2022 - 2024 — McMap. All rights reserved.