Sheriff Detector
with Raku

by Arne Sommer

Sheriff Detector with Raku

[387] Published 8. March 2026.

This is my response to The Weekly Challenge #363.

#363.1 String Lie Detector You are given a string.

Write a script that parses a self-referential string and determines whether its claims about itself are true. The string will make statements about its own composition, specifically the number of vowels and consonants it contains.

Example 1:
Input: $str = "aa — two vowels and zero consonants"
Output: true
Example 2:
Input: $str = "iv — one vowel and one consonant"
Output: true
Example 3:
Input: $str = "hello - three vowels and two consonants"
Output: false
Example 4:
Input: $str = "aeiou — five vowels and zero consonants"
Output: true
Example 5:
Input: $str = "aei — three vowels and zero consonants"
Output: true

Last week I used the «Lingua::EN::Numbers» module to translate numbers to text. This time it is the other way round, and we can use the «Lingua::NumericWordForms» module.

File: string-lie-detector
#! /usr/bin/env raku

use Lingua::NumericWordForms;                             # [1]

unit sub MAIN ($str where $str ~~ /<[\-—]>/,              # [2]
               :a(:$all),                                 # [3]
               :v(:$verbose) = $all);                     # [3a]

my ($source, $desc) = $str.split(/\s*<[\-—]>\s+/);        # [4]

my @sentence;                                             # [5]

my $global-ok = True;                                     # [6]

for $desc.words -> $word                                  # [7]
{
  if $word eq any <vowel vowels consonant consonants>     # [8]
  {
    if ! count-ok($source, @sentence.join(" "), $word)    # [9]
    {
      $global-ok = False;                                 # [9a]
      @sentence  = ();                                    # [9b]
      last unless $all;                                   # [9c]
    }
    else
    {	
       @sentence = ();                                    # [10]
    }
  }
  elsif @sentence.elems == 0 && $word eq "and"            # [11]
  {
    ;                                                     # [11a]
  }
  else
  {
    @sentence.push: $word;                                # [12]
  }
}

if @sentence.elems                                        # [13]
{
  say ": Junk '{ @sentence[] }' after last literal vowel(s)/consonant(s)"
    if $verbose;

  $global-ok = False;                                      # [13a]
}

say $global-ok;                                           # [14]

sub count-ok ($source, $sentence, $type)                  # [15]
{
  my $number = from-numeric-word-form($sentence);         # [16]

  my $count = $source.comb(                               # [17]
     $type ~~ /^vowels+$/                                 # [17a]
       ?? /:i <[aeiouy]>/                                 # [17b]
       !! /:i <[bcdfghjklmnpqrstvwxz]>/).elems;           # [17c, 17d]

  my $ok = $count == $number;                             # [18]

  say ": $source | $sentence = $number | $type: found $count | $ok"
    if $verbose;

  return $ok;                                             # [19]
}

[1] The module doing the translation.

[2] Ensure a string with a dash (ascii or Unicode) character.

[3] «All mode» will run through the text even if it has failed. It enables verbose mode by default [3a].

[4] Split the input on the dash, and get rid of any spaces aropund it.

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

[5] The sentence to parse will be built up here, word for word.

[6] Assume success.

[7] Iterate over the words.

[8] If it is one of the magic words,

[9] Run the sentence test on the partial sentence. If that fails, register global failure [9a], get rid of the sentence (in prepration for a new one [9b]) and exit the parsing unless «all mode» is in operation [9c]. Note the usage of a negated test in if !, as unless does not allow an else part.

[10] As [9b].

[11] Ignore the word «and» if it is the very first one in a new partial sentence.

[12] Add the word to the partial sentence.

[13] Look for random words left over after parsing the input. If so, we have failed [13a].

[14] Print the result.

[15] The procedure doing the sentence check. The first argumnet is a number, the second is the textual representation, hopefully. The third argument is the type: vowel(s) or consonant(s).

[16] Use the «Lingua::NumericWordForms» module to parse the text.

[17] Count [17d] the number of vowels [17b] or consonants [17c], depending on what is requested.

[18] Do we have a match?

[19] Return success or failure.

Running it:

$ ./string-lie-detector "aa — two vowels and zero consonants"
True

$ ./string-lie-detector "iv — one vowel and one consonant"
True

$ ./string-lie-detector "hello - three vowels and two consonants"
False

$ ./string-lie-detector "aeiou — five vowels and zero consonants"
True

$ ./string-lie-detector "aei — three vowels and zero consonants"
True

Looking good.

With verbose mode:

$ ./string-lie-detector -v "aa — two vowels and zero consonants"
: aa | two = 2 | vowels: found 2 | True
: aa | zero = 0 | consonants: found 0 | True
True

$ ./string-lie-detector -v "iv — one vowel and one consonant"
: iv | one = 1 | vowel: found 1 | True
: iv | one = 1 | consonant: found 1 | True
True

$ ./string-lie-detector -v "hello - three vowels and two consonants"
: hello | three = 3 | vowels: found 2 | False
False

$ ./string-lie-detector -v "aeiou — five vowels and zero consonants"
: aeiou | five = 5 | vowels: found 5 | True
: aeiou | zero = 0 | consonants: found 0 | True
True

$ ./string-lie-detector -v "aei — three vowels and zero consonants"
: aei | three = 3 | vowels: found 3 | True
: aei | zero = 0 | consonants: found 0 | True
True

Use «all mode» to get all the error messages, if any, even after failure has been established. It enbles «verbose mode» automatically.

$ ./string-lie-detector -a "hello - three vowels and two consonants"
: hello | three = 3 | vowels: found 2 | False
: hello | two = 2 | consonants: found 3 | False
False

Any non-conforming sentence, or parts thereof, will trigger an error:

$ ./string-lie-detector -a "aei — three vowels and zero consonants and that's it"
: aei | three = 3 | vowels: found 3 | True
: aei | zero = 0 | consonants: found 0 | True
: Junk 'that's it' after last literal vowel(s)/consonant(s)
False

$ ./string-lie-detector -a "aei — whatever, and that's it"
: Junk 'whatever, and that's it' after last literal vowel(s)/consonant(s)
False

#363.2 Subnet Sheriff You are given an IPv4 address and an IPv4 network (in CIDR format).

Write a script to determine whether both are valid and the address falls within the network. For more information see the Wikipedia article.

Example 1:
Input: $ip_addr = "192.168.1.45"
       $domain  = "192.168.1.0/24"
Output: true
Example 2:
Input: $ip_addr = "10.0.0.256"
       $domain  = "10.0.0.0/24"
Output: false
Example 3:
Input: $ip_addr = "172.16.8.9"
       $domain  = "172.16.8.9/32"
Output: true
Example 4:
Input: $ip_addr = "172.16.4.5"
       $domain  = "172.16.0.0/14"
Output: true
Example 5:
Input: $ip_addr = "192.0.2.0"
       $domain  = "192.0.2.0/25"
Output: true

There probably are modules doing this stuff, but I have chosen to do it manually as it actually is quite easy.

File: subnet-sheriff
#! /usr/bin/env raku

subset IPv4 where /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && all($0,$1,$2,$3) < 256;  # [1]
								       
subset CIDR where /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/                      # [2]
  && all($0,$1,$2,$3) < 256 && $4 <= 32;

unit sub MAIN (IPv4 $ipv4, CIDR $cidr, :v(:$verbose));                       # [3]

sub ipv4_2_int (IPv4 $ipv4)                                                  # [4]
{
  say ": IPv4: $ipv4 \tDecimal: { $ipv4.split('.')>>.fmt('%08b'). \
   join.parse-base(2) } - Binary: { $ipv4.split('.')>>.fmt('%08b').join }"
     if $verbose; 
  
  return $ipv4.split('.')>>.fmt('%08b').join.parse-base(2);                  # [5]
}

sub cidr_mask (UInt $size where 0 <= $size <= 32)                            # [6]
{
  say ": CIDR size: $size \tDecimal: { (( '1' x $size ) ~ ( '0' x ( 32 - $size ) \
    )).parse-base(2) } - Binary: { ( '1' x $size ) ~ ( '0' x ( 32 - $size ) ) }"
      if $verbose;

 return (( '1' x $size ) ~ ( '0' x ( 32 - $size ) )).parse-base(2);          # [7]
}

my ($ip, $size) = $cidr.split(/\//);                                         # [8]

say ipv4_2_int($ipv4) +& cidr_mask($size.UInt) ==                            # [9]
    ipv4_2_int($ip) +& cidr_mask($size.UInt);

[1] A custom type set up with subset and where to validate the IP address.

[2] A second custom type, to validate the CIDR input.

[3] Apply the custom types on the input.

[4] Procedure to convert a (valid) IPv4 address to a decimal value.

[5] We do this by splitting the string on the dots, converting each part (an integer value 0-255) to a zero padded binary string of length 8 with fmt (as 255 decimal is 11111111 binary). Then we glue them together to a 32 bit binary value and convert that to decimal with parse-base.

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

See docs.raku.org/routine/parse-base for more information about parse-base.

[6] Procedure generating a CIDR mask given the length in bits.

[7] This consists of as many «1»s as the specified length, which we get with the string repetition operator x, and padding out with zeroes to get the total 32 bit length. The returned value is in decimal.

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

[8] Get the IP and network size parts.

[9] Apply the CIDR mask on the IPv4 address and the CIDR IPv4 value. The resulting decimal values are the network, and they must be identical for the CIDR rule to match (validate) the address.

See /docs.raku.org/language/operators#infix_+& for more information about the infix bitwise AND operator +&.

Running it:

$ ./subnet-sheriff "192.168.1.45" "192.168.1.0/24"
True

$ ./subnet-sheriff "10.0.0.256" "10.0.0.0/24"
Usage:
  subnet-sheriff [-v|--verbose[=Any]] <:ipv4> <cidr>

$ ./subnet-sheriff "172.16.8.9" "172.16.8.9/32"
True

$ ./subnet-sheriff "172.16.4.5" "172.16.0.0/14"
True

$ ./subnet-sheriff "192.0.2.0" "192.0.2.0/25"
True

Looking good, except for the second example as 256 is illegal. I think it is fair to crash on illegal input, but I'll do as the example prescribes.

But let us show off verbose mode first, for example 1, 3, 4, and 5:

$ ./subnet-sheriff -v "192.168.1.45" "192.168.1.0/24"
: IPv4: 192.168.1.45 Decimal: 3232235821 - Binary: 11000000101010000000000100101101
: CIDR size: 24      Decimal: 4294967040 - Binary: 11111111111111111111111100000000
: IPv4: 192.168.1.0  Decimal: 3232235776 - Binary: 11000000101010000000000100000000
: CIDR size: 24      Decimal: 4294967040 - Binary: 11111111111111111111111100000000
True

$ ./subnet-sheriff -v "172.16.8.9" "172.16.8.9/32"
: IPv4: 172.16.8.9   Decimal: 2886731785 - Binary: 10101100000100000000100000001001
: CIDR size: 32      Decimal: 4294967295 - Binary: 11111111111111111111111111111111
: IPv4: 172.16.8.9   Decimal: 2886731785 - Binary: 10101100000100000000100000001001
: CIDR size: 32      Decimal: 4294967295 - Binary: 11111111111111111111111111111111
True

$ ./subnet-sheriff -v "172.16.4.5" "172.16.0.0/14"
: IPv4: 172.16.4.5   Decimal: 2886730757 - Binary: 10101100000100000000010000000101
: CIDR size: 14      Decimal: 4294705152 - Binary: 11111111111111000000000000000000
: IPv4: 172.16.0.0   Decimal: 2886729728 - Binary: 10101100000100000000000000000000
: CIDR size: 14      Decimal: 4294705152 - Binary: 11111111111111000000000000000000
True

$ ./subnet-sheriff -v "192.0.2.0"  "192.0.2.0/25"
: IPv4: 192.0.2.0    Decimal: 3221225984 - Binary: 11000000000000000000001000000000
: CIDR size: 25      Decimal: 4294967168 - Binary: 11111111111111111111111110000000
: IPv4: 192.0.2.0    Decimal: 3221225984 - Binary: 11000000000000000000001000000000
: CIDR size: 25      Decimal: 4294967168 - Binary: 11111111111111111111111110000000
True

Then a version that does not crash on illegal input:

File: subnet-sheriff-false
#! /usr/bin/env raku

subset IPv4 where /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && all($0,$1,$2,$3) < 256;
								       
subset CIDR where /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/ 
  && all($0,$1,$2,$3) < 256 && $4 <= 32;

multi sub MAIN (IPv4 $ipv4, CIDR $cidr, :v(:$verbose))  # [1]
{
  my ($ip, $size) = $cidr.split(/\//);

  say ipv4_2_int($ipv4, $verbose) +& cidr_mask($size.UInt, $verbose) == 
      ipv4_2_int($ip, $verbose) +& cidr_mask($size.UInt, $verbose);
}

multi sub MAIN ($ipv4, $cidr, :v(:$verbose))            # [2]
{
  say ": Illegal input" if $verbose;

  say False;
}

sub ipv4_2_int (IPv4 $ipv4, $verbose)
{
  say ": IPv4: $ipv4 \tDecimal: { $ipv4.split('.')>>.fmt('%08b'). \
   join.parse-base(2) } - Binary: { $ipv4.split('.')>>.fmt('%08b').join }"
     if $verbose; 
  
  return $ipv4.split('.')>>.fmt('%08b').join.parse-base(2);
}

sub cidr_mask (UInt $size where 0 <= $size <= 32, $verbose)
{
  say ": CIDR size: $size \tDecimal: { (( '1' x $size ) ~ ( '0' x ( 32 - $size ) \
    )).parse-base(2) } - Binary: { ( '1' x $size ) ~ ( '0' x ( 32 - $size ) ) }"
      if $verbose;
}

[1] This time I have used multiple dispatch (with multi). The first version is the same as before.

[2] The second version kick in on illegal input and gives the prescribed «False». For absolutely all illegal input.

See docs.raku.org/syntax/multi for more information about multi.

Note that I had to send the verbose argument to the procedures this time, as they (the procedures) are outside of the MAIN scope(s).

Running it on the second example:

$ ./subnet-sheriff-false -v "10.0.0.256" "10.0.0.0/24"
False

$ ./subnet-sheriff-false -v "10.0.0.256" "10.0.0.0/24"
: Illegal input
False

Let us simplify the program. The binary and (+&) is not needed. The CIDR size can be applied as a substring lookup on the binary values instead - to get the network part:

File: subnet-sheriff-substr
#! /usr/bin/env raku

subset IPv4 where /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && all($0,$1,$2,$3) < 256;

subset CIDR where /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/
  && all($0,$1,$2,$3) < 256 && $4 <= 32;

multi sub MAIN (IPv4 $ipv4, CIDR $cidr, :v(:$verbose))
{
  my ($ip, $size) = $cidr.split(/\//);

  say ipv4_2_network($ipv4, $size, $verbose) == 
      ipv4_2_network($ip, $size, $verbose);
}

multi sub MAIN ($ipv4, $cidr, :v(:$verbose))
{
  say ": Illegal input" if $verbose;

  say False;
}

sub ipv4_2_network (IPv4 $ipv4, $size, $verbose)  # [1]
{
  say ": IPv4: $ipv4 \tDecimal: { $ipv4.split('.')>>.fmt('%08b') \
     .join.parse-base(2) } - Binary: { $ipv4.split('.')>>.fmt('%08b').join } \
     - Network: { $ipv4.split('.')>>.fmt('%08b').join.substr(0, $size) } \
     /$size" if $verbose; 
  
  return $ipv4.split('.')>>.fmt('%08b').join.substr(0, $size);
}

[1] This procedure requires the size, and it will only return that many binary digits.

Running it gives the expected result, but verbose mode looks different:

$ ./subnet-sheriff-substr -v "192.168.1.45" "192.168.1.0/24"
: IPv4: 192.168.1.45 	Decimal: 3232235821 \
   - Binary: 11000000101010000000000100101101 \
  - Network: 110000001010100000000001/24
: IPv4: 192.168.1.0 	Decimal: 3232235776 \
   - Binary: 11000000101010000000000100000000 \
  - Network: 110000001010100000000001/24
True

)$ ./subnet-sheriff-substr -v "10.0.0.256" "10.0.0.0/24"
: Illegal input
False

$ ./subnet-sheriff-substr -v "172.16.8.9" "172.16.8.9/32"
: IPv4: 172.16.8.9 	Decimal: 2886731785 \
   - Binary: 10101100000100000000100000001001 \
  - Network: 10101100000100000000100000001001/32
: IPv4: 172.16.8.9 	Decimal: 2886731785 \
   - Binary: 10101100000100000000100000001001 \
  - Network: 10101100000100000000100000001001/32
True

$ ./subnet-sheriff-substr -v "172.16.4.5" "172.16.0.0/14"
: IPv4: 172.16.4.5 	Decimal: 2886730757 \
   - Binary: 10101100000100000000010000000101 \
  - Network: 10101100000100/14
: IPv4: 172.16.0.0 	Decimal: 2886729728 \
   - Binary: 10101100000100000000000000000000 \
  - Network: 10101100000100/14
True

$ ./subnet-sheriff-substr -v "192.0.2.0"  "192.0.2.0/25"
: IPv4: 192.0.2.0 	Decimal: 3221225984 \
   - Binary: 11000000000000000000001000000000 \
  - Network: 1100000000000000000000100/25
: IPv4: 192.0.2.0 	Decimal: 3221225984 \
   - Binary: 11000000000000000000001000000000 \
  - Network: 1100000000000000000000100/25
True

And that's it.