aboutsummaryrefslogtreecommitdiff
path: root/challenge-166
diff options
context:
space:
mode:
authorPerlMonk-Athanasius <PerlMonk.Athanasius@gmail.com>2022-05-29 23:36:55 +1000
committerPerlMonk-Athanasius <PerlMonk.Athanasius@gmail.com>2022-05-29 23:36:55 +1000
commit78ac8ae58db45a4c86c7056af3c1f27062357007 (patch)
tree70d55743c5623171dea336958d72e65d07b0be2b /challenge-166
parent5cb0fb12db949edd184f8510cb110fa870e07e20 (diff)
downloadperlweeklychallenge-club-78ac8ae58db45a4c86c7056af3c1f27062357007.tar.gz
perlweeklychallenge-club-78ac8ae58db45a4c86c7056af3c1f27062357007.tar.bz2
perlweeklychallenge-club-78ac8ae58db45a4c86c7056af3c1f27062357007.zip
Perl & Raku solutions to Tasks 1 & 2 for Week 166
Diffstat (limited to 'challenge-166')
-rw-r--r--challenge-166/athanasius/perl/ch-1.pl259
-rw-r--r--challenge-166/athanasius/perl/ch-2.pl280
-rw-r--r--challenge-166/athanasius/raku/ch-1.raku268
-rw-r--r--challenge-166/athanasius/raku/ch-2.raku277
4 files changed, 1084 insertions, 0 deletions
diff --git a/challenge-166/athanasius/perl/ch-1.pl b/challenge-166/athanasius/perl/ch-1.pl
new file mode 100644
index 0000000000..15aab9670a
--- /dev/null
+++ b/challenge-166/athanasius/perl/ch-1.pl
@@ -0,0 +1,259 @@
+#!perl
+
+###############################################################################
+=comment
+
+Perl Weekly Challenge 166
+=========================
+
+TASK #1
+-------
+*Hexadecimal Words*
+
+Submitted by: Ryan J Thompson
+
+As an old systems programmer, whenever I needed to come up with a 32-bit num-
+ber, I would reach for the tired old examples like 0xDeadBeef and 0xC0dedBad. I
+want more!
+
+Write a program that will read from a dictionary and find 2- to 8-letter words
+that can be “spelled” in hexadecimal, _with_ the addition of the following
+letter substitutions:
+
+ • o ⟶ 0 (e.g., 0xf00d = “food”)
+ • l ⟶ 1
+ • i ⟶ 1
+ • s ⟶ 5
+ • t ⟶ 7
+
+You can use your own dictionary or you can simply open
+../../../data/dictionary.txt (relative to your script’s location in our
+[ https://github.com/manwar/perlweeklychallenge-club |GitHub repository]) to
+access the dictionary of common words from
+[ https://theweeklychallenge.org/blog/perl-weekly-challenge-161/ |Week #161].
+
+Optional Extras (for an 0xAddedFee, of course!)
+ 1. Limit the number of “special” letter substitutions in any one result
+ to keep that result at least somewhat comprehensible. (0x51105010 is
+ an actual example from my sample solution you may wish to avoid!)
+
+ 2. Find _phrases_ of words that total 8 characters in length (e.g.,
+ 0xFee1Face), rather than just individual words.
+
+=cut
+###############################################################################
+
+#--------------------------------------#
+# Copyright © 2022 PerlMonk Athanasius #
+#--------------------------------------#
+
+#==============================================================================
+=comment
+
+Optional Extras
+---------------
+1. Limit "special" letter substitutions. Command-line options:
+
+ --single Prevent '1' from representing both 'i' and 'l' in the same word.
+ Applies before --max or --prop.
+ --max=i Limit the total number of digits in any word to the given integer
+ (which must be between 0 and 8 inclusive). Takes precedence over
+ the --prop flag.
+ --prop=f Limit the number of digits in any word to the given floating-
+ point number, which is the proportion of digits to total char-
+ acters. Must be between 0 and 1 inclusive. Has no effect if the
+ --max flag is also set.
+
+2. Find phrases. Command-line option:
+
+ --phrases Only 8-character phrases are output: 8-character words, and
+ phrases composed of 2 shorter words. Where the phrase contains 2
+ words, the shorter of the two appears first. Note that the
+ command-line flags described above apply to the selection of
+ individual words, before phrases are constructed.
+
+=cut
+#==============================================================================
+
+use strict;
+use warnings;
+use Const::Fast;
+use Getopt::Long;
+
+const my $DICTIONARY => '../../../data/dictionary.txt';
+const my $HEX_LETTERS => 'ABCDEF';
+const my %HEX_NUMBERS => ( I => '1', L => '1', O => '0', S => '5', T => '7' );
+const my $LETTERS => $HEX_LETTERS . join '', sort keys %HEX_NUMBERS;
+const my $MIN_LETTERS => 2;
+const my $MAX_LETTERS => 8;
+const my $USAGE =>
+"Usage:
+ perl $0 [--max[=Int]] [--prop[=Real]] [--single] [--phrases]
+
+ --max[=Int] Maximum digits in word
+ --prop[=Real] Maximum proportion of digits
+ --single Disallow '1' as both 'I' & 'L' [default: False]
+ --phrases Find 8-letter phrases [default: False]\n";
+
+#------------------------------------------------------------------------------
+BEGIN
+#------------------------------------------------------------------------------
+{
+ $| = 1;
+ print "\nChallenge 166, Task #1: Hexadecimal Words (Perl)\n\n";
+}
+
+#==============================================================================
+MAIN:
+#==============================================================================
+{
+ my ($max_nums, $max_prop, $single_def_1, $phrases) = parse_command_line();
+
+ my $table_re = join '|', sort keys %HEX_NUMBERS;
+ my @words;
+
+ open my $fh, '<', $DICTIONARY
+ or die qq[Cannot open file "$DICTIONARY" for reading, stopped];
+
+ while (my $line = <$fh>)
+ {
+ chomp $line;
+ my $word = ucfirst $line;
+ my $length = length $word;
+
+ if ($MIN_LETTERS <= $length <= $MAX_LETTERS &&
+ $word =~ / ^ [$LETTERS]+ $ /ix)
+ {
+ my $hex = test_word( $word, $table_re, $max_nums, $max_prop,
+ $single_def_1 );
+
+ push @words, [ $word, $hex ] if defined $hex;
+ }
+ }
+
+ close $fh
+ or die qq[Cannot close file "$DICTIONARY", stopped];
+
+ display_words( $phrases ? find_phrases( \@words ) : \@words );
+}
+
+#------------------------------------------------------------------------------
+sub find_phrases
+#------------------------------------------------------------------------------
+{
+ my ($words) = @_;
+ my %len2words;
+
+ for my $word (@$words)
+ {
+ push @{ $len2words{ length $word->[ 0 ] } }, $word;
+ }
+
+ my @phrases = @{ $len2words{ $MAX_LETTERS } };
+
+ for my $i (2 .. int( $MAX_LETTERS / 2 ))
+ {
+ my $j = $MAX_LETTERS - $i;
+
+ for my $word_i (@{ $len2words{ $i } })
+ {
+ for my $word_j (@{ $len2words{ $j } })
+ {
+ push @phrases, [ $word_i->[ 0 ] . $word_j->[ 0 ],
+ $word_i->[ 1 ] . $word_j->[ 1 ] ];
+ }
+ }
+ }
+
+ return \@phrases;
+}
+
+#------------------------------------------------------------------------------
+sub test_word
+#------------------------------------------------------------------------------
+{
+ my ($word, $table_re, $max_nums, $max_prop, $single_def_1) = @_;
+
+ if ($single_def_1)
+ {
+ return if $word =~ / I /ix && $word =~ / L /ix;
+ }
+
+ my $hex = $word =~ s/ ($table_re) /$HEX_NUMBERS{ uc $1 }/girx;
+ my $from = uc( $HEX_LETTERS ) . lc( $HEX_LETTERS );
+ my $nums = length eval "\$hex =~ tr/$from//dr";
+
+ return if (defined $max_nums && $nums > $max_nums) ||
+ (defined $max_prop && $nums / length( $hex ) > $max_prop);
+
+ my $ch0 = substr $hex, 0, 1;
+
+ substr $hex, 0, 1, uc( $ch0 ) if $ch0 =~ / [$HEX_LETTERS] /ix;
+
+ return $hex;
+}
+
+#------------------------------------------------------------------------------
+sub display_words
+#------------------------------------------------------------------------------
+{
+ my ($words) = @_;
+
+ print " Word Hex\n";
+ print " --------------------\n";
+
+ for my $word (@$words)
+ {
+ printf " %-8s 0x%-10s\n", $word->[ 0 ], $word->[ 1 ];
+ }
+
+ print " --------------------\n";
+ printf " Count: %d\n", scalar @$words;
+}
+
+#------------------------------------------------------------------------------
+sub parse_command_line
+#------------------------------------------------------------------------------
+{
+ my ($max_numbers, $max_proportion);
+ my $single_def_1 = 0;
+ my $find_phrases = 0;
+
+ GetOptions
+ (
+ 'max=i' => \$max_numbers,
+ 'prop=f' => \$max_proportion,
+ 'single' => \$single_def_1,
+ 'phrases' => \$find_phrases,
+
+ ) or error( 'Error in command line arguments' );
+
+ my $args = scalar @ARGV;
+ $args == 0
+ or error( "Expected 0 command line arguments, found $args" );
+
+ if (defined $max_numbers)
+ {
+ 0 <= $max_numbers <= $MAX_LETTERS
+ or error( qq[Invalid value "$max_numbers" for max] );
+ }
+
+ if (defined $max_proportion)
+ {
+ 0 <= $max_proportion <= 1
+ or error( qq[Invalid value "$max_proportion" for prop] );
+ }
+
+ return ($max_numbers, $max_proportion, $single_def_1, $find_phrases);
+}
+
+#------------------------------------------------------------------------------
+sub error
+#------------------------------------------------------------------------------
+{
+ my ($message) = @_;
+
+ die "ERROR: $message\n$USAGE";
+}
+
+###############################################################################
diff --git a/challenge-166/athanasius/perl/ch-2.pl b/challenge-166/athanasius/perl/ch-2.pl
new file mode 100644
index 0000000000..160a945a04
--- /dev/null
+++ b/challenge-166/athanasius/perl/ch-2.pl
@@ -0,0 +1,280 @@
+#!perl
+
+###############################################################################
+=comment
+
+Perl Weekly Challenge 166
+=========================
+
+TASK #2
+-------
+*K-Directory Diff*
+
+Submitted by: Ryan J Thompson
+
+Given a few (three or more) directories (non-recursively), display a side-by-
+side difference of files that are missing from at least one of the directories.
+Do not display files that exist in every directory.
+
+Since the task is non-recursive, if you encounter a subdirectory, append a /,
+but otherwise treat it the same as a regular file.
+
+Example
+
+Given the following directory structure:
+
+ dir_a:
+ Arial.ttf Comic_Sans.ttf Georgia.ttf Helvetica.ttf Impact.otf
+ Verdana.ttf Old_Fonts/
+
+ dir_b:
+ Arial.ttf Comic_Sans.ttf Courier_New.ttf Helvetica.ttf Impact.otf
+ Tahoma.ttf Verdana.ttf
+
+ dir_c:
+ Arial.ttf Courier_New.ttf Helvetica.ttf Impact.otf Monaco.ttf
+ Verdana.ttf
+
+The output should look similar to the following:
+
+ dir_a | dir_b | dir_c
+ -------------- | --------------- | ---------------
+ Comic_Sans.ttf | Comic_Sans.ttf |
+ | Courier_New.ttf | Courier_New.ttf
+ Georgia.ttf | |
+ | | Monaco.ttf
+ Old_Fonts/ | |
+ | Tahoma.ttf |
+
+=cut
+###############################################################################
+
+#--------------------------------------#
+# Copyright © 2022 PerlMonk Athanasius #
+#--------------------------------------#
+
+#==============================================================================
+=comment
+
+Note
+----
+This script receives the names of 3 or more actual directories as input, and
+prepares the output table according to the files which those directories
+contain. However, no attempt is made to verify that files sharing the same name
+are in fact identical.
+
+=cut
+#==============================================================================
+
+use strict;
+use warnings;
+use Const::Fast;
+use List::Util qw( first );
+
+const my $USAGE =>
+"Usage:
+ perl $0 [<dirs> ...]
+
+ [<dirs> ...] 3 or more directory names\n";
+
+#------------------------------------------------------------------------------
+BEGIN
+#------------------------------------------------------------------------------
+{
+ $| = 1;
+ print "\nChallenge 166, Task #2: K-Directory Diff (Perl)\n\n";
+}
+
+#==============================================================================
+MAIN:
+#==============================================================================
+{
+ my @dirs = parse_command_line();
+ my $dir2files = map_dir_to_files( \@dirs );
+ my %file2dirs;
+
+ for my $dir (keys %$dir2files)
+ {
+ for my $file (@{ $dir2files->{ $dir } })
+ {
+ push @{ $file2dirs{ $file } }, $dir;
+ }
+ }
+
+ my $dir_count = scalar @dirs;
+
+ for my $filename (keys %file2dirs)
+ {
+ if (scalar @{ $file2dirs{ $filename } } == $dir_count)
+ {
+ delete $file2dirs{ $filename };
+ }
+ }
+
+ display_table( \@dirs, \%file2dirs );
+}
+
+#------------------------------------------------------------------------------
+sub map_dir_to_files
+#------------------------------------------------------------------------------
+{
+ my ($dirs) = @_;
+ my %dir2files;
+
+ for my $dir (@$dirs)
+ {
+ if (-d $dir)
+ {
+ opendir my $dh, $dir
+ or die qq[Cannot opendir "$dir"\n];
+
+ while (my $file = readdir $dh)
+ {
+ next if $file =~ / ^ \. /x;
+
+ my $filename = $file;
+ $filename .= '/' if -d "$dir/$file";
+
+ push @{ $dir2files{ $dir } }, $filename;
+ }
+
+ closedir $dh
+ or die qq[Cannot closedir "$dir"\n];
+ }
+ else
+ {
+ error( qq[Directory "$dir" not found] );
+ }
+ }
+
+ return \%dir2files;
+}
+
+#------------------------------------------------------------------------------
+sub display_table
+#------------------------------------------------------------------------------
+{
+ my ($dirs, $file2dirs) = @_;
+
+ my $dir2width = get_max_widths( $dirs, $file2dirs );
+
+ display_header( $dirs, $dir2width );
+
+ for my $file (sort keys %$file2dirs)
+ {
+ display_line( $file, $dirs, $file2dirs, $dir2width );
+ }
+}
+
+#------------------------------------------------------------------------------
+sub get_max_widths
+#------------------------------------------------------------------------------
+{
+ my ($dirs, $file2dirs) = @_;
+
+ my %dir2width = map { $_ => 0 } @$dirs;
+
+ for my $filename (keys %$file2dirs)
+ {
+ for my $dir (@{ $file2dirs->{ $filename } })
+ {
+ if (length $filename > $dir2width{ $dir })
+ {
+ $dir2width{ $dir } = length $filename;
+ }
+ }
+ }
+
+ return \%dir2width;
+}
+
+#------------------------------------------------------------------------------
+sub display_header
+#------------------------------------------------------------------------------
+{
+ my ($dirs, $dir2width) = @_;
+
+ my $dir0 = $dirs->[ 0 ];
+
+ printf ' %-*s', $dir2width->{ $dir0 }, $dir0;
+
+ for my $dir (@$dirs[ 1 .. $#$dirs ])
+ {
+ printf ' | %-*s', $dir2width->{ $dir }, $dir;
+ }
+
+ printf "\n %s", '-' x $dir2width->{ $dir0 };
+
+ for my $dir (@$dirs[ 1 .. $#$dirs ])
+ {
+ printf ' | %s', '-' x $dir2width->{ $dir };
+ }
+
+ print "\n";
+}
+
+#------------------------------------------------------------------------------
+sub display_line
+#------------------------------------------------------------------------------
+{
+ my ($file, $dirs, $file2dirs, $dir2width) = @_;
+
+ my $dir0 = $dirs->[ 0 ];
+
+ if (first { $_ eq $dir0 } @{ $file2dirs->{ $file } })
+ {
+ printf ' %-*s', $dir2width->{ $dir0 }, $file;
+ }
+ else
+ {
+ printf ' %s', ' ' x $dir2width->{ $dir0 };
+ }
+
+ for my $dir (@$dirs[ 1 .. $#$dirs ])
+ {
+ print ' | ';
+
+ if (first { $_ eq $dir } @{ $file2dirs->{ $file } })
+ {
+ printf '%-*s', $dir2width->{ $dir }, $file;
+ }
+ else
+ {
+ printf '%s', ' ' x $dir2width->{ $dir };
+ }
+ }
+
+ print "\n";
+}
+
+#------------------------------------------------------------------------------
+sub parse_command_line
+#------------------------------------------------------------------------------
+{
+ my $args = scalar @ARGV;
+ $args >= 3
+ or error( 'Expected at least 3 command line arguments, found ' .
+ $args );
+
+ my %dirs;
+ ++$dirs{ $_ } for @ARGV;
+
+ my @dirs = sort keys %dirs;
+
+ scalar @dirs >= 3
+ or error( 'Input contains only ' . scalar( @dirs ) . ' distinct ' .
+ 'directories, must have at least 3' );
+
+ return @dirs;
+}
+
+#------------------------------------------------------------------------------
+sub error
+#------------------------------------------------------------------------------
+{
+ my ($message) = @_;
+
+ die "ERROR: $message\n$USAGE";
+}
+
+###############################################################################
diff --git a/challenge-166/athanasius/raku/ch-1.raku b/challenge-166/athanasius/raku/ch-1.raku
new file mode 100644
index 0000000000..648c71bf7a
--- /dev/null
+++ b/challenge-166/athanasius/raku/ch-1.raku
@@ -0,0 +1,268 @@
+use v6d;
+
+###############################################################################
+=begin comment
+
+Perl Weekly Challenge 166
+=========================
+
+TASK #1
+-------
+*Hexadecimal Words*
+
+Submitted by: Ryan J Thompson
+
+As an old systems programmer, whenever I needed to come up with a 32-bit num-
+ber, I would reach for the tired old examples like 0xDeadBeef and 0xC0dedBad. I
+want more!
+
+Write a program that will read from a dictionary and find 2- to 8-letter words
+that can be “spelled” in hexadecimal, _with_ the addition of the following
+letter substitutions:
+
+ • o ⟶ 0 (e.g., 0xf00d = “food”)
+ • l ⟶ 1
+ • i ⟶ 1
+ • s ⟶ 5
+ • t ⟶ 7
+
+You can use your own dictionary or you can simply open
+../../../data/dictionary.txt (relative to your script’s location in our
+[ https://github.com/manwar/perlweeklychallenge-club |GitHub repository]) to
+access the dictionary of common words from
+[ https://theweeklychallenge.org/blog/perl-weekly-challenge-161/ |Week #161].
+
+Optional Extras (for an 0xAddedFee, of course!)
+ 1. Limit the number of “special” letter substitutions in any one result
+ to keep that result at least somewhat comprehensible. (0x51105010 is
+ an actual example from my sample solution you may wish to avoid!)
+
+ 2. Find _phrases_ of words that total 8 characters in length (e.g.,
+ 0xFee1Face), rather than just individual words.
+
+=end comment
+###############################################################################
+
+#--------------------------------------#
+# Copyright © 2022 PerlMonk Athanasius #
+#--------------------------------------#
+
+#==============================================================================
+=begin comment
+
+Optional Extras
+---------------
+1. Limit "special" letter substitutions. Command-line options:
+
+ --single Prevent '1' from representing both 'i' and 'l' in the same word.
+ Applies before --max or --prop.
+ --max=i Limit the total number of digits in any word to the given integer
+ (which must be between 0 and 8 inclusive). Takes precedence over
+ the --prop flag.
+ --prop=f Limit the number of digits in any word to the given floating-
+ point number, which is the proportion of digits to total char-
+ acters. Must be between 0 and 1 inclusive. Has no effect if the
+ --max flag is also set.
+
+2. Find phrases. Command-line option:
+
+ --phrases Only 8-character phrases are output: 8-character words, and
+ phrases composed of 2 shorter words. Where the phrase contains 2
+ words, the shorter of the two appears first. Note that the
+ command-line flags described above apply to the selection of
+ individual words, before phrases are constructed.
+=end comment
+#==============================================================================
+
+my Str constant $DICTIONARY = '../../../data/dictionary.txt';
+my Set[Str] constant $HEX-LETTERS = Set[Str].new: 'a' .. 'f';
+my constant %HEX-NUMBERS = :i('1'), :l('1'), :o('0'), :s('5'),
+ :t('7');
+my Set[Str] constant $LETTERS = $HEX-LETTERS ∪
+ Set[Str].new: %HEX-NUMBERS.keys;
+my UInt constant $MIN-LETTERS = 2;
+my UInt constant $MAX-LETTERS = 8;
+
+#------------------------------------------------------------------------------
+BEGIN
+#------------------------------------------------------------------------------
+{
+ "\nChallenge 166, Task #1: Hexadecimal Words (Raku)\n".put;
+}
+
+#==============================================================================
+sub MAIN
+(
+ Int:_ :max(:$max-numbers), #= Maximum digits in word
+ Real:_ :prop(:$max-proportion), #= Maximum proportion of digits
+ Bool:D :single(:$single-def1) = False, #= Disallow '1' as both 'I' & 'L'
+ Bool:D :phrases(:$find-phrases) = False, #= Find 8-letter phrases
+)
+#==============================================================================
+{
+ validate-arguments( $max-numbers, $max-proportion );
+
+ my Array[Str] @words;
+
+ for $DICTIONARY.IO.lines -> Str $line
+ {
+ my Str $word = $line.chomp.tc;
+ my UInt $length = $word.chars;
+
+ if $MIN-LETTERS <= $length <= $MAX-LETTERS
+ {
+ my Set[Str] $chars = Set[Str].new:
+ $word.split( '', :skip-empty ).map: { .lc };
+
+ if $chars ⊆ $LETTERS
+ {
+ my Str $hex = test-word( $word, $max-numbers,
+ $max-proportion, $single-def1 );
+
+ @words.push: Array[Str].new( $word, $hex ) if $hex.defined;
+ }
+ }
+ }
+
+ display-words( $find-phrases ?? find-phrases( @words ) !! @words );
+}
+
+#------------------------------------------------------------------------------
+sub find-phrases
+(
+ Array:D[Array:D[Str:D]] $words
+--> Array:D[Array:D[Str:D]]
+)
+#------------------------------------------------------------------------------
+{
+ my Array[Array[Str]] %len2words;
+
+ for @$words -> Array[Str] $word
+ {
+ %len2words{ $word[ 0 ].chars }.push: $word;
+ }
+
+ my Array[Str] @phrases = |%len2words{ $MAX-LETTERS };
+
+ for 1 .. ($MAX-LETTERS / 2).floor -> UInt $i
+ {
+ next if $i == 1;
+
+ my UInt $j = $MAX-LETTERS - $i;
+
+ for %len2words{ $i }.list -> Array[Str] $word-i
+ {
+ for %len2words{ $j }.list -> Array[Str] $word-j
+ {
+ @phrases.push: Array[Str].new: $word-i[ 0 ] ~ $word-j[ 0 ],
+ $word-i[ 1 ] ~ $word-j[ 1 ];
+ }
+ }
+ }
+
+ return @phrases;
+}
+
+#------------------------------------------------------------------------------
+sub test-word
+(
+ Str:D $word,
+ UInt $max-numbers,
+ Real $max-proportion,
+ Bool:D $single-def1
+--> Str
+)
+#------------------------------------------------------------------------------
+{
+ return if $single-def1 && $word ~~ m:i/ I / && $word ~~ m:i/ L /;
+
+ my Str @chars = $word.split: '', :skip-empty;
+ my Str $hex;
+
+ for @chars -> Str $char
+ {
+ if %HEX-NUMBERS{ $char.lc }:exists
+ {
+ $hex ~= %HEX-NUMBERS{ $char.lc };
+ }
+ else
+ {
+ $hex ~= $char;
+ }
+ }
+
+ $hex ~~ m:g/ \d /;
+ my UInt $nums = $/.elems;
+
+ return if $max-numbers.defined && $nums > $max-numbers;
+ return if $max-proportion.defined && $nums / $hex.chars > $max-proportion;
+
+ return $hex;
+}
+
+#------------------------------------------------------------------------------
+sub display-words
+(
+ Array:D[Array:D[Str:D]] $words
+)
+#------------------------------------------------------------------------------
+{
+ ' Word Hex'.put;
+ ' --------------------'.put;
+
+ for @$words -> Array[Str] $word
+ {
+ " %-8s 0x%-10s\n".printf: $word[ 0 ], $word[ 1 ];
+ }
+
+ ' --------------------'.put;
+ " Count: %d\n".printf: +@$words;
+}
+
+#------------------------------------------------------------------------------
+sub validate-arguments
+(
+ UInt $max-numbers,
+ Real $max-proportion
+)
+#------------------------------------------------------------------------------
+{
+ if $max-numbers.defined
+ {
+ 0 <= $max-numbers <= $MAX-LETTERS
+ or error( qq[Invalid value "$max-numbers" for max] );
+ }
+
+ if $max-proportion.defined
+ {
+ 0 <= $max-proportion <= 1
+ or error( qq[Invalid value "$max-proportion" for prop] );
+ }
+}
+
+#------------------------------------------------------------------------------
+sub error
+(
+ Str:D $message
+)
+#------------------------------------------------------------------------------
+{
+ "ERROR: $message".put;
+
+ USAGE();
+
+ exit 0;
+}
+
+#------------------------------------------------------------------------------
+sub USAGE()
+#------------------------------------------------------------------------------
+{
+ my Str $usage = $*USAGE;
+
+ $usage ~~ s/ ($*PROGRAM-NAME) /raku $0/;
+
+ $usage.put;
+}
+
+###############################################################################
diff --git a/challenge-166/athanasius/raku/ch-2.raku b/challenge-166/athanasius/raku/ch-2.raku
new file mode 100644
index 0000000000..5f78efdd2d
--- /dev/null
+++ b/challenge-166/athanasius/raku/ch-2.raku
@@ -0,0 +1,277 @@
+use v6d;
+
+###############################################################################
+=begin comment
+
+Perl Weekly Challenge 166
+=========================
+
+TASK #2
+-------
+*K-Directory Diff*
+
+Submitted by: Ryan J Thompson
+
+Given a few (three or more) directories (non-recursively), display a side-by-
+side difference of files that are missing from at least one of the directories.
+Do not display files that exist in every directory.
+
+Since the task is non-recursive, if you encounter a subdirectory, append a /,
+but otherwise treat it the same as a regular file.
+
+Example
+
+Given the following directory structure:
+
+ dir_a:
+ Arial.ttf Comic_Sans.ttf Georgia.ttf Helvetica.ttf Impact.otf
+ Verdana.ttf Old_Fonts/
+
+ dir_b:
+ Arial.ttf Comic_Sans.ttf Courier_New.ttf Helvetica.ttf Impact.otf
+ Tahoma.ttf Verdana.ttf
+
+ dir_c:
+ Arial.ttf Courier_New.ttf Helvetica.ttf Impact.otf Monaco.ttf
+ Verdana.ttf
+
+The output should look similar to the following:
+
+ dir_a | dir_b | dir_c
+ -------------- | --------------- | ---------------
+ Comic_Sans.ttf | Comic_Sans.ttf |
+ | Courier_New.ttf | Courier_New.ttf
+ Georgia.ttf | |
+ | | Monaco.ttf
+ Old_Fonts/ | |
+ | Tahoma.ttf |
+
+=end comment
+###############################################################################
+
+#--------------------------------------#
+# Copyright © 2022 PerlMonk Athanasius #
+#--------------------------------------#
+
+#==============================================================================
+=begin comment
+
+Note
+----
+This script receives the names of 3 or more actual directories as input, and
+prepares the output table according to the files which those directories
+contain. However, no attempt is made to verify that files sharing the same name
+are in fact identical.
+
+=end comment
+#==============================================================================
+
+#------------------------------------------------------------------------------
+BEGIN
+#------------------------------------------------------------------------------
+{
+ "\nChallenge 166, Task #2: K-Directory Diff (Raku)\n".put;
+}
+
+#==============================================================================
+sub MAIN
+(
+ *@dirs where { .all ~~ Str:D && +@dirs >= 3 } #= 3 or more directory names
+)
+#==============================================================================
+{
+ my Set[Str] $dirs-set = Set[Str].new: |@dirs;
+ my Str @dirs-srt = $dirs-set.keys.sort;
+
+ +@dirs-srt >= 3
+ or error( "Input contains only { +@dirs-srt } distinct directories, " ~
+ "must have at least 3" );
+
+ my Array[Str] %dir2files = map-dir-to-files( @dirs-srt );
+ my Array[Str] %file2dirs;
+
+ for %dir2files.keys -> Str $dir
+ {
+ for %dir2files{ $dir }.list -> Str $file
+ {
+ %file2dirs{ $file }.push: $dir;
+ }
+ }
+
+ my UInt $dir-count = +@dirs-srt;
+
+ for %file2dirs.keys -> Str $filename
+ {
+ if %file2dirs{ $filename }.list.elems == $dir-count
+ {
+ %file2dirs{ $filename }:delete;
+ }
+ }
+
+ display-table( @dirs-srt, %file2dirs );
+}
+
+#------------------------------------------------------------------------------
+sub map-dir-to-files
+(
+ Array:D[Str:D] $dirs
+--> Hash:D[Array:D[Str:D]]
+)
+#------------------------------------------------------------------------------
+{
+ my Array[Str] %dir2files;
+
+ for @$dirs -> Str $dir
+ {
+ if $dir.IO.d
+ {
+ for $dir.IO.dir -> IO::Path $file
+ {
+ my Str $filename = $file.basename;
+
+ $filename ~= '/' if $file.d;
+
+ %dir2files{ $dir }.push: $filename;
+ }
+ }
+ else
+ {
+ error( qq[Directory "$dir" not found] );
+ }
+ }
+
+ return %dir2files;
+}
+
+#------------------------------------------------------------------------------
+sub display-table
+(
+ Array:D[Str:D] $dirs,
+ Hash:D[Array:D[Str:D]] $file2dirs
+)
+#------------------------------------------------------------------------------
+{
+ my UInt %dir2width = get-max-widths( $dirs, $file2dirs );
+
+ display-header( $dirs, %dir2width );
+
+ for $file2dirs.keys.sort -> Str $file
+ {
+ display-line( $file, $dirs, $file2dirs, %dir2width );
+ }
+}
+
+#------------------------------------------------------------------------------
+sub get-max-widths
+(
+ Array:D[Str:D] $dirs,
+ Hash:D[Array:D[Str:D]] $file2dirs
+--> Hash:D[UInt:D]
+)
+#------------------------------------------------------------------------------
+{
+ my UInt %dir2width = $dirs.map: { $_ => 0 };
+
+ for $file2dirs.keys -> Str $filename
+ {
+ for $file2dirs{ $filename }.list -> Str $dir
+ {
+ if $filename.chars > %dir2width{ $dir }
+ {
+ %dir2width{ $dir } = $filename.chars;
+ }
+ }
+ }
+
+ return %dir2width;
+}
+
+#------------------------------------------------------------------------------
+sub display-header
+(
+ Array:D[Str:D] $dirs,
+ Hash:D[UInt:D] $dir2width
+)
+#------------------------------------------------------------------------------
+{
+ my Str $dir0 = $dirs[ 0 ];
+
+ ' %-*s'.printf: $dir2width{ $dir0 }, $dir0;
+
+ for $dirs[ 1 .. * ] -> Str $dir
+ {
+ ' | %-*s'.printf: $dir2width{ $dir }, $dir;
+ }
+
+ "\n %s".printf: '-' x $dir2width{ $dir0 };
+
+ for $dirs[ 1 .. * ] -> Str $dir
+ {
+ ' | %s'.printf: '-' x $dir2width{ $dir };
+ }
+
+ put();
+}
+
+#------------------------------------------------------------------------------
+sub display-line
+(
+ Str:D $file,
+ Array:D[Str:D] $dirs,
+ Hash:D[Array:D[Str:D]] $file2dirs,
+ Hash:D[UInt:D] $dir2width
+)
+#------------------------------------------------------------------------------
+{
+ my Str $dir0 = $dirs[ 0 ];
+
+ if $file2dirs{ $file }.first: $dir0
+ {
+ ' %-*s'.printf: $dir2width{ $dir0 }, $file;
+ }
+ else
+ {
+ ' %s'.printf: ' ' x $dir2width{ $dir0 };
+ }
+
+ for $dirs[ 1 .. * ] -> Str $dir
+ {
+ if $file2dirs{ $file }.first: $dir
+ {
+ ' | %-*s'.printf: $dir2width{ $dir }, $file;
+ }
+ else
+ {
+ ' | %s'.printf: ' ' x $dir2width{ $dir };
+ }
+ }
+
+ put();
+}
+
+#------------------------------------------------------------------------------
+sub error
+(
+ Str:D $message
+)
+#------------------------------------------------------------------------------
+{
+ "ERROR