aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--challenge-166/james-smith/README.md204
1 files changed, 203 insertions, 1 deletions
diff --git a/challenge-166/james-smith/README.md b/challenge-166/james-smith/README.md
index b653ba22ba..222ff79205 100644
--- a/challenge-166/james-smith/README.md
+++ b/challenge-166/james-smith/README.md
@@ -96,5 +96,207 @@ If we include the g->9 mapping we can have:
```
glossologists 0x 0000 91055 0109 1575 ( 2,551,232,066,033,013)
```
-# Challenge 2 - Z-diff
+# Challenge 2 - k-diff
+## The solution
+
+I approached this in a few different ways:
+
+### Getting the filenames...
+
+We can:
+ 1) Cheat - and create a hash of arrays;
+ 2) Use `opendir`/`readdir`/`closedir`;
+ 3) Use `blob '*/*'`;
+ 4) Use `<*/*>`.
+
+Each have advantages/disadvantages:
+
+ * the first doesn't require us to create files on disk for testing;
+ * 2 allows you to find hidden file names but then you have to be careful with `.` & `..`. You have to join directory and filename to get the path
+ * 3 & 4 are essentially the same and get all "folders/entries". You have to split the path to get the directory and filename.
+
+The simplest approach is to use 3/4.
+
+```perl
+ my %directories;
+ for my $path ( sort blob '*/*' ) {
+ my( $dir, $file ) = split m{/}, $path;
+ push @{ $directories{$dir} }, -d $path ? "$file/" : $file;
+ }
+```
+
+### Finding a complete list of different filenames
+
+```perl
+ my @paths = sort keys %directories;
+ my %filename_counts;
+ for my $path ( @paths ) {
+ $filename_counts{ $_ }++ for @{$directories{ $path }};
+ }
+```
+
+### Build templates for printing page horizontal line, sprintf table of heading/contents
+
+```perl
+ my $HORIZONTAL_LINE = join '-' x ( $length+2 ), ('+') x (1+@paths);
+ my $TEMPLATE = '|' . " %-${length}s |" x @paths;
+
+ say $HORIZONTAL_LINE;
+ say sprintf $TEMPLATE, @paths;
+ say $HORIZONTAL_LINE;
+```
+
+### Workout what to print and printing
+
+``perl
+ ## map({$u{$F=$_}<@p?sprintf$T,map{($d{$_}[0]//'')ne$F?'':
+ ## shift@{$d{$_}}}@p:map{shift@{$d{$_}};()}@p}sort keys%u)
+ for my $filename ( sort keys %filename_counts ) {
+ if( $filename_counts{ $filename } == @paths ) {
+ shift @{$_} for values %directories;
+ next;
+ }
+ my @columns;
+ for (@paths) {
+ if( @{$directories{$_}} && $directories{$_}[0] eq $filename ) {
+ push @columns, shift @{$directories{$_}};
+ } else {
+ push @columns, '';
+ }
+ }
+ say sprintf $TEMPLATE, @columns;
+ }
+```
+
+## Just a minor block at the end
+say $1
+## The full code (with comments)
+
+```perl
+sub z_diff {
+
+ ## Declare variables
+
+ ## my($l,%d,$F,@p,%u,$T,$H)=0;
+ my( $length, %directories, %filename_counts )=0;
+
+ ## Read all sub-directories containing files and store
+ ## in data structure as a hash of ordered arrays
+ ## For directories add a trailing slash...
+
+ ## (@_=split/\//),push@{$d{$_[0]}},-d$_?"$_[1]/":$_[1]for sort<*/*>;
+ for my $pat ( sort blob '*/*' ) {
+ my( $dir, $file ) = split m{/}, $path;
+ push @{ $directories{$dir} }, -d $path ? "$file/" : $file;
+ }
+
+ ## Get out an ordered list of directories...
+
+ ## $u{$_}++for map{@{$d{$_}}}@p=sort keys%d;
+ my @paths = sort keys %directories;
+
+ ## Find the length of the longest directory name, and
+ ## keep a record of the number of times each filename
+ ## has been seen (also gives list of all unique filenames)
+
+ for my $path ( @paths ) {
+ $filename_counts{ $_ }++ for @{$directories{$path}};
+ }
+
+ ## Now find the length of the longest filename or directory name
+ ## This gives us the column widths for the pretty display...
+
+ ## (length$_>$l)&&($l=length$_)for@p,keys%u;
+ for ( @paths, keys %filename_counts ) {
+ $length = length $_ if length $_ > $length;
+ }
+
+ ## Generate the ASCII code for a horizontal bar in the table
+ ## and a template for sprintf for the rows of the table
+
+ my $HORIZONTAL_LINE = join '-' x ( $length+2 ), ('+') x (1+@paths);
+ my $TEMPLATE = '|' . " %-${length}s |" x @paths;
+
+ ## Draw the header {directory names} of the table....
+
+ ## say for$H=join('-'x($l+2),('+')x(1+@p)),
+ ## sprintf($T='|'." %-${l}s |"x@p,@p),$H,
+ say $HORIZONTAL_LINE;
+ say sprintf $TEMPLATE, @paths;
+ say $HORIZONTAL_LINE;
+
+ ## Now draw the body - we loop through each of the unique filenames
+ ## and see whether it is in all 4 columns (in which case we skip)
+ ## otherwise we look to see which entries are present in each
+ ## directory and show those....
+
+ ## map({$u{$F=$_}<@p?sprintf$T,map{($d{$_}[0]//'')ne$F?'':
+ ## shift@{$d{$_}}}@p:map{shift@{$d{$_}};()}@p}sort keys%u)
+ for my $filename ( sort keys %filename_counts ) {
+
+ ## If we have seen the file in all directories - we remove
+ ## it from all directory lists {and do nothing}
+
+ if( $filename_counts{ $filename } == @paths ) {
+ shift @{$_} for values %directories;
+ next;
+ }
+
+ ## If we haven't we loop through the rows if
+ ## the first entry is the file then we push it
+ ## on the list to print {and remove it from the directory list}
+ ## if not we just push an empty string to the
+ ## list
+
+ my @columns;
+ for (@paths) {
+ if( @{$directories{$_}} && $directories{$_}[0] eq $filename ) {
+ push @columns, shift @{$directories{$_}};
+ } else {
+ push @columns, '';
+ }
+ }
+ say sprintf $TEMPLATE, @columns;
+ }
+
+ ## Finally print out the bottom line
+
+ ## $H
+ say $HORIZONTAL_LINE;
+}
+```
+## Obfurscated/Golf code...
+
+I started with a "simple" compact version of the code and then came
+discussions with Eliza on the Perl Programmers Facebook group and things
+slowly got smaller. A few bytes at a time...
+
+```perl
+sub z{my($l,$F,%d,%u,$T,$H)=0;(@_=split'/'),push@{$d{$_[0]}},-d?"$_[1]/":$_[1]
+for<*/*>;$u{$_}++for map{@{$d{$_}}}my@p=sort keys%d;$l<length?$l=length:1for@p,
+@_=keys%u;say for$H=join('-'x($l+2),('+')x(1+@p)),sprintf($T="| %-${l}s "x@p.'|'
+,@p),$H,map({$u{$F=$_}<@p?sprintf$T,map{($d{$_}[0]//'')ne$F?'':shift@{$d{$_}}}@p
+:map{shift@{$d{$_}};()}@p}sort@_),$H}
+```
+
+**or** if we "allow" return characters inside strings - this is 351 bytes of
+perly goodness...
+
+```perl
+sub x{my($l,$F,%d,%u,$T,$H)=0;(@_=split'/'),push@{$d{$_[0]}},-d?"$_[1]/":$_[1]
+for<*/*>;$u{$_}++for map{@{$d{$_}}}my@p=sort keys%d;$l<length?$l=length:1for@p,
+@_=keys%u;say$H=join('-'x($l+2),('+')x@p,'+
+'),sprintf($T="| %-${l}s "x@p.'|
+',@p),$H,map({$u{$F=$_}<@p?sprintf$T,map{($d{$_}[0]//'')ne$F?'':shift@{$d{$_}}
+}@p:map{shift@{$d{$_}};()}@p}sort@_),$H}
+```
+
+**Notes**
+ - Where we use `$_` there are numerous function calls which use this when no
+ parameter is passed - in this case `length`, `split`, `-d`.
+ - When a "string" starts with a number than has a letter in it treats it as if
+ to add a space between the number and the rest of the string so we can rewrite
+ `1 for $condition` as `1for$condition`.
+ - we don't need to do `sort blob '*/*'` or `sort <*/*>` as for all "current"
+ versions of Perl we can assume that perl does this for us.