This is derived in part from my blog post made in answer to the Week 26 of the Perl Weekly Challenge organized by Mohammad S. Anwar as well as answers made by others to the same challenge.
The challenge reads as follows:
Create a script that accepts two strings, let us call it, “stones” and “jewels”. It should print the count of “alphabet” from the string “stones” found in the string “jewels”. For example, if your stones is “chancellor” and “jewels” is “chocolate”, then the script should print “8”. To keep it simple, only A-Z,a-z characters are acceptable. Also make the comparison case sensitive.
We're given two strings and need to find out how many letters of the second string can be found in the first string.
This is straight forward. Our script should be given two arguments (else the program aborts). We split the first string into individual letters and store them in the $letters
set. Note that we filter out any character not in the <[A-Za-z]>
character class. Then we split the second string into individual letters, keep only letters found in the $letters
set and finally use the .elems
method to count the number of letters.
use v6;
sub MAIN (Str $str1, Str $str2) {
my $letters = $str1.comb.grep( /<[A..Za..z]>/ ).Set;
my $count = $str2.comb.grep( { $_ (elem) $letters} ).elems;
say "$str2 has $count letters from $str1";
}
This works as expected:
$ perl6 count_letters.p6 chocolate chancellor
chancellor has 8 letters from chocolate
$ perl6 count_letters.p6 chocolate CHANCELLOR
CHANCELLOR has 0 letters from chocolate
This week, we are welcoming two new members, Donald Hunter and Markus Holzer, who both provided very interesting solutions.
Arne Sommer used a subset AtoZ
of strings to enforce strings with only ASCII lower case and upper case letters. The code doing the work is very concise and holds in just one code line:
say ($alphabet.comb.Set ⊍ $string.comb.Bag).Int;
Donald Hunter used a collect
subroutine to return a Bag
of letters for each input string. Then, computing the count of common letters is one code line:
say [+] collect($stones){collect($jewels).keys};
Donald also suggest a one-liner in his blog post:
[+] 'chancellor'.comb(/<[A..Z a..z]>/).Bag{'chocolate'.comb(/<[A..Z a..z]>/).Bag.keys}
Mark Senn provided not less than four possible solutions: array-based, cross-product-based, hash-based and set-based. Let me illustrate with the cross-product-based solution, which is, IMHO, quite original:
$count = 0;
(@a X @b).map({$_[0] eq $_[1] and $count++});
$count.say;
Markus Holzer's program is quite uncommon, creative, and clever. He first created a multi <∈
element-of operator between an iterable and a set that returns a sequence of all elements on the left side (the iterable) that are in the right side (the set):
multi sub infix:<\<∈>( Iterable $stones, Set $jewels ) returns Seq
{
# constant runs at BEGIN time, so this work gets only done once
constant \alphabet = ( 'a' .. 'z', 'A' .. 'Z' ).Set;
$stones.grep({ $_ ∈ alphabet && $_ ∈ $jewels });
}
His program then extends this operator to also work on two iterables and re-uses the previous definition of the operator in this new one:
multi sub infix:<\<∈>( Iterable $stones, Iterable $jewels ) returns Seq
{
$stones <∈ $jewels.Set
}
Note that, thanks to the multi
mechanism, the program is able to use the previously defined <∈
operator between an iterable and a set within the definition of the same operator between two iterables.
And, it finally extends is again to work on two strings. After these definitions, the code to find common letters is incredibly simple:
say "chancellor" <∈ "chocolates" ).chars;
Truly a beautiful use of Perl 6 expressive power.
Noud created a count_abc
subroutine to do the work:
sub count_abc(Str $stones, Str $jewels) {
$jewels.comb.grep({$_ (elem) $stones.comb.Set}).elems;
}
Ozzy used a for
loop to do the heavy work:
$count++ if @string2.grep: { $_ eq $i };
}
Simon Proctor used the SimpleLetters
subset of ASCII upper and lower case letters strings to validate the input. His program then uses the following MAIN
subroutine:
multi sub MAIN(
SimpleLetters $stones, #= String to find letters in
SimpleLetters $jewels #= String of letters to look for
) {
my $stone-set = $stones.comb.Set;
$jewels.comb.grep( { $_ (elem) $stone-set } ).elems.say;
}
Athanasius provided, as often, a solution a bit too long for quoting here. Most of the work is done in the following for
loop:
for $stones.split('').grep( { $ALPHA } ) -> Str $letter
{
if %jewels{$letter}:exists
{
++$count;
@letters.push($letter) if $show;
}
}
Jaldhar H. Vyas provided a Perl 6 one-liner:
perl6 -e 'my @a = @*ARGS[0].comb ∩ @*ARGS[1].comb; @*ARGS[1].comb.grep({$_ ∈ @a.any }).elems.say;' chancellor chocolate
Joelle Maslak used a set to find the common letters:
sub MAIN(Str:D $stones, Str:D $jewels) {
my $stone-set = $stones.comb.cache;
my $matches = $jewels.comb.grep: { $^a ∈ $stone-set };
say $matches.elems;
}
Roger Bell West used a hash to record the letters of the first string, and then updated a counter to account for letters of the second string found in the aforesaid hash.
Ruben Westerberg used a somewhat unexpected methodology to find the letters:
put "Number of letters of Alphabet found in Test: ", $jewels.chars-(S:g/[@stones]// given $jewels).chars;
Yet Ebreo also used an unexpected methodology:
say ($string2.chars-$string2.trans( $string1 => "").chars);
Four blog posts this time:
-
Arne Sommer: https://perl6.eu/string-angling.html;
-
Donald Hunter: http://donaldh.wtf/2019/09/stones-and-jewels/
-
Jaldar H. Vyas: https://www.braincells.com/perl/2019/09/perl_weekly_challenge_week_26.html
-
Roger Bell West: https://blog.firedrake.org/archive/2019/09/Perl_Weekly_Challenge_26.html
Please let me know if I forgot any of the challengers or if you think my explanation of your code misses something important (you can just file an issue against this GitHub page).
If you want to participate to the Perl Weekly Challenge, please connect to this site.