How do I retrieve an integer's ordinal suffix in Perl (like st, nd, rd, th)
Asked Answered
M

6

15

I have number and need to add the suffix: 'st', 'nd', 'rd', 'th'. So for example: if the number is 42 the suffix is 'nd' , 521 is 'st' and 113 is 'th' and so on. I need to do this in perl. Any pointers.

Mendelevium answered 6/7, 2012 at 21:31 Comment(0)
C
17

Try this:

my $ordinal;
if ($foo =~ /(?<!1)1$/) {
    $ordinal = 'st';
} elsif ($foo =~ /(?<!1)2$/) {
    $ordinal = 'nd';
} elsif ($foo =~ /(?<!1)3$/) {
    $ordinal = 'rd';
} else {
    $ordinal = 'th';
}
Chesnut answered 6/7, 2012 at 21:35 Comment(3)
Upvote for productive use of the elusive zero-width negative look-behind assertion. Though (sadly) as Bill Ruppert points out, there's a CPAN module for this already.Ribaldry
Even though there is a CPAN solution, I like this one too. It's well thought-out, highly readable, devoid of dependencies, and as accurate as the CPAN solution for any integer.Grapefruit
The same, but rewritten more perlish: sub ordinal { my $n = shift; return "${n}st" if $n =~ /(?<!1)1$/; return "${n}nd" if $n =~ /(?<!1)2$/; return "${n}rd" if $n =~ /(?<!1)3$/; return "${n}th"; }. (Note: this also returns the number itself)Exhibition
O
27

Use Lingua::EN::Numbers::Ordinate. From the synopsis:

use Lingua::EN::Numbers::Ordinate;
print ordinate(4), "\n";
 # prints 4th
print ordinate(-342), "\n";
 # prints -342nd

# Example of actual use:
...
for(my $i = 0; $i < @records; $i++) {
  unless(is_valid($record[$i]) {
    warn "The ", ordinate($i), " record is invalid!\n"; 
    next;
  }
  ...
}
Osset answered 6/7, 2012 at 21:35 Comment(0)
C
17

Try this:

my $ordinal;
if ($foo =~ /(?<!1)1$/) {
    $ordinal = 'st';
} elsif ($foo =~ /(?<!1)2$/) {
    $ordinal = 'nd';
} elsif ($foo =~ /(?<!1)3$/) {
    $ordinal = 'rd';
} else {
    $ordinal = 'th';
}
Chesnut answered 6/7, 2012 at 21:35 Comment(3)
Upvote for productive use of the elusive zero-width negative look-behind assertion. Though (sadly) as Bill Ruppert points out, there's a CPAN module for this already.Ribaldry
Even though there is a CPAN solution, I like this one too. It's well thought-out, highly readable, devoid of dependencies, and as accurate as the CPAN solution for any integer.Grapefruit
The same, but rewritten more perlish: sub ordinal { my $n = shift; return "${n}st" if $n =~ /(?<!1)1$/; return "${n}nd" if $n =~ /(?<!1)2$/; return "${n}rd" if $n =~ /(?<!1)3$/; return "${n}th"; }. (Note: this also returns the number itself)Exhibition
H
7

Try this brief subroutine

use strict;
use warnings;

sub ordinal {
  return $_.(qw/th st nd rd/)[/(?<!1)([123])$/ ? $1 : 0] for int shift;
}

for (42, 521, 113) {
  print ordinal($_), "\n";
}

output

42nd
521st
113th
Hypsometry answered 6/7, 2012 at 22:4 Comment(4)
There is something I don't fully understand here. Why a for loop when there is only one element as argument? It could also work return int( shift ) . (qw/.... For several parameters the for loop wouldn't work neither because of the return statement. It works fine as is, but did I miss something about the loop?Freckle
@Birei: it's just a way of putting $_[0] into $_. Your way wouldn't work as the regular expression needs the value to be in $_. It's very like the new given language word but you can't use that as a statement modifier as you can with for.Hypsometry
Ah, ok. Thank you. Didn't get the point of $_. It deserves a +1.Freckle
@BillRuppert: Thanks Bill. I had forgotten I'd written this! I think that for is all the broken given should do, but that's another storyHypsometry
P
3

Here's a solution which I originally wrote for a code golf challenge, slightly rewritten to conform to usual best practices for non-golf code:

$number =~ s/(1?\d)$/$1 . ((qw'th st nd rd')[$1] || 'th')/e;

The way it works is that the regexp (1?\d)$ matches the last digit of the number, plus the preceding digit if it is 1. The substitution then uses the matched digit(s) as an index to the list (qw'th st nd rd'), mapping 0 to th, 1 to st, 2 to nd, 3 to rd and any other value to undef. Finally, the || operator replaces undef with th.

If you don't like s///e, essentially the same solution could be written e.g. like this:

for ($number) {
    /(1?\d)$/ or next;
    $_ .= (qw'th st nd rd')[$1] || 'th';
}

or as a function:

sub ordinal ($) {
    $_[0] =~ /(1?\d)$/ or return;
    return $_[0] . ((qw'th st nd rd')[$1] || 'th');
}
Pyknic answered 25/7, 2012 at 0:30 Comment(0)
A
1

Another solution (though I like the preexisting answers that are independent of using modules better):

use Date::Calc 'English_Ordinal';
print English_Ordinal $ARGV[0];
Alameda answered 16/10, 2015 at 20:7 Comment(0)
S
1

And here's an entirely non-tricky way to do it.

sub english_ordinal( $n ) {
    my @suffixes = qw( th st nd rd th th th th th th );

    my $x = $n % 100;

    my $suffix;
    if ( $x >= 10 && $x <= 19 ) {
        $suffix = 'th';
    }
    else {
        $suffix = $suffixes[$x % 10];
    }

    return "$n$suffix";
}

Could you make it take up much less space, be more clever, and maybe run faster? Sure, but the rest of the answers have that covered.

Might as well have some unit tests on it while we're at it.

my %tests = (
    0   => '0th',
    1   => '1st',
    2   => '2nd',
    3   => '3rd',
    4   => '4th',
    5   => '5th',
    6   => '6th',
    7   => '7th',
    8   => '8th',
    9   => '9th',
    10  => '10th',
    11  => '11th',
    12  => '12th',
    13  => '13th',
    14  => '14th',
    15  => '15th',
    16  => '16th',
    17  => '17th',
    18  => '18th',
    19  => '19th',
    20  => '20th',
    21  => '21st',
    22  => '22nd',
    23  => '23rd',
    24  => '24th',
    25  => '25th',
    26  => '26th',
    27  => '27th',
    28  => '28th',
    29  => '29th',
    30  => '30th',
    100 => '100th',
    101 => '101st',
    102 => '102nd',
    111 => '111th',
);

while ( my ($n,$s) = each %tests ) {
    is( english_ordinal($n), $s, "$n -> $s" );
}
Scenic answered 15/11, 2022 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.