Perl Weekly Challenge: Appear Twice, Count Once

There’s no relation to the problems for this week’s musical theme. Earlier this week, my wife and I watched the documentary Gordon Lightfoot: If You Could Read My Mind, so you’re getting my favorite Gordon Lightfoot song… Canadian Railroad Trilogy.

So now, while we’re listening to the construction of the Canadian Pacific Railway, let’s construct some code as part of Perl Weekly Challenge 280!

Task 1: Twice Appearance

You are given a string, $str, containing lowercase English letters only.

Write a script to print the first letter that appears twice.

Example 1

Input: $str = "acbddbca"
Output: "d"

Example 2

Input: $str = "abccd"
Output: "c"

Example 3

Input: $str = "abcdabbb"
Output: "a"

Approach

At first, I thought I wanted to use a Bag for this but then I realized that Bags are good for counting items all at once. I don’t want a count of all the characters… I just want to find the first character that occurs more than once. So I dropped the fancy-pants object types, and just went with a hash.

I did, however, want to go above and beyond the examples. What if no character occurs more than once? I needed a fallback case.

Raku

sub twiceAppearance($str) {
  my @chars = $str.comb;
  my %count;
  for @chars -> $char {
    %count{$char}++;
    return $char if %count{$char} > 1;
  }
  return "␀"; # fallback
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: $str = "acbddbca"
Output: "d"

Example 2:
Input: $str = "abccd"
Output: "c"

Example 3:
Input: $str = "abcdabbb"
Output: "a"

Example 4:
Input: $str = "abcdefg"
Output: "␀"

Perl

The Perl version of the script is just like the Raku version (or do I still write very Perl-ish Raku?).

sub twiceAppearance($str) {
  my @chars = split //, $str;
  my %count;
  foreach my $char ( @chars ) {
    $count{$char}++;
    return $char if $count{$char} > 1;
  }
  return "␀"; # fallback
}

View the entire Perl script for this task on GitHub.

Python

In Python, I still want to use a Counter object, though, because they return a zero count for missing items instead of raising an error, which means we can just increment elements that don’t exist yet into existence (to use a Perl term, autovivification).

from collections import Counter

def twiceAppearance(str):
  count = Counter()
  for char in str:
    count[char] += 1
    if count[char] > 1:
       return char
  return "␀" # fallback

View the entire Python script for this task on GitHub.

Elixir

When I got to the Elixir solution, I realized I didn’t need to increment the value in the hash, er, Map. I just needed to test to see if the character was already in the map. If it was, I could return the character. If it wasn’t, I add the character to the map and process the next character.

  def twiceAppearance([], _), do: "␀" # fallback

  def twiceAppearance([next | rest], count) do
    if Map.has_key?(count, next) do
      next
    else
      twiceAppearance(rest, Map.put(count, next, 1))
    end
  end

  def twiceAppearance(str) do
    # split the string into characters and
    # reprocess with an empty Map
    twiceAppearance(String.graphemes(str), %{})
  end

View the entire Elixir script for this task on GitHub.

Task 2: Count Asterisks

You are given a string, $str, where every two consecutive vertical bars are grouped into a pair.

Write a script to return the number of asterisks, *, excluding any between each pair of vertical bars.

Example 1

Input: $str = "p|*e*rl|w**e|*ekly|"
Ouput: 2

The characters we are looking here are "p" and "w**e".

Example 2

Input: $str = "perl"
Ouput: 0

Example 3

Input: $str = "th|ewe|e**|k|l***ych|alleng|e"
Ouput: 5

The characters we are looking here are "th", "e**", "l***ych" and "e".

Approach

When I saw this one, it screamed regular expression to me. I could use a regex to remove characters between pairs of vertical bars, and then just count the asterisks.

Raku

Here’s where a Bag came in handy, because I could use it to get a count of all the different characters all at once. But because the $str parameter is read-only (and I can’t make it read-write because I’m passing in a string constant), I need to copy it to another variable before I modify it. Fortunately the (my $newvar = $oldvar) construction I learned in Perl still works in Raku.

sub countAsterisks($str) {
  (my $excluded = $str) ~~ s:global/\| <-[ | ]> * \|//;
  my %count = $excluded.comb.Bag;
  return 0 unless %count{"*"}:exists;
  return %count{"*"};
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $str = "p|*e*rl|w**e|*ekly|"
Output: 2

Example 2:
Input: $str = "perl"
Output: 0

Example 3:
Input: $str = "th|ewe|e**|k|l***ych|alleng|e"
Output: 5

Edit: I’m reading through other solutions to these problems, and Laurent Rosenfield presented the following solution:

sub count-asterisks ($in is copy) {
    $in ~~ s:g/'|'.*?'|'//;
    return +($in ~~ tr/*//);
}

🤦🏻‍♂️ I don’t have to copy to a new variable! Also his regex is much cleaner. And he counts the asterisks in a similar fashion to what I wind up doing down in the Elixir solution.

Perl

Again, I’m writing my Raku with my Perl brain, so converting my Raku code back to Perl isn’t that hard. I do wish I had a Bag type in Perl… well, there’s a Set::Bag module in CPAN, but it looks more complex to use than what I need, so I’m just emulating a Bag with a hash.

sub countAsterisks($str) {
  (my $excluded = $str) =~ s/\|[^|]*\|//g;
  my %count;
  map { $count{$_}++ } split //, $excluded;
  return 0 unless exists $count{"*"};
  return $count{"*"};
}

View the entire Perl script for this task on GitHub.

Python

Here’s where I got to use a Counter like a Bag: just pass the string to the counter after it’s been stripped by the regex. Here’s where the Counter returning a zero count for missing items really comes in handy.

from collections import Counter
import re

def countAsterisks(str):
  count = Counter(re.sub(r'\|[^|]+\|', '', str))
  return count["*"]

View the entire Python script for this task on GitHub.

Elixir

And just with task 1, I got all the way to the Elixir implementation before I realized I could do things a slightly different way: instead of counting the asterisks remaining in the string once I’d stripped out everything that was between pairs of vertical bars, I could just use another regex to strip everything that wasn’t an asterisk, and then just return the string length.

  def countAsterisks(str) do
    excluded  = Regex.replace(~r/\|[^|]+\|/, str, "")
    Regex.replace(~r/[^*]+/, excluded, "")
    |> String.length
  end

I only wish Regex.replace/4 took the string as the first parameter, because then the entire function could be a single pipe. In fact, I wanted to do this so much, I went searching, and I saw this statement:

It’s worth noting that you can use

  • String.replace(string, regex, replacement),
  • String.split(string, regex), and
  • String.match?(string, regex)

instead of the equivalent Regex functions.

https://hexdocs.pm/elixir/String.html

🤦🏻‍♂️

  def countAsterisks(str) do
    str
    |> String.replace(~r/\|[^|]+\|/, "")
    |> String.replace(~r/[^*]+/, "")
    |> String.length
  end

MUCH BETTER.

View the entire Elixir script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-280/packy-anderson