Perl Weekly Challenge 015: Strong and Weak Primes, and Vigenère Cipher
Strong and Weak Primes
Write a script to generate first 10 strong and weak prime numbers.
The two sets of primes are defined with the following formulas:
- p(n) is Strong when
p(n) > [ p(n-1) + p(n+1) ] / 2
- p(n) is Weak when
p(n) < [ p(n-1) + p(n+1) ] / 2
It took me some time to realise there is a third set of primes which isn’t mentioned in the above list: there are also Balanced primes. The comparison operator for them is =
.
To generate primes, I reused the (slightly modified) module I created for the Challenge 012. It caches the primes found so far and extends their list when needed.
package My::Primes;
use warnings;
use strict;
sub new { bless [2, 3], shift }
sub is_prime {
my ($self, $n) = @_;
$self->grow until $self->[-1] >= $n;
return !! grep $_ == $n, @$self
}
sub grow {
my ($self) = @_;
my $candidate = $self->[-1];
my $is = 0;
PRIME:
until ($is) {
$candidate += 2;
my $i = 0;
$is = 1;
while ($self->[$i] <= sqrt $candidate) {
$is = 0, next PRIME if 0 == $candidate % $self->[$i++];
}
}
push @$self, $candidate;
}
We can let the module generate primes, what’s left to us is to categorise them as weak, strong, or balanced. Instead of an if-elsif-else or a nested ternary operator, I used a trick: I kept a list of the arrays to push to and used the comparison operator <=>
to select the particular array. The operator returns -1, 0, or 1, we can easily use these numbers as list subscripts—just keep in mind that -1 comes last.
#! /usr/bin/perl
use warnings;
use strict;
my $p = 'My::Primes'->new;
my (@strong, @weak, @balanced);
while (@strong < 10 || @weak < 10) {
$p->grow;
push @{
(\@balanced, \@strong, \@weak)[
$p->[-2] <=> ($p->[-3] + $p->[-1]) / 2 ]
}, $p->[-2];
}
And we can output the results (needs use feature qw{ say };
):
say for "Strong: @strong[0 .. 9]",
"Weak: @weak[0 .. 9]",
"Balanced: @balanced";
Vigenère Cipher
I started with a test, based on the Wikipedia page. I decided to use Object Orientation, the constructor would take the keyword as a parameter.
use Test::More tests => 2;
my $v = 'My::Vigenere'->new('LEMON');
is $v->encode('ATTACKATDAWN'), 'LXFOPVEFRNHR', 'encode';
is $v->decode('LXFOPVEFRNHR'), 'ATTACKATDAWN', 'decode';
I then proceeded to implement the two methods. They ended up very similar to each other, so I deduplicated the common logic into an internal method: in encode
, we need to “add” the characters and handle the “overflow” beyond Z, in decode
, we need to “subtract" the characters and handle the “underflow” below A.
package My::Vigenere;
use warnings;
use strict;
use constant CHAR_COUNT => 1 + ord('Z') - ord('A');
sub new { bless \$_[1], $_[0] }
sub encode { $_[0]->_code($_[1], 1) }
sub decode { $_[0]->_code($_[1], -1) }
sub _code {
my ($self, $string, $direction) = @_;
my $x_coded = "";
for my $i (0 .. length($string) - 1) {
my $ch = substr $string, $i, 1;
my $pch = substr $$self, $i % length $$self, 1;
my $ord = ord($ch) + $direction * (ord($pch) - ord('A'));
$ord -= CHAR_COUNT if $ord > ord 'Z';
$ord += CHAR_COUNT if $ord < ord 'A';
$x_coded .= chr $ord ;
}
return $x_coded
}
We need to subtract the ord('A')
because the character codes don’t start at 0 for A.
Leave a comment