diff options
| author | Matthias Muth <matthias.muth@gmx.de> | 2023-06-05 00:44:19 +0200 |
|---|---|---|
| committer | Matthias Muth <matthias.muth@gmx.de> | 2023-06-05 00:44:19 +0200 |
| commit | d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e (patch) | |
| tree | cd2605a781de98686a08004fda8df008fadf8202 | |
| parent | 27b599557a1a4cb031a9550f81bbcb0982e15327 (diff) | |
| download | perlweeklychallenge-club-d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e.tar.gz perlweeklychallenge-club-d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e.tar.bz2 perlweeklychallenge-club-d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e.zip | |
Challenge 219 solutions in Perl by Matthias Muth
| -rw-r--r-- | challenge-219/matthias-muth/README.md | 244 | ||||
| -rw-r--r-- | challenge-219/matthias-muth/blog.txt | 1 | ||||
| -rw-r--r-- | challenge-219/matthias-muth/perl/TestExtractor.pm | 215 | ||||
| -rwxr-xr-x | challenge-219/matthias-muth/perl/ch-1.pl | 22 | ||||
| -rwxr-xr-x | challenge-219/matthias-muth/perl/ch-2.pl | 36 | ||||
| -rw-r--r-- | challenge-219/matthias-muth/perl/challenge-219.txt | 63 |
6 files changed, 444 insertions, 137 deletions
diff --git a/challenge-219/matthias-muth/README.md b/challenge-219/matthias-muth/README.md index a8034fc57d..7ff542ce5e 100644 --- a/challenge-219/matthias-muth/README.md +++ b/challenge-219/matthias-muth/README.md @@ -1,168 +1,138 @@ -# Highscore! -**Challenge 218 solutions in Perl by Matthias Muth** +# This is Perl! We got this, no problem! +**Challenge 219 solutions in Perl by Matthias Muth** -## Task 1: Maximum Product +This week's challenges confirm to me why I love Perl:<br/> +Using what the language offers, the idea how to implement it transforms directly into code.<br/> +Evrything works well together, and there is no unnecessary clutter. -> You are given a list of 3 or more integers.<br/> -> Write a script to find the 3 integers whose product is the maximum and return it.<br/> +## Task 1: Sorted Squares -If we have exactly three numbers, there's not a lot of choice.<br/> -We return the product of the three numbers that we have got: -```perl - use List::Util qw( product ); - - return product( @list ) - if @list == 3; -``` - -We need to consider that we might have negative numbers in the list, -and there might be situations where we cannot avoid ending up with a negative product. -But, of course, *any* positive product is better that *any* negative one!<br/> -So we first try to find the largest possible *positive* product using three numbers from the list.<br/> +> You are given a list of numbers.<br/> +> Write a script to square each number in the list and return the sorted list, increasing order.<br/> -The largest positive productmy be wither of these: -* the product of the three largest positive numbers, -* the product of the two negatives with the highest absolute value, and the largest positive number<br/> -(the two negatives turning the result into positive). +This one ends up in a typical Perl one-liner.<br/> +The subroutine's input list of numbers (`@_`) is used to square all its entries using a `map` call +(I used the 'code block' variant here), and the resulting list is sorted, +also using a 'code block' that compares two values numerically to determine their order in the sorted output.<br/> +Perl's `<=>` operator comes in handy to tell whether the fist operand +goes left (`-1`) or right (`+1`) or the two operands are equal (`0`). -We cannot say in advance which of these will be better, -because this depends on the absolute values of the positive and negative 'best' numbers, -So, after splitting up our list into positives and negatives, -sorted by highest to lowest absolute values, -we compute them both (if possible), -and then return the better one, if at lest one of them could be produced: +More explanation than code! :-) ```perl - my @pos = sort { abs( $b ) <=> abs( $a ) } grep $_ > 0, @list; - my @neg = sort { abs( $b ) <=> abs( $a ) } grep $_ < 0, @list; - - my @positive_results = ( - @pos >= 3 ? product( @pos[0..2] ) : (), - @pos >= 1 && @neg >= 2 ? product( @neg[0..1], $pos[0] ) : (), - ); - - # If we have at least one of them, we are done. - return max( @positive_results ) - if @positive_results; +sub sorted_squares { + return sort { $a <=> $b } map { $_ ** 2 }, @_; +} ``` -If it was not possible to return a positive result, -we should at least try to limit our outcome to zero.<br/> -We can return a zero result if we have at least one zero in the list, -because no matter which other numbers we multiply it with, it still remains a zero: - +## Task 2: Travel Expenditure + +> You are given two list, @costs and @days.<br/> +> The list @costs contains the cost of three different types of travel cards you can buy.<br/> +> For example @costs = (5, 30, 90)<br/> +> Index 0 element represent the cost of 1 day travel card.<br/> +> Index 1 element represent the cost of 7 days travel card.<br/> +> Index 2 element represent the cost of 30 days travel card.<br/> +> <br/> +> The list @days contains the day number you want to travel in the year.<br/> +> For example: @days = (1, 3, 4, 5, 6)<br/> +> The above example means you want to travel on day 1, day 3, day 4, day 5 and day 6 of the year.<br/> +> <br/> +> Write a script to find the minimum travel cost.<br/> + +Let's take the first day that we want to travel.<br/> +On that day, we have three options to take a travel card. +Depending on which option we take, we +* spend some money for buying that card, +* can possibly use that card for some more days on which we want to travel. + +After the travel pass period is over, we need to decide again which of the options will be cheaper then +for the rest of the list. + +So we are looking at a problem here that should be easy to solve by recursion: +* Apply one of the three options, +* use the same problem solving routine to determine the 'best' solution for the rest of the input list that is not yet covered. + +Do this for all of the options that we have, and then take the best one. + +Our recursive subroutine has two parameters:<br/> +a list of costs (which will always have three elements in this challenge), +and a list of days to find the best combination of travel passes for: ```perl - return 0 - if grep $_ == 0, @list; +sub travel_expenditure { + my ( $costs, $days ) = @_; + ... +} ``` +and it will return the minimum cost for travelling on the days given as parameters. -If this didn't work either, we are set to return a negative result. - -At this point we are sure that we have at least three negative numbers, -because we know that -* we have at least 4 numbers in total<br/> - (because there are >= 3 by definition, and we checked for having exactly three already), -* we have at most 2 positives<br/> - (because we ruled out having 3 positives), -* we don't have any zeroes<br/>, - (we just checked). - -So we have at least 2 negatives.<br/> -From that we can follow that -* we don't even have 1 positive - (because we would have combined it with the two negatives for a positive result). - -So actually we have *only* negative numbers, and at least 4 of them. - -We choose the ones with the lowest absolute value, giving us the highest -(least negative) result possible.<br/> -To get those, we just reverse the list that we already have, -which is equivalent to sorting then into ascending order, -and then take the first three entries. +Any function that is being called recursively has to have an end criteria.<br/> +In our case, we return from the subroutine directly if there are no days in the list. +Of course, we then don't need to buy any travel pass at all, and the cost is zero. ```perl - return product( ( reverse @neg )[0..2] ); + return 0 + if @$days == 0; ``` -And we are done. - -## Task 2: Matrix Score -> You are given a m x n binary matrix i.e. having only 1 and 0.<br/> -> You are allowed to make as many moves as you want to get the highest score.<br/> -> A move can be either toggling each value in a row or column.<br/> -> To get the score, convert the each row binary to dec and return the sum.<br/> +For finding the minimum cost for the options we have for the first day, +I could have written out the three cases explicitly, because the challenge description defines +exactly three travel passes.<br/> +This would be easy to read and understand. I hear: +'Aha! Three very similar lines of code, that clearly corresponds to the three travel passes!'. -So we want to get a final sum that is as high as possible.<br/> -Then let's have a look at what makes up that sum: +Nevertheless I decided to use `map` instead of writing out the three cases explicitly. -In the binary matrix like in Example 1, -``` - [ 0 0 1 1 ] - [ 1 0 1 0 ] - [ 1 1 0 0 ] -``` -we will convert the rows from binary to decimal.<br/> -This means that every bit will be multiplied by a 2<sup>n</sup> value: -``` - [ 8 4 2 1 ] -``` +This is mainly because I don't like repetitions.<br/> +But then also because by abstracting into a loop, the solution 'scales' better:<br/> +I want to be ready for a 365 day travel pass in the next challenge! :-) -To get as high as possible a sum, all the bits with a value of `8` should be set to `1`. -No matter what the other bits in the same row are, -a value of `8` is more than all the other bits can achieve together (a maximum of 7). - -So in the first step, we flip all rows that have a `0` in the first position: +So we create an array with the durations of the (three ;-)) travel passes, +to use them in whatever loop or `map` we will use: ```perl - for my $row ( grep $_->[0] == 0, @$matrix ) { - $row->[$_] ^= 1 - for 0..$#$row; - } +my @durations = ( 1, 7, 30 ); ``` -In the second step, we go through all columns but the first one, -and flip all those that have less `1`s that `0`s.<br/> -This maximizes the sum of values that we get from each column. -```perl - my $n_columns = scalar @{$matrix->[0]}; - for my $c ( 1 .. $n_columns - 1 ) { - # Sum up that column's value from every row. - my $column_sum = sum( map $_->[$c], @$matrix ); - if ( $column_sum < $n_columns / 2 ) { - # Flip that column in every row. - $_->[$c] ^= 1 - for @$matrix; - } - } -``` +Then we can 'map' the travel pass numbers (0,1,2) to the respective costs, +determined by the cost for the travel pass itself and the cost for the remaining days that are not covered, +determined by the recursive call. -This should give us a good matrix. +For the list of remaining days, we `grep` those days that are later than the current travel pass's duration, +put them into an anonymous array, and use that as the parameter. -Now we need to return the sum of all rows converted from binary to decimal.<br/> -It requires a little thinking how to do this nicely, without doing all the for-loops -and all the multiplication with the 2<sup>n</sup> values, but everything is already there in perl! +Here is the only glitch:<br/> +The `$_` loop variable in the `map` is the number of the travel pass that we currently try. +Within the `grep`, however, `$_` goes through the list of days. +So if we want to use the current travel pass's duration within the `grep`condition, +we can't use `$durations[$_]` there. +We have to store the duration in a `my $duration` variable before, +and use that one within the grep condition. -My solution for the conversion of one row is this one:<br/> -Looking for a `pack` format that makes the conversion easy, -I choose the 'b32' format to get 4 bytes of data from a maximum of 32 column values.<br/> -This format uses the least significant bit first, and assumes `0` bits when it runs out of values. -This is good, because we don't need do anything to fill up our data to any word boundaries.<br/> -We just need to `reverse` our bit array to have the least significant value first, -and to turn it into a string `0` or `1` characters for each bit value, before passing it to `pack`. +As the overall +result, we return the minimum of the list of costs returned from the `map` call, +using the `min` function from `List::Util` that we include outside of the function. -The resulting `pack` output byte-string matches this description: -> An unsigned long (32-bit) in "VAX" (little-endian) order. +So here is the full implementation: -which means that we can `unpack` it into a numerical value using `unpack`'s `V` format.<br/> -So my function to turn a list of binary values into a decimal value is this: ```perl -sub binary_list_to_integer { - return unpack( "V", pack( "b32", join( "", reverse @_ ) ) ); +use List::Util qw( min ); + +my @durations = ( 1, 7, 30 ); + +sub travel_expenditure { + my ( $costs, $days ) = @_; + return 0 + if @$days == 0; + return min( + map { + my $duration = $durations[$_]; + $costs->[$_] + + travel_expenditure( $costs, + [ grep $_ >= $days->[0] + $duration, @$days ] ); + } 0..$#{$costs} + ); } ``` -And using that function we return the 'matrix score' end result like this: -```perl - return sum( map binary_list_to_integer( @$_ ), @$matrix ); -``` +This was fun! -**Thank you for the challenge!** - +#### **Thank you for the challenge!** diff --git a/challenge-219/matthias-muth/blog.txt b/challenge-219/matthias-muth/blog.txt new file mode 100644 index 0000000000..1eaa31fe0f --- /dev/null +++ b/challenge-219/matthias-muth/blog.txt @@ -0,0 +1 @@ +https://github.com/MatthiasMuth/perlweeklychallenge-club/tree/muthm-219/challenge-219/matthias-muth#readme diff --git a/challenge-219/matthias-muth/perl/TestExtractor.pm b/challenge-219/matthias-muth/perl/TestExtractor.pm new file mode 100644 index 0000000000..dce3669930 --- /dev/null +++ b/challenge-219/matthias-muth/perl/TestExtractor.pm @@ -0,0 +1,215 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# The Test Data Extraction Machine (tm). +# +# Perl solution by Matthias Muth. +# + +use strict; +use warnings; +use feature 'say'; +use feature 'signatures'; +no warnings 'experimental::signatures'; + +package TestExtractor; +use Exporter 'import'; +our @EXPORT = qw( run_tests $verbose %options vsay pp ); + +use Data::Dump qw( pp ); +use Getopt::Long; +use Cwd qw( abs_path ); +use File::Basename; +use Test2::V0; +no warnings 'experimental::signatures'; + +our ( $verbose, %options ); +sub vsay { say @_ if $verbose }; + +sub run_tests { + + $| = 1; + + GetOptions( + "v|verbose!" => \$verbose, + ) or do { say "usage!"; exit 2 }; + + my $dir = dirname abs_path $0; + my ( $challenge, $task ) = + abs_path( $0 ) =~ m{challenge-(\d+) .* (\d+)[^[/\\]*$}x; + unless ( $challenge && $task ) { + say STDERR "ERROR: ", + "Cannot determine challenge number or task number. Exiting."; + exit 1; + } + + my $local_tests; + ( undef, $local_tests ) = read_task( *::DATA ) + if fileno *::DATA; + + my ( $task_title, $task_description ) = + read_task( "$dir/challenge-${challenge}.txt", $task ); + vsay $task_title; + + my @tests = ( + $local_tests ? extract_tests( $local_tests ) : (), + $task_description ? extract_tests( $task_description ) : (), + ); + vsay pp( @tests ); + + ( my $sub_name = lc $task_title ) =~ s/\W+/_/g; + my $sub = \&{"::$sub_name"}; + + do { + my @input_params = + @{$_->{INPUT}} == 1 + ? ( ref $_->{INPUT}[0] eq 'ARRAY' + && ! grep( ref $_, @{$_->{INPUT}[0]} ) ) + ? @{$_->{INPUT}[0]} + : $_->{INPUT}[0] + : @{$_->{INPUT}}; + my $expected = $_->{OUTPUT}; + my $diag = + "$sub_name( " . pp( @input_params ) . " ) == " + . pp( @{$_->{OUTPUT}} ); + # . pp( + # @{$_->{OUTPUT}} == 1 && ref $_->{OUTPUT}[0] eq 'ARRAY' && + # ? @{$_->{OUTPUT}} + # : $_->{OUTPUT} ); + + my $name = "$_->{TEST}"; + $name .= ": $diag" + if $_->{TEST} =~ /^(Test|Example)\s+\d+$/; + $diag = "test: $diag"; + + my @output = $sub->( @input_params ); + + is \@output, $expected, $name, $diag // (); + + vsay ""; + + } for @tests; + + done_testing; +} + +sub read_task( $fd_or_filename, $wanted_task = undef ) { + + my $fd; + if ( ref \$fd_or_filename eq 'SCALAR' ) { + open $fd, "<", $fd_or_filename + or die "ERROR: cannot open '$fd_or_filename': $!\n"; + } + else { + # non-SCALARs, like __DATA__ GLOB. + $fd = $fd_or_filename; + } + + my ( $task, $task_title, $task_text ) = ( -1, undef ); + while ( <$fd> ) { + /^Task (\d+):\s*(.*?)\s*$/ and do { + $task = $1; + $task_title = $2 + if $wanted_task && $task == $wanted_task; + next; + }; + + next + if $wanted_task && $task != $wanted_task; + + $task_text .= $_; + } + + return $task_title, $task_text; +} + +sub extract_tests( $task_text ) { + # vsay "extract_tests( ", pp( $task_text ), " )"; + + # These regular expressions are used for extracting input or output + # test data. + my $var_name = qr/ [\@\$]\w+ /x; + my $literal = qr/ ".*?" | '.*?' | [+-]?\d+ | undef /x; + my $bracketed = qr/ \[ [^\[]*? \] /xs; + my $parenthesized = qr/ \( [^\[]*? \) /xs; + my $entry = qr/ $literal | $bracketed | $parenthesized /x; + my $list = qr/ $entry (?: \s*,\s* $entry )* \s*,? /xs; + + # The combination of what we expect as input or output data. + # Capture unparenthesized lists for special handling. + my $data_re = qr/ (?<lit> $literal ) + | (?<br_list> \[ \s* (?:$list)? \s* \] ) + | (?<par_list> \( \s* (?:$list)? \s* \) ) + | (?<no_paren> $list ) /x; + + my @tests; + while ( $task_text =~ + /^((?:Example|Test).*?)\s*:?\s*$ .*? + ^Input: \s* ( .*? ) \s* + ^Output: \s* ( .*? ) \s*? (?=(?: ^$ | ^\S | \Z )) + /xmsg ) + { + my ( $test, $input, $output) = ( $1, $2, $3 ); + + push @tests, { TEST => $test }; + + for ( $input, $output ) { + # To avoid misinterpretations of '@' or '$' when the data is + # 'eval'ed, we turn all double quotes into single quotes. + s/"/'/g; + + # We convert 'barewords' into quoted strings. + # We search for these patterns, but we just skip them without + # changing them: + # * 'Input:', 'Output:' at the beginning of the string, + # * quoted strings, + # * variable names having a $ or @ sigil. + # After we are sure it's none of those, we also check unquoted + # 'barewords' (here: combinations of letters, digits or underscores, + # starting with a letter) and enclose them in single quotes. + my $bareword = qr/ \b (?!undef) [a-z_][a-z0-9_]* \b /ix; + while ( / ^Input: | ^Output: | '.*?' | [\$\@]$bareword + | ( $bareword ) /xg ) + { + if ( $1 ) { + my $p = pos(); + substr $_, $p - length( $1 ), length( $1 ), "'$1'"; + pos = $p + 2; + } + } + + # As all arrays will be stored as array references, so we just + # convert parentheses (...) to angle brackets [...]. + # s/\(/\[/g; + # s/\)/\]/g; + + # Add missing commas between literals. + while ( s/($literal)\s+($literal)/$1, $2/ ) {} + } + + while ( $input =~ / ($var_name) \s* = \s* ($data_re) /xg ) { + push @{$tests[-1]{VARIABLE_NAMES}}, $1; + push @{$tests[-1]{INPUT}}, + eval( ( $+{no_paren} || $+{par_list} ) ? "[ $2 ]" : $2 ); + }; + + while ( $output =~ /^\s* ($data_re) $/xg ) { + local $_ = $1; + # vsay "\$_: <$_>"; + # Special case: (1,2),(3,4),(5,6) + # should become: [1,2],[3,4],[5,6] ] + if ( $+{no_paren} && /$parenthesized/ ) { + # vsay "found special case <$_>"; + s/\(/\[/g; + s/\)/\]/g; + } + push @{$tests[-1]{OUTPUT}}, + eval( $+{no_paren} ? "( $_ )" : $_ ); + }; + } + return @tests; +} + +1; diff --git a/challenge-219/matthias-muth/perl/ch-1.pl b/challenge-219/matthias-muth/perl/ch-1.pl new file mode 100755 index 0000000000..4d243b0b0b --- /dev/null +++ b/challenge-219/matthias-muth/perl/ch-1.pl @@ -0,0 +1,22 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# Challenge 219 Task 1: Sorted Squares +# +# Perl solution by Matthias Muth. +# + +use strict; +use warnings; +use feature 'say'; + +use lib '.'; +use TestExtractor; + +sub sorted_squares { + return sort { $a <=> $b } map { $_ ** 2 } @_; +} + +run_tests; diff --git a/challenge-219/matthias-muth/perl/ch-2.pl b/challenge-219/matthias-muth/perl/ch-2.pl new file mode 100755 index 0000000000..7b3234b92d --- /dev/null +++ b/challenge-219/matthias-muth/perl/ch-2.pl @@ -0,0 +1,36 @@ +#!/usr/bin/env perl +# +# The Weekly Challenge - Perl & Raku +# (https://theweeklychallenge.org) +# +# Challenge 219 Task 2: Travel Expenditure +# +# Perl solution by Matthias Muth. +# + +use strict; +use warnings; +use feature 'say'; + +use lib '.'; +use TestExtractor; + +use List::Util qw( min ); + +my @durations = ( 1, 7, 30 ); + +sub travel_expenditure { + my ( $costs, $days ) = @_; + return 0 + if @$days == 0; + return min( + map { + my $duration = $durations[$_]; + $costs->[$_] + + travel_expenditure( $costs, + [ grep $_ >= $days->[0] + $duration, @$days ] ); + } 0..$#{$costs} + ); +} + +run_tests; diff --git a/challenge-219/matthias-muth/perl/challenge-219.txt b/challenge-219/matthias-muth/perl/challenge-219.txt new file mode 100644 index 0000000000..158764bca9 --- /dev/null +++ b/challenge-219/matthias-muth/perl/challenge-219.txt @@ -0,0 +1,63 @@ +The Weekly Challenge - 219 +Monday, May 29, 2023 + + +Task 1: Sorted Squares +Submitted by: Mohammad S Anwar + +You are given a list of numbers. +Write a script to square each number in the list and return the sorted list, increasing order. +Example 1 + +Input: @list = (-2, -1, 0, 3, 4) +Output: (0, 1, 4, 9, 16) + +Example 2 + +Input: @list = (5, -4, -1, 3, 6) +Output: (1, 9, 16, 25, 36) + + +Task 2: Travel Expenditure +Submitted by: Mohammad S Anwar + +You are given two list, @costs and @days. +The list @costs contains the cost of three different types of travel cards you can buy. +For example @costs = (5, 30, 90) +Index 0 element represent the cost of 1 day travel card. +Index 1 element represent the cost of 7 days travel card. +Index 2 element represent the cost of 30 days travel card. + +The list @days contains the day number you want to travel in the year. +For example: @days = (1, 3, 4, 5, 6) +The above example means you want to travel on day 1, day 3, day 4, day 5 and day 6 of the year. + +Write a script to find the minimum travel cost. +Example 1: + +Input: @costs = (2, 7, 25) + @days = (1, 5, 6, 7, 9, 15) +Output: 11 + +On day 1, we buy a one day pass for 2 which would cover the day 1. +On day 5, we buy seven days pass for 7 which would cover days 5 - 9. +On day 15, we buy a one day pass for 2 which would cover the day 15. + +So the total cost is 2 + 7 + 2 => 11. + +Example 2: + +Input: @costs = (2, 7, 25) + @days = (1, 2, 3, 5, 7, 10, 11, 12, 14, 20, 30, 31) +Output: 20 + +On day 1, we buy a seven days pass for 7 which would cover days 1 - 7. +On day 10, we buy a seven days pass for 7 which would cover days 10 - 14. +On day 20, we buy a one day pass for 2 which would cover day 20. +On day 30, we buy a one day pass for 2 which would cover day 30. +On day 31, we buy a one day pass for 2 which would cover day 31. + +So the total cost is 7 + 7 + 2 + 2 + 2 => 20. + + +Last date to submit the solution 23:59 (UK Time) Sunday 4th June 2023. |
