aboutsummaryrefslogtreecommitdiff
path: root/challenge-219/matthias-muth/perl
diff options
context:
space:
mode:
authorMatthias Muth <matthias.muth@gmx.de>2023-06-05 00:44:19 +0200
committerMatthias Muth <matthias.muth@gmx.de>2023-06-05 00:44:19 +0200
commitd054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e (patch)
treecd2605a781de98686a08004fda8df008fadf8202 /challenge-219/matthias-muth/perl
parent27b599557a1a4cb031a9550f81bbcb0982e15327 (diff)
downloadperlweeklychallenge-club-d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e.tar.gz
perlweeklychallenge-club-d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e.tar.bz2
perlweeklychallenge-club-d054aa235c76549c8cbe6d4c8e7cb4dd2c2d6e8e.zip
Challenge 219 solutions in Perl by Matthias Muth
Diffstat (limited to 'challenge-219/matthias-muth/perl')
-rw-r--r--challenge-219/matthias-muth/perl/TestExtractor.pm215
-rwxr-xr-xchallenge-219/matthias-muth/perl/ch-1.pl22
-rwxr-xr-xchallenge-219/matthias-muth/perl/ch-2.pl36
-rw-r--r--challenge-219/matthias-muth/perl/challenge-219.txt63
4 files changed, 336 insertions, 0 deletions
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.