Comma quibbling in Perl

Thanks to Rosetta Code, I found out about Eric Lippert's comma quibbling exercise:

Write me a function that takes a non-null IEnumerable and returns a string with the following characteristics:

  1. If the sequence is empty then the resulting string is "{}".
  2. If the sequence is a single item "ABC" then the resulting string is "{ABC}".
  3. (3) If the sequence is the two item sequence "ABC", "DEF" then the resulting string is "{ABC and DEF}".
  4. (4) If the sequence has more than two items, say, "ABC", "DEF", "G", "H" then the resulting string is "{ABC, DEF, G and H}". (Note: no Oxford comma!)

Also note “I am particularly interested in solutions which make the semantics of the code very clear to the code maintainer. (emphasis mine)”

I am going to ignore the requirement that the returned string be enclosed in braces.

Here is the straightforward Perl example that satisfies the clarity requirement:

#!/usr/bin/env perl

use strict;
use warnings FATAL => 'all';

use Test::More;

run();

sub run {
    my @cases = (
        [[], ''],
        [[qw(ABC)], 'ABC'],
        [[qw(ABC DEF)], 'ABC and DEF'],
        [[qw(ABC DEF G H)], 'ABC, DEF, G and H'],
    );

    for my $x (@cases) {
        my ($case, $expected) = @$x;
        ok(comma_quibbling($case) eq $expected);
    }
    done_testing;
    return;
}

sub comma_quibbling {
    my $x = shift;
    my $n = @$x;

    $n or return '';
    $n == 1 and return $x->[0];

    return join(' and ' =>
        # array slice and range operator
        join(', ' => @$x[0 .. ($n - 2)]),
        $x->[-1],
    );
}

If the maintenance programmer has issues with the use of the array slice and range operator above, s/he might use this as an opportunity to learn about them. At the point we construct the range, we know that $n ≥ 2. If $n == 2, the slice collapses to the single string $x->[0], and the join has no effect.

Now, Eric also says “There’s no size limit on the sequence; it could be tiny, it could be thousands of strings. But it will be finite.” When I get a chance, I'll compare the solution above to:

sub comma_quibbling {
    my $x = shift;
    my $n = @$x;

    $n or return '';

    my $r = $x->[0];
    $n == 1 and return $r;
    $n == 2 and return ($r .= ' and ' . $x->[1]);

    for my $i (1 .. ($n - 2)) {
        $r .= ', ' . $x->[$i];
    }

    return ($r .= ' and ' . $x->[-1]);
}

because I am curious.

PS: See also brian's post and replies to it on blogs.perl.org.