ETOOBUSY 🚀 minimal blogging for the impatient
PWC162 - Wheatstone-Playfair
TL;DR
On with TASK #2 from The Weekly Challenge #162. Enjoy!
The challenge
Implement encryption and decryption using the Wheatstone-Playfair cipher.
Examples:
(These combine I and J, and use X as padding.) encrypt("playfair example", "hide the gold in the tree stump") = "bmodzbxdnabekudmuixmmouvif" decrypt("perl and raku", "siderwrdulfipaarkcrw") = "thewexeklychallengex"
The questions
If the example is to be taken into account, I guess that letters I
and
J
should be coalesced, and X
used as a padding. Right?
The solution
Before delving into the solution, we can see that encryption and decryption are almost the same, except for the direction of getting letters in the same row or column, This can be easily arranged with an offset parameter to tell how much displacement we want to account for depending on the operation.
So let’s start with Perl first:
#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';
say encrypt('playfair example', 'hide the gold in the tree stump');
say decrypt('perl and raku', 'siderwrdulfipaarkcrw');
sub encrypt ($key, $message) { wheatstone_playfair($key, $message, 1) }
sub decrypt ($key, $message) { wheatstone_playfair($key, $message, 5 - 1) }
The main workhorse is then the following function, I hope the comments are sufficient!
sub wheatstone_playfair ($key, $message, $off) {
# pre-massage the input, go uppercase and remove all j:s
$_ = lc($_) =~ s{j}{i}rgmxs for $key, $message;
# we don't need no stinkin' matrix, a bijection in two arrays is OK
my %flag;
my @letter_at = grep { $flag{$_}++ == 0 }
split(m{[^a-z]?}imxs, $key), 'a' .. 'i', 'k' .. 'z', 'j';
# the "go back" might be a hash but we are C nostalgic
my $oA = ord('a'); # used to turn lc letters into array indexes
my @pos_of = map { $_->[0] } # get indexes
sort { $a->[1] cmp $b->[1] } # sorted by letter position
map { [$_, $letter_at[$_]] } 0 .. $#letter_at;
# take only letters into consideration, split on everything else
my @message = split m{[^a-z]?}imxs, $message;
my @output;
while (@message) {
# first letter is whatever, second letter might be an X
my $A = shift @message;
my $B = @message && $message[0] ne $A ? shift @message : 'x';
# get positions, $A and $B are spoiled on the way but it's OK
my ($Ax, $Ay, $Bx, $By) = map {
my $v = $pos_of[ord($_) - $oA];
($v % 5, int($v / 5))
} ($A, $B);
# apply Wheatstone-Playfair mapping
($Ax, $Ay, $Bx, $By) =
$Ax == $Bx ? ($Ax, ($Ay + $off) % 5, $Bx, ($By + $off) % 5)
: $Ay == $By ? (($Ax + $off) % 5, $Ay, ($Bx + $off) % 5, $By)
: ($Bx, $Ay, $Ax, $By);
push @output, @letter_at[$Ax + 5 * $Ay, $Bx + 5 * $By];
} ## end while (@message)
return join '', @output;
} ## end sub wheatstone_playfair
I opted for a C accent this time, making things unnecessarily complicated by using an array where the natural choice would be a hash. Call it like playing NetHack with some self-imposed discipline.
Fun fact: I don’t really play NetHack, but I’d love to.
Anyway, this crazyness stops here and in Raku we’re on to proper hashes:
#!/usr/bin/env raku
use v6;
put encrypt('playfair example', 'hide the gold in the tree stump');
put decrypt('perl and raku', 'siderwrdulfipaarkcrw');
sub encrypt ($key, $message) { wheatstone-playfair($key, $message, 1) }
sub decrypt ($key, $message) { wheatstone-playfair($key, $message, 5 - 1) }
sub wheatstone-playfair ($key is copy, $message is copy, $off) {
for $key, $message { $_ = $_.lc; s:g/j/i/ }
# we don't need no stinkin' matrix, a bijection in two arrays is OK
my %flag;
my @letter-at = ($key.comb(/<[a .. z]>/), 'a' .. 'i', 'k' .. 'z', 'j')
.flat.grep({ %flag{$_}++ == 0 });
# in Raku we're not C nostalgic any more
my %pos-of = (0..25).map({ @letter-at[$_] => $_ });
# take only letters into consideration, split on everything else
my @message = $message.comb(/<[ a ..z ]>/);
my @output;
while @message {
# first letter is whatever, second letter might be an X
my $A = shift @message;
my $B = @message && @message[0] ne $A ?? @message.shift !! 'x';
# get positions, $A and $B are spoiled on the way but it's OK
my ($Ax, $Ay, $Bx, $By) =
($A, $B).map({ my $v = %pos-of{$_}; ($v % 5, ($v / 5).Int) }).flat;
# apply Wheatstone-Playfair mapping
($Ax, $Ay, $Bx, $By) =
$Ax == $Bx ?? ($Ax, ($Ay + $off) % 5, $Bx, ($By + $off) % 5)
!! $Ay == $By ?? (($Ax + $off) % 5, $Ay, ($Bx + $off) % 5, $By)
!! ($Bx, $Ay, $Ax, $By);
@output.push: @letter-at[$Ax + 5 * $Ay, $Bx + 5 * $By].Slip;
} ## end while (@message)
return join '', @output;
}
So there we go! Stay safe!