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.

Daniel Li
  • 14,976
  • 6
  • 43
  • 60
bozo user
  • 241
  • 1
  • 6
  • 15

6 Answers6

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;
  }
  ...
}
Sinan Ünür
  • 116,958
  • 15
  • 196
  • 339
Bill Ruppert
  • 8,956
  • 7
  • 27
  • 44
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';
}
nneonneo
  • 171,345
  • 36
  • 312
  • 383
Daniel Li
  • 14,976
  • 6
  • 43
  • 60
  • 2
    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. – Andy Ross Jul 06 '12 at 23:35
  • 4
    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. – DavidO Jul 07 '12 at 08:20
  • 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) – matemaciek Sep 26 '18 at 07:23
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
Borodin
  • 126,100
  • 9
  • 70
  • 144
  • 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? – Birei Jul 23 '12 at 14:00
  • @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`. – Borodin Jul 23 '12 at 14:19
  • Ah, ok. Thank you. Didn't get the point of `$_`. It deserves a **+1**. – Birei Jul 23 '12 at 14:25
  • @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 story – Borodin Jun 05 '14 at 19:53
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');
}
Community
  • 1
  • 1
Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
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];
msh210
  • 256
  • 6
  • 23
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" );
}
Andy Lester
  • 91,102
  • 13
  • 100
  • 152