aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--challenge-326/matthias-muth/README.md229
-rw-r--r--challenge-326/matthias-muth/blog.txt1
-rwxr-xr-xchallenge-326/matthias-muth/perl/ch-1.pl28
-rwxr-xr-xchallenge-326/matthias-muth/perl/ch-2.pl76
4 files changed, 233 insertions, 101 deletions
diff --git a/challenge-326/matthias-muth/README.md b/challenge-326/matthias-muth/README.md
index 75d3c14653..9d30f485fe 100644
--- a/challenge-326/matthias-muth/README.md
+++ b/challenge-326/matthias-muth/README.md
@@ -1,149 +1,176 @@
-# Consecutive, but Maybe Not Final
+# Modern Perl's Victory
-**Challenge 325 solutions in Perl by Matthias Muth**
+**Challenge 326 solutions in Perl by Matthias Muth**
-## Task 1: Consecutive One
+## Task 1: Day of the Year
-> You are given a binary array containing only 0 or/and 1.<br/>
-> Write a script to find out the maximum consecutive 1 in the given array.
+> You are given a date in the format YYYY-MM-DD.<br/>
+> Write a script to find day number of the year that the given date represent.
>
> **Example 1**
>
> ```text
-> Input: @binary = (0, 1, 1, 0, 1, 1, 1)
-> Output: 3
->```
->
->**Example 2**
->
->```text
-> Input: @binary = (0, 0, 0, 0)
-> Output: 0
+> Input: $date = '2025-02-02'
+> Output: 33
+>The 2nd Feb, 2025 is the 33rd day of the year.
> ```
>
-> **Example 3**
+> **Example 2**
>
> ```text
->Input: @binary = (1, 0, 1, 0, 1, 1)
-> Output: 2
+>Input: $date = '2025-04-10'
+> Output: 100
+> ```
+>
+>**Example 3**
+>
+>```text
+> Input: $date = '2025-09-07'
+>Output: 250
> ```
-For this task, first thing I do is to walk through the array and assign to each element the number of `1`s that have been seen since the last `0`. Like this:
-
-```text
-@binary: 0 1 1 0 1 1 1
- | | | | | | |
-# of ones: 0 1 2 0 1 2 3
-```
-
-To do that, I need to carry over the current number of `1`s from position to position. This makes it a perfect use case for the `reductions` function (from `List::Util`). In its code block, the `$a` variable is the result for the previous element, and `$b` is the current element. That makes this type of 'conditional counting' very easy:
-
- ```perl
- reductions { $b ? ++$a : 0 } @binary
- ```
+It's good to have our superb set of on-board tools — the core modules. For this task, `Time::Piece` helps us to create a one-line solution.
-Now it's very simple to find the maximum number of consecutive `1`s: just apply the `max` function on the result.
+The dates we receive as input are ISO date strings. The `strptime` function can easily convert that into a `Time::Piece` object using the `'%F'` format. We can then use the `day_of_year` method (or `yday` for short) to return the day of the year. We just need to add 1 because the result is zero-based, and we want the day numbered from 1 for January 1st.
-Which makes this tasks solution a very nice and simple one-line-of-code function:
+As simple as this:
```perl
-sub consecutive_one( @binary ) {
- return max( reductions { $b ? ++$a : 0 } @binary );
+use v5.36;
+use Time::Piece;
+
+sub day_of_the_year( $date ) {
+ return Time::Piece->strptime( $date, "%F" )->day_of_year + 1;
}
```
-## Task 2: Final Price
+## Task 2: Decompressed List
-> You are given an array of item prices.<br/>
-> Write a script to find out the final price of each items in the given array.<br/>
-> There is a special discount scheme going on. If there’s an item with a lower or equal price later in the list, you get a discount equal to that later price (the first one you find in order).
+> You are given an array of positive integers having even elements.<br/>
+> Write a script to to return the decompress list. To decompress, pick adjacent pair (i, j) and replace it with j, i times.
>
> **Example 1**
>
> ```text
-> Input: @prices = (8, 4, 6, 2, 3)
-> Output: (4, 2, 4, 2, 3)
->
-> Item 0:
-> The item price is 8.
-> The first time that has price <= current item price is 4.
-> Final price = 8 - 4 => 4
->
-> Item 1:
-> The item price is 4.
-> The first time that has price <= current item price is 2.
-> Final price = 4 - 2 => 2
+> Input: @ints = (1, 3, 2, 4)
+> Output: (3, 4, 4)
>
-> Item 2:
-> The item price is 6.
-> The first time that has price <= current item price is 2.
-> Final price = 6 - 2 => 4
->
-> Item 3:
-> The item price is 2.
-> No item has price <= current item price, no discount.
-> Final price = 2
->
-> Item 4:
-> The item price is 3.
-> Since it is the last item, so no discount.
-> Final price = 3
+> Pair 1: (1, 3) => 3 one time => (3)
+> Pair 2: (2, 4) => 4 two times => (4, 4)
>```
>
>**Example 2**
>
>```text
-> Input: @prices = (1, 2, 3, 4, 5)
-> Output: (1, 2, 3, 4, 5)
+> Input: @ints = (1, 1, 2, 2)
+> Output: (1, 2, 2)
+>
+>Pair 1: (1, 1) => 1 one time => (1)
+> Pair 2: (2, 2) => 2 two times => (2, 2)
> ```
>
> **Example 3**
>
> ```text
->Input: @prices = (7, 1, 1, 5)
-> Output: (6, 0, 1, 5)
->
-> Item 0:
->The item price is 7.
-> The first time that has price <= current item price is 1.
-> Final price = 7 - 1 => 6
+>Input: @ints = (3, 1, 3, 2)
+> Output: (1, 1, 1, 2, 2, 2)
>
-> Item 1:
->The item price is 1.
-> The first time that has price <= current item price is 1.
-> Final price = 1 - 1 => 0
->
-> Item 2:
->The item price is 1.
-> No item has price <= current item price, so no discount.
-> Final price = 1
->
-> Item 3:
->The item price is 5.
-> Since it is the last item, so no discount.
-> Final price = 5
+> Pair 1: (3, 1) => 1 three times => (1, 1, 1)
+>Pair 2: (3, 2) => 2 three times => (2, 2, 2)
> ```
-My solution for this task is short , and simple enough, but I don't really like it.<br/>
-I couldn't come up with a way to avoid the 'almost quadratic' behavior of walking through the whole rest of the list to find a discount for *every* entry in the list, that's why I am not really happy.
+This task is about walking through an array in steps of 2 elements.<br/>
+I will compare three different implementations:
-But this is what I have:
-
-I use `map` to map the item prices from the `@prices` array to the final prices to be returned. The final price is the item price minus a possible discount, and for getting that discount, I use `first` to walk through the rest of the array to find the next item price that is lower than or equal to the current one.
+* 'Conventional' :<br/>
+ Using a C-style `for(;;)` loop to increment the index by 2 for each iteration.<br/>A bit old-style, maybe, and for sure a bit cumbersome that we then need to use the index itself *and* (index + 1) for the pair of elements.
+* 'Elegant':<br/>Using `pairmap` to walk through the elements in pairs makes it a very elegant one-liner.
+* 'Modern':<br/>Since Perl 5.36 you can use a *list* of lexical variables as loop variables.<br/>
+ This makes the loop very intuitive to write (and to read and understand).<br/>And wait to see the benchmark results!
+All my solutions are based on
```perl
use v5.36;
-use List::Util qw( first );
-
-sub final_price( @prices ) {
- return map {
- my $price = $prices[$_];
- my $discount = first { $_ <= $price } @prices[ $_ + 1 .. $#prices ];
- $price - ( $discount // 0 );
- } keys @prices;
+```
+to get all kinds of good things, like `use strict`, `use warnings`, and subroutine signatures, which I love.<br/>
+And all solutions use the Perl `x` operator to repeat each element the given number of times, like `( <element> ) x <times>`.
+
+##### Conventional (C-style `for` loop)
+
+This is my solution using the C-style, three-statement `for` loop.<br/>
+As we loop over the index, we need to do the index dereferencing ourselves.
+
+```perl
+sub decompressed_list_c_style_for( @ints ) {
+ my @decompressed;
+ for ( my $i = 0; $i <= $#ints; $i += 2 ) {
+ push @decompressed, ( $ints[ $i + 1 ] ) x $ints[ $i ];
+ }
+ return @decompressed;
+}
+```
+
+It works, but it's a bit clumsy.
+
+##### Elegant (`pairmap`)
+
+Using `pairmap`from the `List::Util` core module results in what probably is the shortest and clearest solution:
+
+```perl
+use List::Util qw( pairmap );
+
+sub decompressed_list_pairmap( @ints ) {
+ return pairmap { ( $b ) x $a } @ints;
+}
+```
+
+It assigns each pair of values to the `$a` and `$b` special variables and calls its code block. It then returns the combined list of all code block results.<br/>Very nice!
+
+##### Modern (multi-value `for` loop)
+
+A multi-value `for` loop walks through several elements at once, assigning them to a list of lexical variables (doing aliasing, actually).<br/>We can use those variables directly to build the result. As we use the elements themselves, not the indexes, there's no need to do any dereferencing.
+
+```perl
+sub decompressed_list_multi_value_for( @ints ) {
+ my @decompressed;
+ for my ( $n, $i ) ( @ints ) {
+ push @decompressed, ( $i ) x $n;
+ }
+ return @decompressed;
}
```
-As I said, simple enough, but I guess there must be a better algorithmic solution ...
+Compared to the C-style `for` loop, this saves a lot of writing and can avoid typical typo errors.<br/>And compared to the `pairmap` solution, it does a lot of things efficiently 'under the hood': It avoids the subroutine call with its need to copy parameters, and I guess that error checking and the assignments to the `$a` and `$b` variables are more expensive than aliasing the lexical variables.
+
+Let's see what the benchmark says:
+
+##### Benchmark
+
+I ran a little benchmark:
+
+```perl
+cmpthese -3, {
+ c_style_for => sub { decompressed_list_c_style_for( 3, 1, 3, 2 ); },
+ pairmap => sub { decompressed_list_pairmap( 3, 1, 3, 2 ); },
+ multi_value_for => sub { decompressed_list_multi_value_for( 3, 1, 3, 2 ); },
+};
+```
+
+with results that astonished me:
+
+```text
+ Rate pairmap c_style_for multi_value_for
+pairmap 999261/s -- -22% -41%
+c_style_for 1288603/s 29% -- -23%
+multi_value_for 1679823/s 68% 30% --
+```
+
+The 'modern' solution with the multi-value `for` loop beats all the others!
+
+The `pairmap` solution may be the most beautiful, but it definitely isn't the best performing one.
+
+And there's no good reason at all to use the more complicated conventional C-style `for` loop.
+
+Lessons learned!
#### **Thank you for the challenge!**
diff --git a/challenge-326/matthias-muth/blog.txt b/challenge-326/matthias-muth/blog.txt
new file mode 100644
index 0000000000..e2ead02e82
--- /dev/null
+++ b/challenge-326/matthias-muth/blog.txt
@@ -0,0 +1 @@
+https://github.com/MatthiasMuth/perlweeklychallenge-club/tree/muthm-326/challenge-326/matthias-muth#readme
diff --git a/challenge-326/matthias-muth/perl/ch-1.pl b/challenge-326/matthias-muth/perl/ch-1.pl
new file mode 100755
index 0000000000..0189d14264
--- /dev/null
+++ b/challenge-326/matthias-muth/perl/ch-1.pl
@@ -0,0 +1,28 @@
+#!/usr/bin/env perl
+#
+# The Weekly Challenge - Perl & Raku
+# (https://theweeklychallenge.org)
+#
+# Challenge 326 Task 1: Day of the Year
+#
+# Perl solution by Matthias Muth.
+#
+
+use v5.36;
+
+use Time::Piece;
+
+sub day_of_the_year( $date ) {
+ return Time::Piece->strptime( $date, "%F" )->day_of_year + 1;
+}
+
+use Test2::V0 qw( -no_srand );
+
+is day_of_the_year( "2025-02-02" ), 33,
+ 'Example 1: day_of_the_year( "2025-02-02" ) == 33';
+is day_of_the_year( "2025-04-10" ), 100,
+ 'Example 2: day_of_the_year( "2025-04-10" ) == 100';
+is day_of_the_year( "2025-09-07" ), 250,
+ 'Example 3: day_of_the_year( "2025-09-07" ) == 250';
+
+done_testing;
diff --git a/challenge-326/matthias-muth/perl/ch-2.pl b/challenge-326/matthias-muth/perl/ch-2.pl
new file mode 100755
index 0000000000..f138a62645
--- /dev/null
+++ b/challenge-326/matthias-muth/perl/ch-2.pl
@@ -0,0 +1,76 @@
+#!/usr/bin/env perl
+#
+# The Weekly Challenge - Perl & Raku
+# (https://theweeklychallenge.org)
+#
+# Challenge 326 Task 2: Decompressed List
+#
+# Perl solution by Matthias Muth.
+#
+
+use v5.36;
+
+sub decompressed_list_multi_value_for( @ints ) {
+ my @decompressed;
+ for my ( $n, $i ) ( @ints ) {
+ push @decompressed, ( $i ) x $n;
+ }
+ return @decompressed;
+}
+
+use List::Util qw( pairs pairmap );
+
+sub decompressed_list_pairmap( @ints ) {
+ return pairmap { ( $b ) x $a } @ints;
+}
+
+sub decompressed_list_pairs( @ints ) {
+ return map { ( $_->[1] ) x $_->[0] } pairs @ints;
+}
+
+sub decompressed_list_map_keys( @ints ) {
+ return map {
+ $_ & 1 ? () : ( $ints[ $_ + 1 ] ) x $ints[$_];
+ } keys @ints;
+}
+
+sub decompressed_list_for_keys( @ints ) {
+ my @decompressed;
+ for ( keys @ints ) {
+ push @decompressed, ( $ints[ $_ ] ) x $ints[ $_ - 1 ]
+ if $_ & 0x01;
+ }
+ return @decompressed;
+}
+
+sub decompressed_list_c_style_for( @ints ) {
+ my @decompressed;
+ for ( my $i = 0; $i <= $#ints; $i += 2 ) {
+ push @decompressed, ( $ints[ $i + 1 ] ) x $ints[ $i ];
+ }
+ return @decompressed;
+}
+
+*decompressed_list = \&decompressed_list_c_style_for;
+
+use Test2::V0 qw( -no_srand );
+
+is [ decompressed_list( 1, 3, 2, 4 ) ], [ 3, 4, 4 ],
+ 'Example 1: decompressed_list( 1, 3, 2, 4 ) == (3, 4, 4)';
+is [ decompressed_list( 1, 1, 2, 2 ) ], [ 1, 2, 2 ],
+ 'Example 2: decompressed_list( 1, 1, 2, 2 ) == (1, 2, 2)';
+is [ decompressed_list( 3, 1, 3, 2 ) ], [ 1, 1, 1, 2, 2, 2 ],
+ 'Example 3: decompressed_list( 3, 1, 3, 2 ) == (1, 1, 1, 2, 2, 2)';
+
+done_testing;
+
+use Benchmark qw( cmpthese );
+
+cmpthese -3, {
+ multi_value_for => sub { decompressed_list_multi_value_for( 3, 1, 3, 2 ); },
+ pairmap => sub { decompressed_list_pairmap( 3, 1, 3, 2 ); },
+ # pairs => sub { decompressed_list_pairs( 3, 1, 3, 2 ); },
+ # map_keys => sub { decompressed_list_map_keys( 3, 1, 3, 2 ); },
+ # for_keys => sub { decompressed_list_for_keys( 3, 1, 3, 2 ); },
+ c_style_for => sub { decompressed_list_c_style_for( 3, 1, 3, 2 ); },
+};