aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--challenge-331/matthias-muth/README.md263
-rw-r--r--challenge-331/matthias-muth/blog.txt1
-rwxr-xr-xchallenge-331/matthias-muth/perl/ch-1.pl28
-rwxr-xr-xchallenge-331/matthias-muth/perl/ch-2.pl55
4 files changed, 288 insertions, 59 deletions
diff --git a/challenge-331/matthias-muth/README.md b/challenge-331/matthias-muth/README.md
index 7ca569fa15..3fc0ddbb23 100644
--- a/challenge-331/matthias-muth/README.md
+++ b/challenge-331/matthias-muth/README.md
@@ -1,113 +1,258 @@
-# Capitalizing on Regular Expressions
+# Buddy's Last Word
-**Challenge 330 solutions in Perl by Matthias Muth**
+**Challenge 331 solutions in Perl by Matthias Muth**
-## Task 1: Clear Digits
+## Task 1: Last Word
-> You are given a string containing only lower case English letters and digits.<br/>
-> Write a script to remove all digits by removing the first digit and the closest non-digit character to its left.
+> You are given a string.<br/>
+> Write a script to find the length of last word in the given string.
>
> **Example 1**
>
> ```text
-> Input: $str = "cab12"
-> Output: "c"
->
-> Round 1: remove "1" then "b" => "ca2"
-> Round 2: remove "2" then "a" => "c"
+> Input: $str = "The Weekly Challenge"
+> Output: 9
>```
>
>**Example 2**
>
>```text
-> Input: $str = "xy99"
-> Output: ""
->
->Round 1: remove "9" then "y" => "x9"
-> Round 2: remove "9" then "x" => ""
+> Input: $str = " Hello World "
+> Output: 5
> ```
>
> **Example 3**
>
> ```text
->Input: $str = "pa1erl"
-> Output: "perl"
+>Input: $str = "Let's begin the fun"
+> Output: 3
> ```
-Seems we need to remove pairs of non-digit and digit characters, repeatedly.
-
-Not a big deal for regular expressions.<br/>
-A substitution operator will do the removing, and as the result of that substitution indicates whether a replacement was found or not, it can serve as a loop condition, too.
-
-As there is nothing else to be done, the loop will consist of the loop condition only, with an empty body. I like to put a comment into empty loops to make it obvious for the reader.
+This looks so complicated, and it is so easy...<br/>
+We let
-That
+```perl
+ split " ", $str
+```
+do the separation into words.
+The good thing is that with `" "` as a separator parameter,
+`split` ignores leading and trailing whitespace, so the
+```perl
+ " Hello World "
+```
+example still results in `( "Hello", "world" )`.<br/>
+Then we take the last word of the resulting word list, like
+```perl
+ ( split " ", $str )[-1]
+```
+If we didn't find any words at all, the list will be empty,
+and the `(...)[-1]` will return `undef`.<br/>
+We can use the `//` *defined or* operator to turn that `undef`
+into an empty string,
+so that we can use the `length` function on what we have:
+```perl
+ length( ( split " ", $str )[-1] // "" )
+```
+This makes the solution a one-liner:
```perl
use v5.36;
-sub clear_digits( $str ) {
- while ( $str =~ s/[a-z]\d// ) {
- # Everything is in the loop condition.
- }
- return $str;
+sub last_word_length( $str ) {
+ return length( ( split " ", $str )[-1] // "" );
}
-
```
-## Task 2: Title Capital
+## Task 2: Buddy Strings
-> You are given a string made up of one or more words separated by a single space.<br/>
-> Write a script to capitalise the given title. If the word length is 1 or 2 then convert the word to lowercase otherwise make the first character uppercase and remaining lowercase.
+> You are given two strings, source and target.<br/>
+> Write a script to find out if the given strings are Buddy Strings.<br/>
+> If swapping of a letter in one string make them same as the other then they are `Buddy Strings`.
>
> **Example 1**
>
> ```text
-> Input: $str = "PERL IS gREAT"
-> Output: "Perl is Great"
-> ```
+> Input: $source = "fuck"
+> $target = "fcuk"
+> Output: true
>
-> **Example 2**
->
-> ```text
-> Input: $str = "THE weekly challenge"
-> Output: "The Weekly Challenge"
+> The swapping of 'u' with 'c' makes it buddy strings.
+>```
+>
+>**Example 2**
+>
+>```text
+> Input: $source = "love"
+> $target = "love"
+> Output: false
> ```
>
> **Example 3**
>
> ```text
-> Input: $str = "YoU ARE A stAR"
-> Output: "You Are a Star"
+>Input: $source = "fodo"
+> $target = "food"
+> Output: true
+> ```
+>
+>**Example 4**
+>
+>```text
+> Input: $source = "feed"
+> $target = "feed"
+> Output: true
> ```
-The second task, too, is easily solved with a regular expression.
+This one is a bit more tricky, and I deal with it step by step.
+
+##### First step: Check correct length.
+
+If the `$source` and `$target` strings have different lengths,
+it will never be possible to transform one into the other
+by flipping characters, so I return `false` in that case:
+
+```perl
+ return false
+ unless length( $source ) == length( $target );
+```
+I then can safely assume that the strings *do* have the same length
+in the rest of the code.
+
+##### Second step: Find differing pairs
+I compare the strings character by character,
+by first creating pairs of characters at the same position,
+and then selecting only those where the two characters are not the same.
+Example:
+```text
+$source: f o o d
+$target: f o d o
+@pairs: ( [ "f", "f" ], [ "o", "o" ], [ "o", "d" ], [ "d", "o" ] )
+@diff_pairs: ( [ "o", "d" ], [ "d", "o" ] )
+```
+I use the well-known `split //, STRING`
+for turning the strings into lists of characters,
+and I put both lists into anonymous arrays.
+For creating pairs from those arrays,
+I use `zip` (from `List::Util`),
+which combines elements from the array-refs given as parameters
+into short arrays for each position.
+Next, I `grep` through the pairs,
+selecting only those where the two characters differ:
+```perl
+ my @pairs = zip [ split "", $source ], [ split "", $target ];
+ my @diff_pairs = grep $_->[0] ne $_->[1], @pairs;
+```
-Here, I use three capture buffers:
+##### Third step: Recognize Buddy Strings
-* one for the first letter, which might have to be put into lower or uppercase depending on the length of the word: `(\w)`
-* one for a possible second character: `(\w?)`
-* and one for the (possibly empty) rest of the word, from the third character to the end: `(\w*)`.
+Strings are *Buddy Strings* if we can swap two letters in one string
+and get the other string.
+This is only possible if:
-The third capture has a special role:<br/>
-If it is empty, the whole word is only one or two characters long, and the first letter needs to be lowercase.<br/>
-If it is non-empty, we need to uppercase the first letter.
+* there are exactly two differing pairs,
+ and their letters are the same, only in different order<br/>
+ ('Standard Buddies'),
-The second and third captures will always be lowercased for the result.
+or (as per Example 4):
-My whole solution consists of a single substitution, with a `/e` option to evaluate the substitution part as an expression, a `/g` option to repeat the substitution as often as possible, and a `/r` option to return the resulting final string instead of the number of substitutions done.<br/>
-When I use the `/e` option, I put the expression into a pair of curly brackets, to give an optical hint that this is 'code' to be evaluated. I then use angle brackets for the pattern part.
+* the strings are completely equal,
+ but there is at least one letter that is contained at least twice,
+ so that we can swap the letter with itself<br/>
+ ('Equal Buddies').
-So here we go:
+##### Standard Buddies
+
+The first criteria translates into this:
+```perl
+ return true
+ if @diff_pairs == 2
+ && $diff_pairs[0][0] eq $diff_pairs[1][1]
+ && $diff_pairs[0][1] eq $diff_pairs[1][0];
+```
+If this didn't find us any 'Standard Buddies', we check for 'Equal Buddies'.
+
+##### Equal Buddies
+
+The strings have to be equal,
+but instead of comparing the two strings letter by letter,
+there's a shortcut:
+we have already compiled a list of differing pairs,
+so having *no* differing pairs is the same as the strings being equal:
+```perl
+ return false
+ unless @diff_pairs == 0;
+```
+
+For checking whether there is a character that is contained at least twice,
+the 'elegant' solution would use a self-referencing regular expression:
+```perl
+ $source =~ /(.).*\g1/;
+```
+(We can restrict ourselves to one of the two strings,
+because they are equal.)<br/>
+So the final return could actually look like this:
+```perl
+ return @diff_pairs == 0 && $source =~ /(.).*\g1/;
+```
+
+But as usual, I am a bit concerned about the quadratic runtime behavior
+of this regular expression for very long strings
+(which of course we don't have in the examples!).
+The regex engine must walk through the rest of the string
+for every single character,
+and then backtrack and do the same for the next one,
+resulting in a maximum of ${n(n+1)}\over{2}$ comparisons.
+
+So I resort to a linear approach,
+remembering which letters the string contains
+and returning `true` the moment a letter is found
+that has already been seen:
+
+```perl
+ return false
+ unless @diff_pairs == 0;
+ my %seen;
+ for ( split "", $source ) {
+ return true
+ if $seen{$_}++;
+ }
+ return false;
+```
+
+The complete solution:
```perl
use v5.36;
+use builtin qw( true false );
+use List::Util qw( zip );
+
+sub buddy_strings( $source, $target ) {
+ # Check for equal lengths.
+ return false
+ unless length( $source ) == length( $target );
+
+ # Extract pairs of differing characters.
+ my @pairs = zip [ split "", $source ], [ split "", $target ];
+ my @diff_pairs = grep $_->[0] ne $_->[1], @pairs;
-sub title_capital( $str ) {
- return $str =~ s<(\w)(\w?)(\w*)>{
- ( $3 ? uc $1 : lc $1 ) . lc "$2$3"
- }egr;
+ # Detect 'Standard' Buddies.
+ return true
+ if @diff_pairs == 2
+ && $diff_pairs[0][0] eq $diff_pairs[1][1]
+ && $diff_pairs[0][1] eq $diff_pairs[1][0];
+
+ # Detect 'Equal' Buddies containing at least one repeated character.
+ return false
+ unless @diff_pairs == 0;
+ my %seen;
+ for ( split "", $source ) {
+ return true
+ if $seen{$_}++;
+ }
+ return false;
}
```
+Nice challenge!
+
#### **Thank you for the challenge!**
diff --git a/challenge-331/matthias-muth/blog.txt b/challenge-331/matthias-muth/blog.txt
new file mode 100644
index 0000000000..8fdc3bfb39
--- /dev/null
+++ b/challenge-331/matthias-muth/blog.txt
@@ -0,0 +1 @@
+https://github.com/MatthiasMuth/perlweeklychallenge-club/tree/muthm-331/challenge-331/matthias-muth#readme
diff --git a/challenge-331/matthias-muth/perl/ch-1.pl b/challenge-331/matthias-muth/perl/ch-1.pl
new file mode 100755
index 0000000000..adcb8c2513
--- /dev/null
+++ b/challenge-331/matthias-muth/perl/ch-1.pl
@@ -0,0 +1,28 @@
+#!/usr/bin/env perl
+#
+# The Weekly Challenge - Perl & Raku
+# (https://theweeklychallenge.org)
+#
+# Challenge 331 Task 1: Last Word
+#
+# Perl solution by Matthias Muth.
+#
+
+use v5.36;
+
+sub last_word_length( $str ) {
+ return length( ( split " ", $str )[-1] // "" );
+}
+
+use Test2::V0 qw( -no_srand );
+
+is last_word_length( "The Weekly Challenge" ), 9,
+ 'Example 1: last_word_length( "The Weekly Challenge" ) == 9';
+is last_word_length( " Hello World " ), 5,
+ 'Example 2: last_word_length( " Hello World " ) == 5';
+is last_word_length( "Let" ), 3,
+ 'Example 3: last_word_length( "Let" ) == 3';
+is last_word_length( "" ), 0,
+ 'Test 1: last_word_length( "" ) == 0';
+
+done_testing;
diff --git a/challenge-331/matthias-muth/perl/ch-2.pl b/challenge-331/matthias-muth/perl/ch-2.pl
new file mode 100755
index 0000000000..53f3a6142b
--- /dev/null
+++ b/challenge-331/matthias-muth/perl/ch-2.pl
@@ -0,0 +1,55 @@
+#!/usr/bin/env perl
+#
+# The Weekly Challenge - Perl & Raku
+# (https://theweeklychallenge.org)
+#
+# Challenge 331 Task 2: Buddy Strings
+#
+# Perl solution by Matthias Muth.
+#
+
+use v5.36;
+use builtin qw( true false );
+
+use List::Util qw( zip );
+
+sub buddy_strings( $source, $target ) {
+ # Check for equal lengths.
+ return false
+ unless length( $source ) == length( $target );
+
+ # Extract pairs of differing characters.
+ my @pairs = zip [ split "", $source ], [ split "", $target ];
+ my @diff_pairs = grep $_->[0] ne $_->[1], @pairs;
+
+ # Detect 'Standard' Buddies.
+ return true
+ if @diff_pairs == 2
+ && $diff_pairs[0][0] eq $diff_pairs[1][1]
+ && $diff_pairs[0][1] eq $diff_pairs[1][0];
+
+ # Detect 'Equal' Buddies containing at least one repeated character.
+ return false
+ unless @diff_pairs == 0;
+ my %seen;
+ for ( split "", $source ) {
+ return true
+ if $seen{$_}++;
+ }
+ return false;
+}
+
+use Test2::V0 qw( -no_srand );
+
+is buddy_strings( "fuck", "fcuk" ), T,
+ 'Example 1: buddy_strings( "fuck", "fcuk" ) is true';
+is buddy_strings( "love", "love" ), F,
+ 'Example 2: buddy_strings( "love", "love" ) is false';
+is buddy_strings( "fodo", "food" ), T,
+ 'Example 3: buddy_strings( "fodo", "food" ) is true';
+is buddy_strings( "feed", "feed" ), T,
+ 'Example 4: buddy_strings( "feed", "feed" ) is true';
+is buddy_strings( "", "" ), F,
+ 'Test 1: buddy_strings( "", "" ) is false';
+
+done_testing;