Ryan Thompson › Perl Weekly Review #048

Sunday, Mar 1, 2020| Tags: perl

Continues from previous week.

Welcome to the Perl review for Week 048 of the Weekly Challenge! For a quick overview, go through the original tasks and recap of the weekly challenge.

Getting in Touch

Email › Email me (Ryan) with any feedback about this review.

GitHub › Submit a pull request for any issues you may find with this page.

Twitter › Join the discussion on Twitter!

We’d greatly appreciate any feedback you’d like to give.

Table of Contents

Task 1

[ Alexander Karelas | Alicia Bielsa | Andrezgz | Cheok-Yin Fung | Colin Crain | Dave Cross | Dave Jacoby | Duane Powell | Duncan C. White | E. Choroba | Ian Rifkin | Jaldhar H. Vyas | Javier Luque | Laurent Rosenfeld | Lubos Kolouch | Markus Holzer | Mohammad S Anwar | Phillip Harris | Roger Bell West | Ruben Westerberg | Ryan Thompson | Saif Ahmed | Jen Guerra | Steven Wilson | Ulrich Rieke | User Person | Walt Mankowski | Wanderdoc ]

Task 2

[ Alexander Karelas | Alicia Bielsa | Andrezgz | Cheok-Yin Fung | Colin Crain | Dave Cross | Dave Jacoby | Duane Powell | Duncan C. White | E. Choroba | Ian Rifkin | Jaldhar H. Vyas | Javier Luque | Laurent Rosenfeld | Lubos Kolouch | Mohammad S Anwar | Peter Scott | Phillip Harris | Roger Bell West | Ruben Westerberg | Ryan Thompson | Saif Ahmed | Jen Guerra | Steven Wilson | User Person | Walt Mankowski | Wanderdoc ]

Blogs



Task #1 - Survivor

Here is the original task description:

There are 50 people standing in a circle in position 1 to 50. The person standing at position 1 has a sword. He kills the next person i.e. standing at position 2 and pass on the sword to the immediate next i.e. person standing at position 3. Now the person at position 3 does the same and it goes on until only one survives.

Write a script to find out the survivor.


This is a classic problem in computer science otherwise known as the Josepheus problem. There are several ways to tackle the problem, ranging from brute force methods all the way to the constant-time analytical solutions.

Looping with splice

The splice builtin will remove element(s) at the given position if a LIST argument is not given. Thus some solutions start with an array containing 1..50 and loop until that array has one element, using splice to remove the correct element.

Since this method requires explicit array indexing, it can be a bit error prone to implement, as evidenced by a couple of solutions that arrived at the wrong result. In particular, it’s important to pay careful attention to the index wrap-around; since the sword skips one person, the index sometimes needs to be reset to 1, other times 0. Using the modulo (%) operator is a common trick for handling this.

Circular linked list

Circular linked lists are a very natural way to solve this problem. Perl does not have a built-in linked list type, but that’s perhaps the fun of it: there are a few good ways to make a linked list for this problem. References work, of course, but it’s also possible to make an O(1) linked list from a humble array of integers.

My blog has more discussion on circular linked lists and their use in this problem.

Looping with shift/push

A clever Perlish way to solve the problem involves storing the values 1..50 in an array, and then looping until the array has one element, with the following loop body:

    push @a, shift @a;  # Rotation ("Next")
    shift @a;           # Kill

As to why this works, the rotation is what essentially moves the cursor along. It does not change the relative positions of each element. Then the next shift (kill) just removes the person at that position.

Here is an example for five people:

Start              Rotation     Killed
1 2 3 4 5       -> 2 3 4 5 1 -> 3 4 5 1
    3 4 5 1     -> 4 5 1 3   -> 5 1 3
        5 1 3   -> 1 3 5     -> 3 5
            3 5 -> 5 3       -> 3 -> DONE

Brute force loops

Yet another way to solve this challenge involves putting all of your people into an array of 1s (1 = alive, 0 = dead), and whenever you need to kill someone or pass the sword along, you simply advance the cursor until you find a 1.

It’s a little inefficient, especially in the later rounds when most everyone is dead, but it certainly runs quickly enough for small problem sizes.


Alexander Karelas

This is Alexander’s first time contributing to the Challenge. Welcome, Alexander!

Alexander Karelas’s solution uses splice with no LIST argument to remove elements from the @people list one at a time as they are “killed”, until the array only contains one element: the number of the survivor.

my @people = (1 .. 50);
my $sword_at = 0; # array index
while (@people > 1) {
    if ($sword_at < $#people) {
        splice @people, $sword_at + 1, 1;
        $sword_at++;
    } else {
        splice @people, 0, 1;
        $sword_at = 0;
    }
}
say $people[0];

Alicia Bielsa

Alicia Bielsa’s solution first initializes an array, @aPeople that is a singly-linked list, with an entry for each person. The $swordPosition is then tracked until $numberPeopleAlive is 1. The loop deletes one entry, and advances $swordPosition, just as the problem demands.

my $numberPeople = 50;
my $numberPeopleAlive = $numberPeople;
my @aPeople = ();
foreach my $position (1..$numberPeople){
    my $nextPosition = $position == $numberPeople ? 1 : $position +1;
    my %hTmp = (  'nextPosition' => $nextPosition);
    push (@aPeople, \%hTmp);
}
my $swordPosition  = 1;
while ($numberPeopleAlive > 1){
    my $killPosition = $aPeople[ $swordPosition - 1 ]->{'nextPosition'};
    $aPeople[ $swordPosition - 1 ]->{'nextPosition'}  = $aPeople[ $killPosition - 1 ]->{'nextPosition'};
    $swordPosition = $aPeople[ $killPosition - 1 ]->{'nextPosition'};
    $numberPeopleAlive--;
}
print "Last Position Alive : $swordPosition\n";

This is a good, explicit example of a linked list implementation using an array of hash refs.

Andrezgz

Andrezgz’s solution works with an array of ints and also uses splice to remove victims of the sword.

my @people = (1..50);
my $killed = 0;
while (@people > 1) {
    $killed -= @people if ++$killed > $#people;
    printf '%2d kills %-2d'.$/, $people[$killed-1], $people[$killed];
    splice @people, $killed, 1;
}
# The Highlander, there can be only one
printf $/.'%d is the survivor',@people;

I remove most code comments in these reviews, but the “Highlander” comment was too good to omit.

Cheok-Yin Fung

Cheok-Yin Fung’s solution stores an array of 1s, one for every person who is alive. The nextalive sub skips over all zeroes and returns the index of the first 1 found:

my $n = 50;
my @ppl = (1) x $n;
my $swordposition;
$swordposition = 0;
sub nextalive {
    my $temp = $_[0];
    do {
        $temp = ($temp+1) % $n;
    } until ($ppl[$temp] == 1);
    $temp;
}

The killing sub is efficient in its brutality:

sub killing {
    $ppl[&nextalive($_[0])] = 0;
}

for my $i (1..$n-1) {
    my $soldier = $swordposition;
    &killing($swordposition); # print "one man has been killed. \n";
        $swordposition = &nextalive($soldier);
}
print &nextalive+1;

Colin Crain

Colin Crain’s solution opens with a heavy dose of Colin’s colourful commentary I’ve come to appreciate. I’m not going to spoil it with excerpts; please click on the source link to see the whole thing. That being said, the comments I’ve left in (below) might give you a taste:

my $victims = 50;  ## I think the last person standing still qualifies as a victim in this
                   ## psychopath's fever-dream battle royal
my @circle = (0..$victims-1);
say join ", ", @circle;
my $next = 0;
while ( scalar @circle > 1 ) {
    $next = ++$next % scalar @circle;    ## find the next target position
    splice @circle, $next, 1;            ## do the dirty deed and the next person slide into that spot
    say join "  ", @circle;              ## not necessary but makes the progression visible
}
say "survivor: ", $circle[0], "\n";

Oh, right. I’m supposed to be reviewing the code. Colin’s code is great. We’ve got the splice approach again, here, with some modulo arithmetic to keep the $next index within the circle.

Colin then packaged the while loop and @circle, above, into a survivor sub (not shown, but very similar to above code), and explored who the survivor is for circle sizes of 2..100 people:

for (2..100) {
    printf "%2d --> %d\n", $_, survivor($_);
}

Dave Cross

Dave Cross’ solution is both highly modular and about as self-documenting as it gets:

my @people            = ( undef, (1) x 50 );
my $person_with_sword = 1;
while () {
    kill_someone( $person_with_sword, \@people );
    last unless more_than_one_person(@people);
    $person_with_sword = hand_over_sword( $person_with_sword, \@people );
}
say "Last living person is #$person_with_sword";

@people is an array of 1s, and people who are killed are changed to zero. Dave passes around a ref to @people to avoid using a global, while allowing the subs to mutate the contents:

sub more_than_one_person {
    my $count = grep { $_ } @_;
    return $count > 1;
}

sub get_next_living_person {
    my ( $curr_person, $people ) = @_;
    while (1) {
        ++$curr_person;
        $curr_person = 1 if $curr_person > $#$people;
        return $curr_person if $people->[$curr_person];
    }
}

sub kill_someone {
    my ( $curr_person, $people ) = @_;
    my $person_to_kill = get_next_living_person( $curr_person, $people );
    $people->[$person_to_kill] = 0;
    return $person_to_kill;
}

sub hand_over_sword {
    my ( $curr_person, $people ) = @_;
    return get_next_living_person( $curr_person, $people );
}

Dave Jacoby

Dave Jacoby’s solution is the first example we will see that uses push and shift:

my @x = 1 .. 50;
my $i = 0;
while ( scalar @x > 1 ) {
    push @x, shift @x; # move killer to the end
    shift @x; # killer gets next
}
say join ',', @x;

Note that the push @x, shift @x line simply moves the first element to the end. For example, (1, 2, 3, 4, 5) would become (2, 3, 4, 5, 1), a rotation.

The next line then shifts the first element, removing 2 in this case.

This is a great Perlish solution to the problem.

Duane Powell

Duane Powell’s solution uses a circular linked list:

my $SWORDSMAN = shift || 50;
use constant {
    KILL     => undef,
        NO_SWORD => 0,
        SWORD    => 1,
};
# Swordsman constructor
sub new_swordsman {
    my $name  = shift;
    my $armed = shift;
    return {name => $name, armed => $armed, next => undef};
}
# Create a set of swordsman and arrange them in a cirle
my $first_swordsman = new_swordsman(1, SWORD);
my $swordsman = $first_swordsman;
foreach my $s (2 .. $SWORDSMAN) {
    my $next = new_swordsman($s, NO_SWORD);
    # expand the circle and continue
    $swordsman->{next} = $next;
    $swordsman = $next;
}
# Complete the cirle by linking last swordsman to the first
$swordsman->{next} = $first_swordsman;

With that set up, Duane uses what basically amounts to a “last person standing” loop:

$swordsman = $first_swordsman;
until ($swordsman->{next} == $swordsman) {
    my $condemned = $swordsman->{next}; # ID the condemned
    my $next = $condemned->{next};      # ID who gets the SWORD next
    $condemned = KILL;                  # execute the condemned
    $swordsman->{armed} = NO_SWORD;     # pass the sword, not needed but fun to simulate
    $next->{armed} = SWORD;
    # contract the circle and continue
    $swordsman->{next} = $next;
    $swordsman = $next;
}
say "$SWORDSMAN Swordsman arranged in a circle, the last man standing is Swordsman " . $swordsman->{name};

This is a fairly standard linked list implementation using hash refs as elements, with next => refs to other elements. Perl makes this easy.

Duncan C. White

Duncan C. White’s solution starts with an integer array and then loops through using splice as the “slice” method.

use Function::Parameters;
fun survivor( $n ) {
    my @alive = ( 1..$n );  # @alive list of person numbers still alive
    my $pos = 0;            # position in @alive
    my $nalive = $n;        # number of people still alive
    while( $nalive>1 ) {
        # person at pos $pos kills person next to him
        my $i = $alive[$pos];
        my $nextpos = $pos+1;
        $nextpos = 0 if $nextpos>$#alive;
        my $j = $alive[$nextpos];
        splice( @alive, $nextpos, 1 );
        $nalive--;
        # hand sword on
        $pos++;
        $pos = 0 if $pos>$#alive;
        my $k = $alive[$pos];
    }
    return $alive[0];
}

E. Choroba

E. Choroba’s solution also uses splice, but presents a compact solution thanks to taking advantage of modulo arithmetic:

my @people = 1 .. 50;
my $sword = 0;
while (@people > 1) {
    $sword = (1 + $sword) % @people;
    splice @people, $sword, 1;
    $sword %= @people;
}
say $people[0];

BlogSurvivor and Palindrome Dates

Ian Rifkin

Ian Rifkin is a new member of the Challenge. Welcome, Ian!

Ian Rifkin’s solution uses the push/shift and shift method:

my $num_people = 50;
my @people;
for (1..$num_people) {
    $people[$_] = $_;
}
my $curr = shift @people;
while (scalar(@people) > 1) {
    my $curr = shift @people;
    push @people, $curr;
    my $curr = shift @people;
    next;
}
print "\n***** The lone survivor is @people *****\n\n";

As Ian’s first submission, I’m impressed. The code has all the markers I look for in using the language, while being comprehensible and styled in a way that is easy to look at.

Jaldhar H. Vyas

Jaldhar H. Vyas’s solution stores the @people in an array. If person $n is alive, $people[$n] == $n - 1. If that person is dead, $people[$n] == undef. To find the next person, Jaldhar simply loops until $people[$next] is defined:

my @people = (0 .. 49);
my $remaining = scalar @people;
my $next = 0;
my $victim = 1;
while ($remaining > 1) {
    $people[$victim] = undef;
    $remaining--;
    do {
        $next = ($next + 1) % 50;
    } until defined $people[$next];
    $victim = $next;
    do {
        $victim = ($victim + 1) % 50;
    } until defined $people[$victim];
}
say +(grep { defined $_; } @people)[0] + 1;

With the 0-based indexing, Jaldhar simply adds 1 to the result to get the proper 1-based answer.

Javier Luque

Javier Luque’s solution stores 1..50 in @people and then leaves their fate to kill_and_switch, which uses the push/shift, shift technique:

my @people;
$people[$_] = $_ + 1 for (0..49);
kill_and_switch(\@people) while (scalar(@people > 1));
say $people[0] . " is still alive";

sub kill_and_switch {
    my $people = shift;
    # switch
    push @$people, shift @$people;
    # kill
    shift @$people;
}

Blog048 – Perl Weekly Challenge

Laurent Rosenfeld

Laurent Rosenfeld’s solution uses the push/shift, shift technique as well:

my $number = shift // 50;
my @persons = 1 .. $number;
for (1.. $number - 1) {
    push @persons, shift @persons;
    shift @persons;
}
say "Person @persons is the survivor.\n";

BlogSurvivor and Palindrome Dates

Lubos Kolouch

Lubos Kolouch’s solution has a get_last_man_standing sub that sets up a %people hash (exists $people{$i} iff person $i is alive):

sub get_last_man_standing {
    my $count = shift;
    my %people;
    for ( 1 .. $count ) {
        $people{$_} = 1;
    }
    my $last   = 0;
    my $switch = 0;
    while (%people) {
        for my $key ( sort { $a <=> $b } keys %people ) {
            delete $people{$key} if $switch;
            $last   = $key;
            $switch = $switch == 0 ? 1 : 0;
        }
    }
    return $last;
}
my $people_count = $ARGV[0];
say get_last_man_standing($people_count);

The while loop keeps going until %people is empty (so even the survivor is killed! But fortunately their spirit remains, in $last). The inner loop works on a numerically sorted list of keys %people. The $switch variable is the key, here: by toggling $switch, Lubos ensures that only every other person is killed, each time through the loop, which is exactly how the Josepheus/survivor problem works, if you try it out for yourself.

Markus Holzer

Markus Holzer’s solution uses splice and push together in an interesting way:

my @men = 1 .. 50;
while ( @men > 1 ) { push @men, my ($d,) = splice @men, 0, 2 }
print "Survivor: @men";

First, notice splice @men, 0, 2 removes the first two elements from @men, replacing them with nothing.

Next, in push @men, my ($d,) = , the my ($d) takes the first element (effectively gobbling up the second), so only the first element is pushed to the end of the array. (The comma after $d does nothing.)

This is probably easier to understand in motion. For n = 5, it results in the following sequence:

1,2,3,4,5
3,4,5,1
5,1,3
3,5
3

You may have noticed that this is exactly the same sequence you get with the shift/push, shift method, just written with different builtins.

Mohammad S Anwar

Mohammad S Anwar’s solution uses shift and push:

use warnings;
my @people = (1 ..50);
while (scalar(@people) > 1) {
    my $sword = shift @people;
    shift @people;
    push @people, $sword;
}
print "Survivor is at position $people[0]\n";

I am really glad to see Mohammad not only participating in the challenge, but now blogging about his solutions as well! His first blog post (below) covers the past three weeks (Weeks 046, 047, and 048), and is a good introspective look at Mohammad’s experience contributing solutions in Perl and his early steps in translating those solutions (and, I think, parts of his brain!) to Raku:

BlogMy first date with Raku

Phillip Harris

Phillip Harris’s solution uses a brute force looping method to iterate over the @ppl array, which is filled with the following symbols:

K - The killer      P - Living person       V - Victim

Here is the loop (with some print statements trimmed out):

while () {
    #Find next victim
    until ( $ppl[$x] eq "P" ) {
        $x++;
        if ( $x > $numppl ) { $x = 0 }
    }
    $ppl[$x] = "V";
    $ppl[$x] = "_";

    #Find next killer
    until ( $ppl[$x] eq "P" ) {
        $x++;
        if ( $x == $killer ) {    # only the killer remains
            print $x + 1 . " survives\n";
            exit;
        }
        if ( $x > $numppl ) { $x = 0 }
    }
    $ppl[$x]      = "K";
    $ppl[$killer] = "P";
    $killer       = $x;
}

Phillip’s solution not only finds the survivor, it provides an ASCII visualization of each step of the result (shown here for 11 people):

KVPPPPPPPPP
P_KVPPPPPPP
P_P_KVPPPPP
P_P_P_KVPPP
P_P_P_P_KVP
V_P_P_P_P_K
__K_V_P_P_P
__P___K_V_P
__V___P___K
______K___V
1234567890
Person 7 survives

Roger Bell West

Roger Bell West’s solution splices its way to a final victim:

my @list = ( 1 .. 50 );
my $n    = 0;
while ( scalar @list > 1 ) {
    $n++;
    if ( $n > $#list ) {
        $n = 0;
    }
    splice @list, $n, 1;
}
print $list[0], "\n";

Unfortunately, the wrong person comes away a survivor (the result of a conspiracy, perhaps?). This is because there is an off-by-one error in the index wrap-around code:

    $n++;
    if ( $n > $#list ) {
        $n = 0;
    }

This correctly sets the index back to 0 if we hit the end of the list, but if we were already at the end of the list, the index should be 1, not 0. Changing $n = 0 to $n -= @list fixes the problem. $n = ($n + 1) % @list also works.

Ruben Westerberg

Ruben Westerberg’s solution also uses splice, but is really crunched down into compact form, with some help from the modulo operator:

my @sur=1..50;
my $i=0;
splice @sur, $i=($i+1)%@sur,1 while @sur > 1;
print "Survivor: $sur[0]\n"

Ryan Thompson

My solution is in two parts. First, a circular linked list version:

my @ll = (undef, 2..$N, 1); # Circular linked list
my $cur = 1;
$ll[$cur] = $ll[$ll[$cur]], $cur = $ll[$cur] until $ll[$cur] == $cur;
say $cur;

A linked list is a high level description of a data structure. The underlying implementation can vary, and in this case, I’m using an array of numbers. The array index is the current person, and the value at that index is the next person in the linked list. I’m using two linked list operations, here ($cur is the “cursor”, or index of the current element):

$ll[$cur] = $ll[$ll[$cur]]  # "delete" -- Delete element to the right
    $cur  = $ll[    $cur ]  # "next"   -- Go to next element in list

For my second solution, I went ahead and derived a constant-time formula:

2 * ($N - 2**int( log($N) / log(2) )) + 1;

(This analytic version ran nearly 30x faster than the linked list version, which is actually good news for the linked list version; I thought it’d be slower!)

BlogSurvivor (Josepheus problem)

Saif Ahmed

Saif Ahmed’s solution really went above and beyond this week. The solution uses shift/push to get the survivor, which is all well and good, but then Saif decided to animate it by using clear/cls and then redrawing, plotting all of the victims in an ever-shrinking circle. Here’s one “frame”:

                          13    11    9    7    5    3
                17  15                                      1
            19                                                    50
                                                                    49
        21                                                              48
      23                                                                  47
    25
                                                                          46
    27                                                                    45
    28
      29                                                                  44
        30                                                              43
                                                                    42
          31                                                    41
                32  33                                      40
                          34    35    36    37    38    39

27 killed 28 and passed sword to 29

Unfortunately, I can’t show the animation here, so I encourage you to try his solution yourself.

We’ve seen the shift/push method to find the survivor already, so I’ll show Saif’s circle sub instead, which plots all of the living victims in an ellipsoid:

sub circle {
    my $circleList = shift;
    my @plotArea   = ();      # reset plot area
    push @plotArea, [ ("  ") x 40, "\n" ]
      foreach ( 0 .. 16 );    # two spaces per point
    my $center = [ 9, 20 ];   # adjust for different console sizes
    my $radius = [ 8, 18 ];   # adjust for different console sizes
    foreach my $angle ( 0 .. @$circleList - 1 ) {
        $plotArea[ $$center[0] +
          $$radius[0] * sin( $angle * 6.28 / @$circleList ) ]
          [ $$center[1] - $$radius[1] * cos( $angle * 6.28 / @$circleList ) ] =
          $$circleList[$angle];
    }
    print @{$_} foreach (@plotArea);    # the plot area
}

Jen Guerra

Jen Guerra is new to the Challenge. Welcome, Jen!

Jen Guerra’s solution uses push/shift and shift:

my $peons = 50;
my @peon;
for my $i (1..$peons) {
    push @peon, $i;
}
while (scalar @peon > 1) {
    push @peon, shift @peon;
    shift @peon;
}
say "@peon wins";

This is a very solid opening submission from Jen. I look forward to seeing more!

Steven Wilson

Steven Wilson’s solution uses splice:

my @circle   = 1 .. 50;
my $position = 1;
while ( scalar @circle > 1 ) {
    if ( $position > scalar @circle ) {
        $position = 0;
    }
    splice @circle, $position, 1;
    $position++;
}
say "Survivor is at position $circle[0]";

However, the solution gives the wrong answer, due to an off-by-one error in the index wrap code:

    if ( $position > scalar @circle ) {
        $position = 0;
    }

The problem is that @circle's length changes throughout the loop, so there are actually two possible states: we hit the end of the circle exactly, or we end up one index past the end, depending on whether we were on odd or even indexes. One way to fix this is to subtract the length of @circle from $position, so that $position will be 0 or 1:

    if ( $position >= @circle ) {
        $position -= @circle;
    }

Another way is to use modulo arithmetic. Combined with the increment step, we can use: $position = ($position + 1) % @circle;

Ulrich Rieke

Ulrich Rieke’s solution uses an array of @people = 1..50, who gradually become 'dead'. The findNextAlive sub iterates through to find the position of the next living person in the list:

sub findNextAlive {
    my $pos   = shift;
    my $array = shift;

    #we have to do one step to the right to get away from the last position
    #important to wrap around the end of the array if necessary!
    $pos++;
    if ( $pos > 49 ) {
        $pos = $pos % 50;
    }
    while ( ${$array}[$pos] !~ /\d+/ ) {
        $pos++;
        if ( $pos > 49 ) {
            $pos = $pos % 50;
        }
    }
    return $pos;
}

The main loop then keeps looping, Highlander style, until only one remains:

my @people   = ( 1 .. 50 );
my $sword_at = 0;
my $size     = 50;
while ( $size != 1 ) {
    my $victimpos = findNextAlive( $sword_at, \@people );
    $people[$victimpos] = "dead";
    $sword_at           = findNextAlive( $victimpos, \@people );
    $size               = scalar grep { $_ =~ /\d+/ } @people;
}
my @survivors = grep { $_ =~ /\d+/ } @people;
print "The last survivor is $survivors[0] !\n";

User Person

User Person’s solution provides a complete solution along with Getopt::Long commandline arguments:

use Getopt::Long;
GetOptions(\my %option, 'verbose|v') or die "Bad options\n";
my @circle = ( 1 .. 50 );
for (my $i = 1; scalar @circle > 1 ; ++$i) {
    print "i: $i\n" if $option{verbose};
    my $victim = splice(@circle, $i, 1);
    if ($option{verbose}) {
        print $circle[$i-1] . " killed " . $victim . "\n";
        print "@circle\n\n";
    }
    if ( $#circle - $i == 0) {
        $i = -1;
    } elsif ( $#circle - $i == -1) {
        $i = 0;
    }
}
print "@circle survives\n";

Note the last if .. elsif: if you recall the problem that a few people had with odd/even indexes resulting in off-by-one errors when circling around to the start of the array, User Person solves this uniquely by adding logic to look for that exact scenario, and setting the index appropriately.

Walt Mankowski

Walt Mankowski’s solution uses splice, and the modulo operator to wrap the index around:

my @person = 1..50;
my $i = 0;
while (@person > 1) {
    my $j = ($i + 1) % @person;
    splice(@person, $j, 1);
    $i = ($i + 1) % @person;
}
say "The survivor is @person";

This solution gives the wrong answer (17, expecting 37), because @person's length changes between the $i and $j assignments, so the index wrap ends up being off by 1 some of the time. (In general, it’s really easy to make mistakes like this when you are indexing over a list that changes its length.) Here is how I would fix Walt’s solution:

# Ryan's fix
my @person = 1..50;
my $i = 0;
while (@person > 1) {
    $i = ($i + 1) % @person;
    splice(@person, $i, 1);
}

Wanderdoc

Wanderdoc’s solution uses vec to make a bit vector of living people. First up is the rotate sub:

sub rotate {
     ${$_[0]} %= $par{h} if ( ${$_[0]} > $par{h} );
}

This is called with a scalar ref (rotate(\$sword)), and mutates $sword to contain the remainder of $sword / 50 (default). Here is the loop that finds the survivor:

my %par = (h => 50, w => 2, s => 1);
my $people = '';
vec($people, $_, 1) = 1 for 1 .. $par{h};
my $sword = 1; # 1-indexed by specification.
while ( unpack ('%32b*', $people) > $par{s} ) {
     my $counter = 0;
     while ( $counter < $par{w} - 1 ) {
          $sword++;
          rotate(\$sword);
          $counter++ if  1 == vec($people, $sword, 1);
     }
     vec($people, $sword, 1) = 0;
     print $sword, ' '; # To comment out on big numbers!
     $sword++ and rotate(\$sword) while ( 0 == vec($people, $sword, 1) );
}
my @survived = grep 1 == vec($people, $_, 1), 1 .. $par{h};
print "$/Survived: ", join(" ", @survived), $/;

Aside from using vec instead of array accesses, this solution is similar to the solutions we’ve seen that use fixed-length arrays.



Task #2 - Palindrome Dates

Write a script to print all Palindrome Dates between 2000 and 2999. The format of date is mmddyyyy. For example, the first one was on October 2, 2001 as it is represented as 10022001.


The solutions from this task can be broken into the following main categories:

Solutions using date libraries

Many people used one of the various date libraries on CPAN (or core Time::Local) to help them iterate through and/or validate dates they obtained.

Solutions that did not use date libraries

Others realized that, in fact, the dates meeting the specification can be validated very easily without using a date library, if you are careful with how you set up your loops.


Alexander Karelas

Alexander Karelas’s solution uses Time::Local to reject invalid dates:

use Time::Local;
for (my $year = 2000; $year <= 2999; $year++) {
    my ($day, $month) = (reverse $year) =~ /(..)(..)/;
    eval { timegm(0, 0, 0, $day, $month - 1, $year - 1900) };
    if (! $@) {
        say reverse($year) . $year;
    }
}

Alexander flipped my ($day, $month), so the results are not correct, but simply changing that to my ($month, $day) gives the right results for the required mmddyyyy dates.

Alicia Bielsa

Alicia Bielsa’s solution uses DateTime and Try::Tiny to validate dates, trying each year:

use DateTime;
use Try::Tiny;
use English;
foreach my $date (2000..2999){
    my $palindromeDate = reverse($date).$date;
    if (isValidDate ($palindromeDate )){
            print "Palindrome Date $palindromeDate \n";
    }
}
sub isValidDate {
    my $dateToValidate = shift;
    my $validDate = 1;
    my $dt = try {
         DateTime->new(
        year       => substr($dateToValidate, 4,4),
        month      => substr($dateToValidate, 0,2),
        day        => substr($dateToValidate, 2,2));
    } catch {
        if ($ARG){
            $validDate = 0;
        }
    };
    return $validDate;
}

Andrezgz

Andrezgz’s solution validates dates, but does so with a custom is_valid_dom (day of month) sub, that handles days in every month, including leap days:

sub is_valid_dom {
    my ($y, $m, $d) = @_;
    return if $d < 1 || $d > 31 || $m < 1 || $m > 12; # impossible days/months
    return if $d > 30  && grep {$m == $_} (4,6,9,11); # 30-day months
    return if $d > 29  && $m == 2;                    # 28-day February
    return if $d == 29 && $m == 2                     # 29-day February
              && ! ($y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0) ); # not a leap year
    return 1; # valid day for the month and year
}

The code to check each date, then, just iterates through the years:

my $y = 1999;
my @months = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
while ( ++$y < 3000 ) {
    # year is inverted and divided to get month and day
    my ($m, $d) = scalar(reverse $y) =~ m/../g ;
    next unless is_valid_dom($y, $m, $d);
    print $months[$m-1] . ' ' . $d . ', ' . $y .$/;
}

Andrezgz also pretty-prints the dates with the 3-letter month names, such as Sep 22, 2290.

Cheok-Yin Fung

Cheok-Yin Fung’s solution is the first we’ll see to realize that only certain days of the month will ever occur, so date validation is unnecessary:

my @dd = ("02", 12, 22);
my @ddr = (20, 21, 22);
my @mm;
for my $i (0..11) {
    if ($i <= 8) {
               $mm[$i] = "0"; $mm[$i] .=($i+1) ;
    } else {$mm[$i] = $i+1;}
}
my @temp;
foreach my $i (@mm) {
    $temp[$i] = scalar reverse $i;
}
@temp = sort {$a cmp $b} @temp;

With the valid days and months pre-calculated, the valid dates can be printed with a simple nested loop:

for my $i (0..2) {
    for my $j (0..11) {
        my $ttemp = scalar reverse $temp[$j];
        print $ttemp.$dd[$i].$ddr[$i].$temp[$j]."\n";
    }
}

Colin Crain

Colin Crain’s solution has a custom validate sub that checks for valid day of month and month numbers, recognizing that leap years need not be considered as that would be the year 9220, outside our range.

sub validate {
    my $test = shift;
    my @mlen  = ( 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    my ($m, $d) = map {substr($test, $_, 2)} (0, 2);
    return 0 if ( $m > 12           || $m == 0 );
    return 0 if ( $d > $mlen[$m-1]  || $d == 0 );
    return 1;
}

The main loop then just iterates through the year range and prints the valid palindromes, with hyphens separating the mm-dd-yyyy parts:

for ( 2000..2999 ) {
    my $pal = (reverse $_) . $_;
    if ( validate($pal) ) {
        substr($pal, $_, 0, "-") for (2,5);
        say $pal;
    }
}

Dave Cross

Dave Cross’s solution makes a deliberate decision to use ddmmyyyy dates instead:

# The original question appears to contain a typo as it suggests we use
# the illogical 'mmddyyyy' date format. My solution uses 'ddmmyyyy' instead.

Dave uses the core Time::Piece module to iterate through every day between January 1, 2000 and Dec 31, 2999, printing out the palindromes:

use Time::Piece;
use Time::Seconds;
my $fmt = '%d%m%Y';
my ( $y, $m, $d ) = ( 2000, 1, 1 );
my $str_date = sprintf '%02d%02d%d', $d, $m, $y;
my $date     = Time::Piece->strptime( $str_date, $fmt );

while ( $date->year <= 2999 ) {
    $str_date = $date->strftime($fmt);
    if ( $str_date eq reverse $str_date ) {
        say $date->ymd, " is a palindrome ($str_date)";
    }
    $date += ONE_DAY;
}

Time::Seconds exports the ONE_DAY constant, which I choose to assume without verification, is equal to 86400.

I like the direct approach Dave has taken, here. It does take 3.5 seconds to run on my system, but since this isn’t the sort of thing you’d need to re-run, that’s not really an issue.

Dave Jacoby

Dave Jacoby’s solution iterates through the years, and does a quick inline validation to throw out the invalid dates:

for my $year ( 2000 .. 2999 ) {
    my $month = reverse substr $year, 2, 2;
    my $day   = reverse substr $year, 0, 2;
    next if $day < 1;
    next if $month < 1;
    next if $day > 31;
    next if $month > 12;
    say join '-', $month, $day, $year;
}

This validation works, because the day will never exceed 22. Dave sums this up by saying “the day of the month can only be a number ending in 2, and there are no months having 32 days”.

Duane Powell

Duane Powell’s solution uses DateTime and DateTime::Duration to iterate through each day, and validate those dates:

use DateTime;
use DateTime::Duration;
my $year_end = shift || 3000;
my $dt  = DateTime->new('year' => 2000, 'month' => 1, 'day' => 1);
my $day = DateTime::Duration->new('days' => 1);
until ($dt->year == $year_end) {
    my @pali = split('',$dt->mdy);
    # @pali is a 10 element array MM-DD-YYYY
    say $dt->mdy() if ($pali[0] == $pali[9] and $pali[1] == $pali[8] and $pali[3] == $pali[7] and $pali[4] == $pali[6]);
    $dt->add( $day );
}

This took 47 seconds to run on my machine.

Duncan C. White

Duncan C. White’s solution allows the user to choose between UK (ddmmyyyy), ISO (yyyymmdd), and US (mmddyyyy) date formats, which I quite like.

The palindromic function is rather self-explanatory:

fun palindromic( $s ) {
    return $s eq reverse($s) ? 1 : 0;
}

formdate (not shown) simply takes in a $day, $month, $year and a $format and returns the date formatted to that specification.

The palindromicdates function loops over every day in the date range:

fun palindromicdates( $startyear, $endyear, $format ) {
    my @palindrome;
    foreach my $year ($startyear..$endyear) {
        foreach my $month (1..12) {
            my $ndays = Date_DaysInMonth($month,$year);
            foreach my $day (1..$ndays) {
                my $date = formdate(
                    $day, $month, $year, $format );
                push @palindrome, $date if palindromic($date);
            }
        }
    }
    return @palindrome;
}

E. Choroba

E. Choroba’s solution uses Time::Piece to validate the dates, iterating one day at a time:

#! /usr/bin/perl
use Time::Piece;
my $date = 'Time::Piece'->strptime('2000-01-01', '%Y-%m-%d');
while ($date->year < 3000) {
    my $formatted = $date->strftime('%m%d%Y');
    say $date->strftime('%B %-d, %Y') if $formatted eq reverse $formatted;
    $date += Time::Seconds::ONE_DAY;
    # Optimisation.
    $date = 'Time::Piece'->strptime(($date->year + 1) . '-01-01', '%Y-%m-%d')
        if reverse(int($date->year / 100)) > 31
        || reverse($date->year % 100) > 12;
}

BlogSurvivor and Palindrome Dates

Ian Rifkin

Ian Rifkin’s solution uses Date::Simple to validate each date, one day at a time:

use Date::Simple;
my $check_date = Date::Simple->new('2000-01-01');
my $end_date = Date::Simple->new('2999-12-31');
print "\nThe following dates from $check_date to $end_date are palindromes assuming the format mmddyyy:\n";
while ($check_date <= $end_date) {
    my $forwards = $check_date->format('%m%d%Y');
    my $backwards = reverse $forwards;
    print "$forwards (" . $check_date->format('%m-%d-%Y') . ")\n" if $forwards == $backwards;
    $check_date++
}

Jaldhar H. Vyas

Jaldhar H. Vyas’s solution uses a hard-coded list of reversed months to shorten the search space. He then uses a regex with named captures to make it easy to split out the month and day from the reverse $year:

my @years =
    grep {
        / (?<year> \d\d) $ /gmx;
        grep { $_ == $+{year}} (10, 20, 30, 40, 50, 60 , 70, 80, 90, 1, 11, 21)
    } (2000 .. 2999);
for my $year (@years) {
    (reverse $year) =~ / \A (?<month> \d\d) (?<day> \d\d) \z /gmx;
    if ($+{day} < 23) {
        say join q{/}, ($+{month}, $+{day}, $year);
    }
}

Javier Luque

Javier Luque’s solution is another using Time::Piece, iterating 86400 seconds at a time:

use Time::Piece;
use Time::Seconds;
use feature qw /say/;
my $current_date = Time::Piece->strptime('01-01-2000', '%m-%d-%Y');
my $end_date     = Time::Piece->strptime('12-31-2999', '%m-%d-%Y');
while ($current_date < $end_date) {
    my $date_string = $current_date->strftime('%m%d%Y');
    say $date_string if ($date_string eq reverse($date_string));
    $current_date = $current_date + ONE_DAY;
}

Blog048 – Perl Weekly Challenge

Laurent Rosenfeld

Laurent Rosenfeld’s solution iterates through the years, with a quick inline validation:

for my $year (2000 .. 2300) {
    my ($month, $day) = (reverse $year) =~ /(\d\d)(\d\d)/;
    next if $month > 12 or $month < 1 or $day > 31 or $day < 1;
    say "$month/$day/$year is a palindromic date.";
}

BlogSurvivor and Palindrome Dates

Lubos Kolouch

Lubos Kolouch’s solution uses DateTime to iterate one day (60 * 60 * 24 seconds) at a time, printing out anything that passes the is_palindrome test:

use DateTime;
sub is_palindrome {
    my $dt = shift;
    return 1 if $dt->mdy('') eq reverse $dt->mdy('');
    return 0;
}
my $dt_start = DateTime->new( year => 2000, month => 1, day => 1 );
my $epoch_test = $dt_start->epoch;
my $dt_end = DateTime->new( year => 2999, month => 12, day => 31 );
my $epoch_end = $dt_end->epoch;
while ($epoch_test < $epoch_end) {
    my $dt = DateTime->from_epoch( epoch => $epoch_test);
    say $dt->mdy if is_palindrome($dt);
    $epoch_test += 60 * 60 * 24;
}

Mohammad S Anwar

Mohammad S Anwar’s solution uses Date::Tiny, yet another contender in date/time modules I did not have installed before starting this review!

use Date::Tiny;
my $date = Date::Tiny->new(year => 2000, month => 1, day => 1);
while ($date->year <= 2299) {
    my $date_as_str = sprintf("%02d%02d%04d", $date->month, $date->day, $date->year);
    if ($date_as_str eq reverse($date_as_str)) {
        print "$date_as_str is a Palindrome date.\n";
    }
    my $datetime = $date->DateTime->add(days => 1);
    $date = Date::Tiny->new(year => $datetime->year, month => $datetime->month, day => $datetime->day);
}

Mohammad’s code iterates through one day at a time, checking for palindromes with reverse). The code takes 23 seconds to run on my system, but the results are correct.

BlogMy first date with Raku

Peter Scott

Peter Scott’s solution uses DateTime to advance day by day. Interestingly, Peter uses a regex with backrefs instead of reverse to check for palindromes.


#!/usr/local/bin/perl
use 5.016;
use warnings;
use DateTime;
my $dt=DateTime->new(year=>2000, month=>1, day=>1);
my $dt_end = DateTime->new(year=>2999,month=>12, day=> 31);
while ( DateTime->compare( $dt, $dt_end ) <= 0 ) {
    local $_ = $dt->strftime("%m%d%Y");
    /(\d)(\d)(\d)(\d)\4\3\2\1/ and say;
    $dt->add( days => 1 );
}

DateTime isn’t a particularly fast library, which is why the runtime here is quite long at 57 seconds on my system.

Phillip Harris

Phillip Harris’s solution has a 3-nested loop for every $year, $month, and $day within the range, using Date::Calc‘s check_date function to validate the dates:

use Date::Calc qw(check_date);

$year  = 2000;
$month = 1;
$day   = 1;

for ( $year = 2000 ; $year <= 2999 ; $year++ ) {
    for ( $month = 1 ; $month <= 12 ; $month++ ) {
        for ( $day = 1 ; $day <= 31 ; $day++ ) {
            $md = sprintf( "%02d%02d", $month, $day );
            if ( $md eq reverse($year) ) {
                if ( check_date( $year, $month, $day ) ) {
                    print "$md$year\n";
                }
            }
        }
    }
}

Date::Calc is doing a lot less, so this solution runs in about 1/4 second on my system, underscoring the vast differences between libraries with differing design goals.

Roger Bell West

Roger Bell West’s solution uses strftime from POSIX, along with some magic numbers for day numbers (days past the epoch, Jan 1, 1970):

use strict;
use warnings;
use POSIX qw(strftime);
my $format='%m%d%Y';
foreach my $d (10957..376199) {
  my $u=strftime($format,gmtime($d*86400));
  if ((scalar reverse $u) eq $u) {
    print "$u\n";
  }
}

Ruben Westerberg

Ruben Westerberg’s solution uses the by-now familiar Time::Piece and Time::Seconds modules to iterate and check for palindromes:

use Time::Piece;
use Time::Seconds;
my $s=Time::Piece->strptime("2000-01-01","%Y-%M-%D");
my $e=Time::Piece->strptime("2999-01-01","%Y-%M-%D");
my $d=$s;
while ($d < $e) {
    my $str= $d->strftime("%m%d%Y");
    print "Date is a palindrone: ".$d->strftime."\n" if ($str eq reverse $str );
    $d+=ONE_DAY;
}

Ryan Thompson

My solution started with a bit of analysis, to figure out which months and days would come up. My blog goes into the details, but the key points are:

  1. Because the date is the reverse of the century, which varies from 20..29, all dates must therefore end in 2. Thus, the date ($dd) can only be 02, 12, or 22.

  2. The 2-digit year ($yy) is also the reverse of the month, which must be 01..12. I assemble @yy so that the dates will be in sorted order.

After this, I just need the cross product of qw<02 12 22> and @yy, so a simple nested loop will suffice:

my @yy = sort map { chop . ($_||0) } 1..12;
for my $dd (qw<02 12 22>) {
    for my $yy (@yy) {
        printf "%02d-%02d-%02d%02d\n",
            scalar reverse($yy), $dd, scalar reverse($dd), $yy;
    }
}

Note that no validation is needed with this method! It runs in around 3ms, most of which is perl startup. Benchmark tells me it runs in 40μs if I change the printf to sprintf, as terminal output becomes a bottleneck.

BlogPalindrome Dates (mm/dd/yyyy)

Saif Ahmed

Saif Ahmed’s solution is another that allows the user to choose their preferred date format, in this case MDY, DMY, or YMD.

Saif iterates over each year, and then uses reverse with substr to pull out the $mm and $dd from that, swapping them if the date format needs it:

foreach my $yyyy (2000..2999){
   my $mm   =  reverse substr($yyyy,2,2);
   my $dd   =  reverse substr($yyyy,0,2);
   ($mm,$dd)=($dd,$mm) if $format eq "DMY";    # Swap if DDMMYYY required
   next if ($mm >12 or $mm ==0);               # Discard invalid months
   if ($mm =~/02/){                            # February is special case
       $notLeapYear=($y % 4)||(!($y%100)&&($y%400));
       my $FebDays=(28+($notLeapYear?0:1))."";
       next if ($dd gt $FebDays or $dd eq "00")
   }
   elsif ($mm=~/^01|03|05|07|08|10|12/){       # months with 31 days
       next if ($dd gt "31"     or $dd eq "00");
   }
   else{                                       # all the rest have 30 days
       next if ($dd gt "30"     or $dd eq "00");
  }
   print "M $mm, D $dd, Y $yyyy :  $mm$dd$yyyy \n" if $format eq "MDY";
   print "D $dd, M $mm, Y $yyyy :  $dd$mm$yyyy \n" if $format eq "DMY";
   print "Y $yyyy, M $mm, D $dd :  $yyyy$mm$dd \n" if $format eq "YMD";
}

Saif then checks for the correct number of days in the month, and if the date passes all of those checks, it is printed in the requested format.

Jen Guerra

Jen Guerra’s solution uses Date::Calc's check_date:

use Date::Calc qw/check_date/;
my $yyyy = 2000;
my $ymax = 3000;
while ($yyyy < $ymax) {
    my $mm = reverse(substr($yyyy, 2, 2));
    $yyyy++ and next if $mm > 12 || !$mm;
    my $dd = reverse(substr($yyyy, 0, 2));
    $yyyy++ and next if $dd > 31 || !$dd;
    say "$mm$dd$yyyy is a palindrome" if check_date($yyyy, $mm, $dd);
    $yyyy++;
}

Jen also uses reverse and substr to pull out the $mm and $dd components of the date, and prints out the palindromes that pass check_date. Even with the external validation, this solution runs in a mere 35ms.

Steven Wilson

Steven Wilson’s solution uses DateTime to iterate through each year, pulling it apart to get the month and day. Steven wraps the DateTime->new call in an eval to catch invalid dates:

use DateTime;
my $year = 2000;
while ( $year < 3000 ) {
    eval {
        my $dt = DateTime->new(
            year  => $year,
            month => substr( $year, -1, 1 ) . ( substr $year, -2, 1 ),
            day   => substr( $year, -3, 1 ) . ( substr $year, -4, 1 ),
        );
        my $reay = reverse $year;
        say "$reay$year";
    };
    $year++;
}

If DateTime->new dies, the say statement will not be reached, thus only palindromic dates will be printed.

User Person

User Person’s solution did some analysis of their own to optimize their solution. I’ve left the comments in, as they are more or less required reading:

#  M   M   D   D  Y   Y   Y   Y
# [01][*][012][2][2][012][*][01]
#  $k $j  $i         $i  $j  $k
#
# $k - Months can only begin with 0 or 1.
# $j - The second months digit needs all numbers e.g. January 01 to October 10 (and of course beyond).
# $i - Days begin with 0,1,2,3, However all years begin with 2 so the cooresponding number
# means all days end in 2. 32 is not a valid day so the 3 is not needed.
for ( my $i=0; $i < 3; ++$i ){
    for ( my $j=0; $j < 10; ++$j ){
      EDGES:
        for ( my $k=0; $k < 2; ++$k ){
            if (( $k == 1 and $j > 2 ) or ( $k == 0 and $j == 0 )) { # Months cannot be > 12 or 00
                next EDGES;
            }
            print "$k$j-${i}2-2$i$j$k\n";
        }
    }
}

User Person’s nested loops iterate over the 10s digit of the day ($i), 1s digit of the month ($j), and 10s digit of the month ($k). The if statement handles the bit of validation required.

Walt Mankowski

Walt Mankowski’s solution uses DateTime, looping through each day to find the palindromes:

use DateTime;
for my $year (2000..2999) {
    my $month = reverse(substr($year, 2, 2));
    if ($month >= 1 && $month <= 12) {
        my $dt = DateTime->new(year => $year, month => $month);
        for my $day (1..$dt->month_length()) {
            my $date = sprintf("%02d%02d%d", $month, $day, $year);
            say $date if $date eq reverse($date);
        }
    }
}

Using month_length method, this solution runs in just 175ms on my system, which is orders of magnitude faster than other DateTime solutions which iterate day by day like this one.

Wanderdoc

Wanderdoc’s solution is another example of Time::Piece:

use Time::Piece;
use Time::Seconds;
my $START = "01.01.2000";
my $STOP  = "12.31.2999";
my $INPUT_FORMAT = "%m.%d.%Y";
my $t1 = Time::Piece->strptime($START, $INPUT_FORMAT);
my $t2 = Time::Piece->strptime($STOP , $INPUT_FORMAT);
my $counter;
while ( $t1 <= $t2  ) {
     ((reverse $t1->yy) + 0) > 12 and
          $t1 = Time::Piece->strptime("12.31." . $t1->year , $INPUT_FORMAT);
     my $output_mdy = $t1->mdy('');
     print join("\t", ++$counter, $output_mdy), $/
          if $output_mdy eq reverse $output_mdy;
     $t1 += ONE_DAY;
}


See Also

Blogs this week:

E. ChorobaSurvivor and Palindrome Dates

Javier Luque048 – Perl Weekly Challenge

Laurent RosenfeldSurvivor and Palindrome Dates

Mohammad S AnwarMy first date with Raku

Ryan ThompsonSurvivor (Josepheus problem) | Palindrome Dates (mm/dd/yyyy)

SO WHAT DO YOU THINK ?

If you have any suggestions or ideas then please do share with us.

Contact with me