Count Twice
with Raku

by Arne Sommer

Count Twice with Raku

[301] Published 4. August 2024.

This is my response to The Weekly Challenge #280.

Challenge #280.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"

Brute force, i.e. a loop, is probably the best approach:

File: twice-appearance
#! /usr/bin/env raku

unit sub MAIN ($str where $str ~~ /^<[a..z]>+$/);  # [1]

my %seen;                                          # [2]

for $str.comb -> $letter                           # [3]
{
  if %seen{$letter}                                # [4]
  {
    say $letter;                                   # [4a]
    last;                                          # [4b]
  }

  %seen{$letter} = True;                           # [5]
}

[1] Ensure that the string contains lowercase English letters only, one or more.

[2] Already encountered letters will be stored here.

[3] Iterate over each letter in the word.

[4] Have we seen the current letter before? If so, print it [4a] and exit [4b].

[5] Mark the current letter as seen.

Running it:

$ ./twice-appearance acbddbca
d

$ ./twice-appearance abccd
c

$ ./twice-appearance abcdabbb
a

Looking good.

No verbose mode, but here are some additional examples:

$ ./twice-appearance 12
Usage:
  ./twice-appearance [-v|--verbose[=Any]] <str>

$ ./twice-appearance abcdefg

We can make it shorter, almost a one liner, with map instead of the explicit loop:

File: twice-appearance-map
#! /usr/bin/env raku

unit sub MAIN ($str where $str ~~ /^<[a..z]>+$/);

$str.comb.map({ state %seen; (say $_; last) if %seen{$_}++ });

Note the state variable declared inside the map.

See docs.raku.org/syntax/state for more information about the variable declarator state.

See docs.raku.org/routine/map for more information about map.

Running this program gives the expected result:

$ ./twice-appearance-map acbddbca
d

$ ./twice-appearance-map abccd
c

$ ./twice-appearance-map abcdabbb
a

Challenge #280.2:

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".

If we split a string on, let us say a comma, we get the parts without the commas. This is indeed what happens on parsing a CSV (comma separated values) file.

An example:

> "This,does,not,make,sense".split(",").raku
("This", "does", "not", "make", "sense").Seq

We can split on regular expressions instead of single characters (or strings).

> "This,does not;make,any  sense".split(/<[\ ,;.]>+/).raku
("This", "does", "not", "make", "any", "sense").Seq

So it should not come as a big surprise that we can use a pattern matching the pairs of vertical bars, and split on that.

File: count-asterisks
#! /usr/bin/env raku

unit sub MAIN ($str, :v(:$verbose));             # [1]

my @parts = $str.split(/\|.*?\|/);               # [2]

say ": Parts: { @parts.raku }" if $verbose;

@parts.join.comb.grep( * eq '*').elems.say;      # [3]

[1] No restrictions on the string this time.

[2] Split on pairs of vertical bars (with some non-«vertical bars» between them).

[3] Join the resulting array, split it into individual characters, get rid of non-asterisks, and count the result (i.e. the asterisks).

Running it:

$ ./count-asterisks "p|*e*rl|w**e|*ekly|"
2

$ ./count-asterisks "perl"
0

$ ./count-asterisks "th|ewe|e**|k|l***ych|alleng|e"
5

Looking good.

With verbose mode:

$ ./count-asterisks -v "p|*e*rl|w**e|*ekly|"
: Parts: ["p", "w**e", ""]
2

$ ./count-asterisks -v "perl"
: Parts: ["perl"]
0

$ ./count-asterisks -v "th|ewe|e**|k|l***ych|alleng|e"
: Parts: ["th", "e**", "l***ych", "e"]
5

And that's it.