Advent Calendar - December 3, 2021

Friday, Dec 3, 2021| Tags: Perl, Raku

Advent Calendar 2021

| Day 2 | Day 3 | Day 4 |


The gift is presented by Arne Sommer. Today he is talking about his solution to “The Weekly Challenge - 097”. This is re-produced for Advent Calendar 2021 from the original post by Arne Sommer.



Task #1: Caesar Cipher

You are given string $S containing alphabets A..Z only and a number $N.

Write a script to encrypt the given string $S using Caesar Cipher with left shift of size $N.



The expression alphabets A..Z only is wrong, as the example has several spaces as well. So they should be allowed.

File: caesar-cipher

#! /usr/bin/env raku

subset AZ-space of Str where /^ <[ A .. Z \s ]>+ $/;   # [1]
subset PosInt of Int where -25 <= $_ <= 25;            # [2]

unit sub MAIN (AZ-space $S = 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG',
               PosInt $N = 3);                         # [3]

say $S.comb.map({ caesar($_, $N) }).join;              # [4]

sub caesar ($char, $shift)
{
  return $char if $char eq " ";                        # [5]

  my $code = $char.ord;                                # [6]

  $code -= $shift;                                     # [7]

  $code += 26 if $code < 65;  # 'A'                    # [8]
  $code -= 26 if $code > 90;  # 'Z'                    # [8a]

  return $code.chr;                                    # [9]
}

[1] The allowed characters (or «domain specific alphabet»).

[2] The challenge says that the left shift value is a number. It does not make sense to allow anything other than integers, so I restrict the value to that type. Negative values should be ok, and they mean a right shift value (instead of left).

[3] The arguments, with default values as given in the challenge.

[4] Split the string into single charactes (with comb, apply the «caesar» function on each one (with map), join the characters together as a string again (with join), and print it.

[5] Do not shift spaces.

[6] Get the character codepoint.

[7] Subtract the shift value (as we shift to the left, or lower in the alphabet).

[8] Wrap around if we shift out of the A-Z range, here lower - or higher in [8b]

[9] Get the character with the specified codepoint.


See docs.raku.org/routine/ord for more information about ord.

See docs.raku.org/routine/chr for more information about chr.

Running it:

$ ./caesar-cipher 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' 3
QEB NRFZH YOLTK CLU GRJMP LSBO QEB IXWV ALD

$ ./caesar-cipher 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' -3
WKH TXLFN EURZQ IRA MXPSV RYHU WKH ODCB GRJ

$ ./caesar-cipher 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' 13
GUR DHVPX OEBJA SBK WHZCF BIRE GUR YNML QBT

$ ./caesar-cipher 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' -13
GUR DHVPX OEBJA SBK WHZCF BIRE GUR YNML QBT

Raku has a ords variant that takes a whole string, and not a single character as ord. And chrs which takes an array of codepoints and turns them into a string, and not a single codepoint to a character as chr. Let us use them to write a shorter program:

File: caesar-cipher-map


#! /usr/bin/env raku

subset AZ-space of Str where /^ <[ A .. Z \s ]>+ $/;
subset PosInt of Int where -25 <= $_ <= 25;

unit sub MAIN (AZ-space $S = 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG',
               PosInt $N = 3);

say caesar($S, $N);

sub caesar ($string, $shift)
{
  return $string.ords.map({$_ == 32 ?? 32 !! (($_ - $shift - 65) % 26 ) + 65}).chrs;
    # #################### # 1a ############# ############ # 1b  # 1c ## 1d
}

[1] We use map to change the individual codepoints. We let the spaces with codepoint 32 alone [1a]. Every other value we reduce to a number between 0 and 25 (by subtracting the codepoint of the first letter (A: 65) and the shift value [1b]. The modulo operator (%) takes care of negative values for us, doing the right thing. E.g. -2 % 26 -> 24 [1c]. Then we add adjust the values up to where they should be (A to Z) [1d] before we turn the whole array of codepints into a string.


See docs.raku.org/routine/ords for more information about ords.

See docs.raku.org/routine/chrs for more information about chrs.

Running it gives the same result as before:

$ ./caesar-cipher-map 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' 3
QEB NRFZH YOLTK CLU GRJMP LSBO QEB IXWV ALD

$ ./caesar-cipher-map 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' -3
WKH TXLFN EURZQ IRA MXPSV RYHU WKH ODCB GRJ

$ ./caesar-cipher-map 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' 13
GUR DHVPX OEBJA SBK WHZCF BIRE GUR YNML QBT

$ ./caesar-cipher-map 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' -13
GUR DHVPX OEBJA SBK WHZCF BIRE GUR YNML QBT

A Perl Version

This is straight forward translation of the first Raku version.

File: caesar-cipher-perl

#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use feature 'signatures';

no warnings "experimental::signatures";

my $S = shift(@ARGV) // 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG';

die "Illegal characters" unless $S =~ /^[A-Z\s]+$/;

my $N = shift(@ARGV) // 3;

die "Illegal shift $N" if $N !~ /^\-?\d+$/ || $N < -25 || $N > 25;

say join("", map { caesar($_, $N) } split(//, $S));

sub caesar ($char, $shift)
{
  return $char if $char eq " ";

  my $code = ord($char);

  $code -= $shift;

  $code += 26 if $code < 65;  # 'A'
  $code -= 26 if $code > 90;  # 'Z'

  return chr($code);
}

Running it gives the same result as the Raku version:


$ ./caesar-cipher-perl 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' 3
QEB NRFZH YOLTK CLU GRJMP LSBO QEB IXWV ALD

$ ./caesar-cipher-perl 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' -3
WKH TXLFN EURZQ IRA MXPSV RYHU WKH ODCB GRJ

$ ./caesar-cipher-perl 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' 13
GUR DHVPX OEBJA SBK WHZCF BIRE GUR YNML QBT

$ ./caesar-cipher-perl 'THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG' -13
GUR DHVPX OEBJA SBK WHZCF BIRE GUR YNML QBT

If you have any suggestion then please do share with us perlweeklychallenge@yahoo.com.

Advent Calendar 2021

SO WHAT DO YOU THINK ?

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

Contact with me