Skip to content

Latest commit

 

History

History
179 lines (122 loc) · 8.15 KB

common-letter-count.md

File metadata and controls

179 lines (122 loc) · 8.15 KB

Common Letter Count

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.

My Solution

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

Alternate Solutions

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 multimechanism, 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);

See Also

Four blog posts this time:

Wrapping up

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.