[SPOILER] Solution to Perl 'Easy' Quiz #2005-2
Daniel Martin <martin-+m399P62/[email protected]> Sat, 05 Feb 2005 11:33:43 -0500
| Newsgroups | gmane.comp.lang.perl.qotw.discuss |
|---|---|
| Message-ID | <[email protected]> |
And here's my solution, attached.
I noticed several similarities between what I did and the other
solutions already posted. I guess there are only so many ways to do
it.
By the way, the timings of the posted perl routines are on my machine:
(these are timings for 500 randomly-generated test cases)
Rate
fish-recursive_perl 0.891/s
martin-wwhd 0.958/s
duhl-delpoint8 1.60/s
martin-nestySub 2.90/s
duhl-delpoint7 3.98/s
duhl-delpoint6 4.84/s
fish-elim_last 4.97/s
duhl-delpoint3 8.09/s
duhl-delpoint4 8.66/s
martin-reverseMapReverse 9.00/s
martin-redoAppend 9.57/s
fish-count_and_replace 9.63/s
duhl-delpoint5 9.79/s
duhl-delpoint2 9.85/s
martin-rindexMapPos 12.9/s
martin-regexIt 13.0/s
fish-sexeger 14.1/s
martin-splitJoin 18.1/s
fish-two_parts 19.0/s
martin-twoRegex 19.1/s
fish-via_split 20.1/s
duhl-delpoint1 21.8/s
martin-rindexRegex 26.4/s
fish-rindex 35.0/s
fish-look_ahead 42.1/s
martin-rindexTr 47.6/s
And I'll note that the fastest one in this list exists only because I
was inspired by fish-rindex - it's a combination of fish-rindex and my
very similar martin-rindexRegex.
martin.pl
(text/x-perl, 5.3 KB)
#!perl
use strict;
# These are sorted in the order I wrote them, not in order of
# complexity or in speed order.
#
# Incidentally, the speed of each algorithm to do 500 random
# conversions works out to this on my (slow, behind the times)
# machine:
#
# Rate
# wwhd 0.921/s
# nestySub 2.88/s
# reverseMapReverse 8.86/s
# redoAppend 9.90/s
# regexIt 12.6/s
# rindexMapPos 12.7/s
# splitJoin 16.7/s
# twoRegex 19.1/s
# rindexRegex 26.5/s
# rindexTr 47.1/s
#
# I admit that I'm not at all surprised that wwhd and nestySub are
# at the bottom of the heap; they're certainly the most complex.
# However, I was a bit surprised by some of the other rankings,
# and by how truly abyssmal wwhd was.
# regexIt
# The canonical perl solution, a single regex. I have to use
# zero-width look-ahead to do it in one fell swoop; a variant
# would be:
# while ($a =~ s/\.([^.]*\.)/$1/g) {1;}
# But that's not as clean as being able to do it in one pass.
sub regexIt {
my $a = shift;
$a =~ s/\.([^.]*)(?=\.)/$1/g;
$a;
}
# twoRegex
# I suspect solutions like this one will be common. Basically,
# a regex is used to separate the input into
# (everything before the last dot) (last dot and trailer)
# then, the obvious regex substitution is made to strip dots in
# the first half.
sub twoRegex {
return $_[0] unless $_[0] =~ /^(.*)(\.[^.]*)$/;
my ($a, $b) = ($1, $2);
$a =~ s/\.//g;
$a . $b;
}
# nestySub
# So I was pondering how to be more exotic, and thought: "What would
# Haskell do?" (or rather, "What would a Haskel programmer do to
# solve this in Haskell?"), and after throwing out some ideas as not
# being sufficiently obscure, came up with this. (Though I admit it
# has much more of a scheme flavor than a Haskell flavor)
#
# Basically, by counting the number of dots, I define a function that
# can then be applied to each character of the input, and the
# function changes itself each time it hits a '.' until it becomes
# the identity function for the trailing portion of the input.
sub nestySub {
my $f = 0;
while ($_[0] =~ m/\./g) {
my $prevf = $f;
$f = sub {
if ($prevf and $_[0] eq '.') {$f=$prevf;'';}
else {$_[0];}
};
}
$f ||= sub {return $_[0];};
join('', map {$_=&$f($_);} split(//,$_[0]));
}
# rindexMapPos
# Looking for other ways to approach it, I looked through perlfunc
# and, sure enough, there was rindex ready to find the last
# occurrence of '.' for me. So I used it, and then in the last line
# I say: "Give me a string that has the same letters as $a, except
# that in positions less than $b, has nothing if $a had a period
# there."
sub rindexMapPos {
my $a = shift;
my $b = rindex($a,'.');
join('', map {my $q=substr($a,$_,1); ($q eq '.' and $_<$b)?'':$q;}
(0..length($a)-1));
}
# reverseMapReverse
# Well that was sufficiently overcomplicated - what if I just built
# the string backwards, and used a flag variable ($x) to tell me if
# I'd already hit the one allowed dot?
sub reverseMapReverse {
my $x=0;
join('', reverse map {if ($_ eq '.') {$_ = $x?'':'.'; $x=1;} $_;}
reverse split(//,$_[0]));
}
# splitJoin
# I suspect solutions like this one will be common.
#
# Basically, use split to separate the input into '.'-delimited fields
# and then use join to put the fields back together, gluing all but
# the last one together with ''.
sub splitJoin {
my @a = split(/\./,$_[0],-1);
return $a[0] unless defined($a[1]);
join('.', join('', @a[0..$#a-1]),$a[-1]);
}
# redoAppend
# What if we built up the output one field at a time, and just
# constantly corrected ourself for putting the trailing '.' on the
# output variable?
#
# Note the regex option /gc - very handy for something that scans a
# string by repeated passes with a regular expression.
sub redoAppend {
my $a = shift;
return $a unless $a =~ /^([^.]*\.)/g;
my $b = $1;
while ($a =~ /\G([^.]*\.)/gc) {substr($b,-1,1)=''; $b .= $1;}
$b .= $1 if $a =~ /\G([^.]*)$/g;
$b;
}
# rindexRegex
# This was written after I'd written my timing comparison program and
# noticed that twoRegex was so far the fastest. This is an attempt
# to spead up twoRegex on the assumption that although a regex may be
# the fastest way to remove periods, rindex is the fastest way to
# find the last one.
sub rindexRegex {
my $a = shift;
my $b = rindex($a,'.');
substr($a,0,$b) =~ s/\.//g unless $b < 1;
$a;
}
# wwhd
# This was written after I started these comments and decided what a
# Haskell programmer would really do.
#
# And no, I hadn't seen the posted Haskell solutions yet, but this
# bears a strong resemblance to Shlomi Fish's first Haskell solution.
sub wwhd {
my $wwhd_sub = sub {};
$wwhd_sub = sub {
return ($_[0],0) unless length($_[0]);
my $x = substr($_[0],0,1);
my ($xs, $founddot) = $wwhd_sub->(substr($_[0],1));
if ($x eq '.') {if ($founddot) {return ($xs, $founddot);}
else {return ($x . $xs, 1);} }
else {return ($x . $xs, $founddot);}
};
my ($retval, $ignore) = $wwhd_sub->($_[0]);
return $retval;
}
# rindexTr
# This is a speedup of rindexRegexp made after I saw Shlomi Fish's
# post and realized that a tr is faster at wiping single characters
# than a regexp.
sub rindexTr {
my $a = shift;
my $b = rindex($a,'.');
substr($a,0,$b) =~ y/.//d unless $b < 1;
$a;
}