Ordered Ordinal number methods
Avi Gross
avigross at verizon.net
Sun Feb 3 16:22:36 EST 2019
[NOTE: message is a tad long as it discusses multiple possible solutions and
concerns including code.]
The original question was how to do some reasonable translation from
something like the "switch" statement in languages that have it, including C
and R. Other languages use their own variants like cond in LISP. Some have
nothing quite like it. They also differ in many ways such as the ability to
handle arbitrary conditions or bundle multiple conditions to result in the
same result.
AS noted, Python deliberately omitted this feature entirely and it is
suggested that one method is to use multiple "elif" clauses. I did a search
to see if some module may play games with that notation to solve the
specific issue relating to how to produce strings from the numbers in the
range of 1:31 used in months on the calendar used in much of the world.
(there are calendars limited to about 29 I use regularly.)
Here is a page where a method is shown using that construction:
https://codereview.stackexchange.com/questions/41298/producing-ordinal-numbe
rs
def ordinal(self, num):
"""
Returns ordinal number string from int, e.g. 1, 2, 3 becomes 1st, 2nd,
3rd, etc.
"""
self.num = num
n = int(self.num)
if 4 <= n <= 20:
suffix = 'th'
elif n == 1 or (n % 10) == 1:
suffix = 'st'
elif n == 2 or (n % 10) == 2:
suffix = 'nd'
elif n == 3 or (n % 10) == 3:
suffix = 'rd'
elif n < 100:
suffix = 'th'
ord_num = str(n) + suffix
return ord_num
The above is not my code. It is an example of how someone else solved a
similar problem. It is clearly not a free function but a method. You can
easily modify it and I annotate it below that way as a simple function to
ask some dumb questions. But first, the IF is followed by a sequence of ELIF
and no ELSE at the end. It works but is not written in a style I might
choose. It does illustrate a sort of way to do cases where a switch might be
used elsewhere. My earlier examples using inline and nested IF statements
realistically could be expanded into something more like this, too.
Here is the altered code with comments:
def ordinal(num):
"""
Returns ordinal number string from int, e.g. 1, 2, 3 becomes 1st, 2nd,
3rd, etc.
"""
n = int(num)
if 4 <= n <= 20:
suffix = 'th'
elif n == 1 or (n % 10) == 1:
suffix = 'st'
elif n == 2 or (n % 10) == 2:
suffix = 'nd'
elif n == 3 or (n % 10) == 3:
suffix = 'rd'
elif n < 100:
suffix = 'th'
ord_num = str(n) + suffix
return ord_num
I do like the way the above function accepts any kind of argument that can
be converted to an int. It does have a serious flaw in that ordinal(100) and
beyond generate an error. It does return 0th on 0 and the slightly odd -1th,
-2th and so on.
My approach was to look for patterns and note we cared only as a first
approximation at numbers ending with 1,2,3 as special and even then, only if
the preceding column was a 1. The teens are an exception. The above uses a
different paradigm using inequalities so anything between 4 and 20
(inclusive) is noted as a "th" then a slightly redundant check for ending in
1 is checked as the sole exception of 11 has already been handled earlier.
Now is the following redundant or efficient?
elif n == 1 or (n % 10) == 1:
The reality is that "1 % 10 " also matches n being 1. The OR though is a
short-circuit so in the case that n == 1, the mod operator and comparison is
skipped.
Going on, a similar check is made for numbers ending in 2 and then 3. The
last condition requires the number to be less than 100. Strictly speaking,
the 100th would also be fine. And, as noted in another message, simply
taking any positive number modulo 100 gets the same result so 103 would
follow the rule for 3 and become 103rd.
But if you really wanted to emulate a more strict model, a set of elif with
exact integers would be reasonable using 31 conditions.
if num == 1: suff = "st"
elif num == 2: suff = "nd"
. # 3 through 30
elif num == 31: suff = "st"
That is horrible code and in the worst case does 31 comparisons.
Not sure if the "in" operator is cheap enough for this version:
if num in [1,21,31]: suff = "st"
elif num in [2,22]: suff = "nd"
elif num in [3,23]: suff = "rd"
else: suff = "th"
That has fewer comparisons albeit the "in" operator does additional
searches. And, it does not scale up well as continuing to 100 means adding
items like 32 and 33 and 41 to the lists.
I already showed ways to make a dictionary version but there seem to be an
indefinite number of variations on how to solve ANYTHING in python despite
the founding lie that it is designed to do everything ONE way when possible.
I thought to use functions in the dictionary instead, but as these are
called without any known way to include arguments, I see no advantage in
making 4 functions that each just return a string like "th" when I can just
store and return the same string. If something long like a chapter of a book
was wanted, yes, copying that umpteen times might be an annoyance.
Nested dictionaries might make some sense. Again, like everything here, this
is speculating on ONE way not suggesting this is great or elegant.
I want to be able to break up my day number like 12 into a 1 and a 2. You
can use str(day) or sprint() or many other methods and then ask for the last
digit and optionally the digit before that. However you do it, you index
this way using an int in my example although '1' and '2' could easily be
used if altered.:
>>> dictByTwo[1][2]
'th'
>>> dictByTwo[2][2]
'nd'
To do the above, I made a special case for the first (tens) number to be
optionally a zero, the null string, or the None or Ellipsis unique objects.
Here is the definition of the required dictionaries:
dictNorm = { 0: "th",
1: "st",
2: "nd",
3: "rd",
4: "th",
5: "th",
6: "th",
7: "th",
8: "th",
9: "th"
}
dictTeens = {0: "th",
1: "th",
2: "th",
3: "th",
4: "th",
5: "th",
6: "th",
7: "th",
8: "th",
9: "th"
}
dictByTwo = {'': dictNorm,
None: dictNorm,
...: dictNorm,
0: dictNorm,
1: dictTeens,
2: dictNorm,
3: dictNorm,
4: dictNorm,
5: dictNorm,
6: dictNorm,
7: dictNorm,
8: dictNorm,
9: dictNorm
}
The latter dictionary actually expands out if shown to contain copies of the
other dictionaries:
{'': {0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7:
'th', 8: 'th', 9: 'th'}, None: {0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th',
5: 'th', 6: 'th', 7: 'th', 8: 'th', 9: 'th'}, Ellipsis: {0: 'th', 1: 'st',
2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7: 'th', 8: 'th', 9: 'th'}, 0:
{0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7: 'th', 8:
'th', 9: 'th'}, 1: {0: 'th', 1: 'th', 2: 'th', 3: 'th', 4: 'th', 5: 'th', 6:
'th', 7: 'th', 8: 'th', 9: 'th'}, 2: {0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4:
'th', 5: 'th', 6: 'th', 7: 'th', 8: 'th', 9: 'th'}, 3: {0: 'th', 1: 'st', 2:
'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7: 'th', 8: 'th', 9: 'th'}, 4: {0:
'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7: 'th', 8:
'th', 9: 'th'}, 5: {0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6:
'th', 7: 'th', 8: 'th', 9: 'th'}, 6: {0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4:
'th', 5: 'th', 6: 'th', 7: 'th', 8: 'th', 9: 'th'}, 7: {0: 'th', 1: 'st', 2:
'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7: 'th', 8: 'th', 9: 'th'}, 8: {0:
'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6: 'th', 7: 'th', 8:
'th', 9: 'th'}, 9: {0: 'th', 1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th', 6:
'th', 7: 'th', 8: 'th', 9: 'th'}}
But who sees that, normally? LOL!
Here are some more runs:
>>> dictByTwo[''][1]
'st'
>>> dictByTwo[None][2]
'nd'
>>> dictByTwo[...][3]
'rd'
>>> dictByTwo[6][9]
'th'
>>> str(69) + dictByTwo[6][9]
'69th'
>>> str(63) + dictByTwo[6][3]
'63rd'
Again, not a serious suggestion. I prefer a function method to do it all. As
an enhancement, so that multiple calls are fast, you can use some method
that retains memory such as a function closure or a callable class. I am
thinking a bit further out of the box here for any functionality that does
significant calculations but has a great chance to reuse previously computed
results. So what if you are asked to process a LONG list of dates in a
DataFrame object and print them out as January 1st and January 2nd and
January 3rd and January 4th and so on? (I hope the flattened file showed
suffixes.)
The plan would be for the function or class that has access to ongoing
memory to create dictionaries like the above or of any other kinds or any
sub-functions or methods when used first time. When invoked again, it skips
that and uses what it created. Even worse, it may keep track of what it has
been called with. If I call it with say the day 5 once, it can compute "5th"
and store it in a dictionary or list. I mean as the fifth item in the list
or the key value pair in a dict. If called later with the same number, a
constant-time lookup is done first and only if that fails do you do another
calculation and store that.
Note the newer generator functions can be used this way as they can accept a
new value on the "yield" command. So, yet another python object that retains
memory so it can use these techniques, perhaps.
More information about the Python-list
mailing list