Perl Weekly Challenge: Week 363
Challenge 1:
String Lie Detectorr
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
The MAIN() function for the Raku solution is very simple; we just print the
output of the stringLieDetector() function.
say stringLieDetector($str);
The stringLieDetector() function is where all the work is done. It takes
one parameter; a self-referential string; and returns True or False;
sub stringLieDetector(Str $str) {
We are going to need to parse number words. I've arbitrarily chosen to stop
at ten which is good enough for the examples. %numbers is a Hash that
acts as a dictionary that matches number words with their corresponding numeric value.
Because this will never change during repeated function invocations, I've made it
a state variable.
state %numbers = (
'zero' => 0, 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4,
'five' => 5, 'six' => 6, 'seven' => 7, 'eight' => 8, 'nine' => 9,
'ten' => 10,
);
Now we split the input string into two with -- as the divider. The part
on the left is the content we will be analyzing and the part on the righr
will be claims made about the content,
my ($content, $claims) = $str.split(/<[—-]>/);
If either part is missing, the input string is invalid so we return False.
unless defined $content && defined $claims {
return False;
}
Unnecessary white space is .trim()ed from the beginning and end of $content and for
similicities sake, it is converted to all lower case with .lc().
$content = $content.trim.lc;
$claims also needs to be trimmed.
$claims = $claims.trim;
Now we count the vowels and consonants in $content in a case-insensitive way.
my $vowelCount = 0;
my $consonantCount = 0;
for $content.comb -> $char {
if $char ~~ /<[aeiou]>/ {
$vowelCount++;
} elsif $char ~~ /<[bcdfghjklmnpqrstvwxyz]>/ {
$consonantCount++;
}
}
The next part is a bit tricky. We need to extract number words from $claims.
my $claimedVowels;
my $claimedConsonants;
if $claims ~~ /(\w+) \s+ vowel/ {
$claimedVowels = %numbers{$0.lc};
}
if $claims ~~ /(\w+) \s+ consonant/ {
$claimedConsonants = %numbers{$0.lc};
}
We now have the tools to find out...
Whether the number of vowels in
$contentmatches the number of vowels claimed.my $vowelMatch = $vowelCount == $claimedVowels;Whether the number of consonants in
$contentmatches the number of consonants claimed.my $consonantMatch = $consonantCount == $claimedConsonants;Whether any number of vowels was claimed at all.
my $defVowels = defined $claimedVowels;Whether any number of consonants was claimed at all.
my $defConsonants = defined $claimedConsonants;
If all these things are true, the function will return True else it will return False.
return $defVowels && $defConsonants && $vowelMatch && $consonantMatch;
}
For Perl, we need a replacement for .trim(). It's a very simple function.
sub trim($str) {
$str =~ s/^\s+//;
$str =~ s/\s+$//;
return $str;
}
Other than that, most of the code is equivalent to Raku.
sub stringLieDetector($str) {
state %numbers = (
'zero' => 0, 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4,
'five' => 5, 'six' => 6, 'seven' => 7, 'eight' => 8, 'nine' => 9,
'ten' => 10, 'eleven' => 11, 'twelve' => 12,
);
my ($content, $claims) = split q{—}, $str;
unless (defined $content && defined $claims) {
return false;
}
$content = lc trim($content);
$claims = trim($claims);
my $vowelCount = 0;
my $consonantCount = 0;
for my $char (split //, $content) {
if ($char =~ /[aeiou]/) {
$vowelCount++;
} elsif ($char =~ /[bcdfghjklmnpqrstvwxyz]/) {
$consonantCount++;
}
}
my $claimedVowels;
my $claimedConsonants;
if ($claims =~ /(\w+) \s+ vowel/x) {
$claimedVowels = $numbers{lc $1};
}
if ($claims =~ /(\w+) \s+ consonant/x) {
$claimedConsonants = $numbers{lc $1};
}
my $vowelMatch = $vowelCount == $claimedVowels;
my $consonantMatch = $consonantCount == $claimedConsonants;
my $defVowels = defined $claimedVowels;
my $defConsonants = defined $claimedConsonants;
return $defVowels && $defConsonants && $vowelMatch && $consonantMatch;
}
say stringLieDetector($str) ? 'true' : 'false';
Challenge 2:
Subnet Sherriff
You are given an
IPv4 addressand anIPv4 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
This was the kind of task Perl was invented for in days of long ago and Raku is just as good.
Once again the MAIN() function is extremely short. It just prints the result
of the addressInNetwork() function.
say addressInNetwork($address, $network);
The addressInNetwork() function takes two String parameters; an IPv4 address
and a network address in CIDR format. It will return True if both are syntactically valid
and the IP address is in the the network range or False otherwise.
sub addressInNetwork(Str $address, Str $networkcidr) {
Validation of IPv4 and CIDR formats is done by the validIPv4() and validCIDR()
functions which will be described later. For now all we need to know is that
if either return False, there is no point in proceeding so this function will return
False.
unless validIPv4($address) {
return False;
}
unless validCIDR($networkcidr) {
return False;
}
The network CIDR address consists of two parts, the network address itself, and a prefix. These are separated here.
my ($network, $prefix) = $networkcidr.split('/');
Both the IP and Network addresses are converted into integers here using another
helper function IPToInt() which will be described later.
my $addressInt = IPToInt($address);
my $netInt = IPToInt($network);
The $prefix is used to create a mask which is applied to the network
address to determine which IP addresses belong to it. E.g. for /24, the
mask would be 0xFFFFFF00.
my $mask = (0xFFFFFFFF +< (32 - $prefix)) +& 0xFFFFFFFF;
We logically AND the mask both to the IP address and network address (as integers) and whether they are equal, we know the IP address is in that network or not.
return ($addressInt +& $mask) == ($netInt +& $mask);
}
The validIPv4() function, as was mentioned previously, determines if a string
represents a valid IPv4 address or not.
sub validIPv4(Str $ip) {
As the linked Wikipedia article explains, an IPv4 address is usually expressed
in "dotted-quad" format which consists of four numbers separated by 'dotcharacters.
This line checks if the string is in this format. We returnFalse` if it isn't.
unless $ip ~~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ {
return False;
}
Furthermore each of these numbers (or octets as they are known) represents an 8-byte
integer so must be between 0 and 255. If any one of them is not, we return False.
my @octets = $ip.split('.');
# Check each octet is in range 0-255
for @octets -> $octet {
unless $octet ~~ 0 .. 255 {
return False;
}
}
If we've passed both of these tests, the IP address is valid so we return True.
return True;
}
validCIDE() works in a similar fashion. Here the format consists of a dotted-quad
address and a number separated by an '/' character.
sub validCIDR(Str $cidr) {
unless $cidr ~~ /^(.+)\/(\d+)$/ {
return False;
}
my ($network, $prefix) = $cidr.split('/');
To validate the dotted-quad part we xan simply call the validIPv4() function.
unless validIPv4($network) {
return False;
}
The prefix must be a number between 0 and 32. If it isn't we return False.
unless $prefix ~~ 0 .. 32 {
return False;
}
If we pass all the tests, we return True.
return True;
}
IPToInt() converts a dotted-quad IPv4 address to 32-bit integer in a very elegant
and efficient way by bit-shifting each octet into it's proper place in the integer.
sub IPToInt(Str $ip) {
my @octets = $ip.split('.');
return (@octets[0] +< 24) + (@octets[1] +< 16) + (@octets[2] +< 8) + @octets[3];
}
This is the Perl version. This time we didn't need to provide any extra code because Perl has everything we need.
sub validIPv4($ip) {
unless ($ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
return false;
}
my @octets = split /\./, $ip;
for my $octet (@octets) {
The only real non-syntactic change I had to make was in a couple of places like this. You cannot smartmatch a number against a range in Perl without warnings so instead I compared with the minimum and maximum bounds like this:
unless ($octet >= 0 && $octet <= 255) {
return false;
}
}
return true;
}
sub validCIDR($cidr) {
unless ($cidr =~ /^(.+)\/(\d+)$/) {
return false;
}
my ($network, $prefix) = split /\//, $cidr;
unless (validIPv4($network)) {
return false;
}
unless ($prefix >= 0 && $prefix <= 32) {
return false;
}
return true;
}
sub IPToInt($ip) {
my @octets = split /\./, $ip;
return ($octets[0] << 24) + ($octets[1] << 16) + ($octets[2] << 8) + $octets[3];
}
sub addressInNetwork($address, $networkcidr) {
unless (validIPv4($address)) {
return false;
}
unless (validCIDR($networkcidr)) {
return false;
}
my ($network, $prefix) = split /\//, $networkcidr;
my $addressInt = IPToInt($address);
my $netInt = IPToInt($network);
my $mask = (0xFFFFFFFF << (32 - $prefix)) & 0xFFFFFFFF;
return ($addressInt & $mask) == ($netInt & $mask);
}
say addressInNetwork($address, $network) ? 'true' : 'false';