This is a solution to task #2 from the 61st Perl Weekly Challenge, written in Raku.
You are given a string containing only digits (0..9). The string should have between 4 and 12 digits.
Write a script to print every possible valid IPv4 address that can be made by partitioning the input string.
For the purpose of this challenge, a valid IPv4 address consists of four “octets” i.e. A, B, C and D, separated by dots (.).
Each octet must be between 0 and 255, and must not have any leading zeroes. (e.g., 0 is OK, but 01 is not.)
Matching IP addresses
This challenge looks like it can be solved using regular expressions. Let's start by defining a regex for valid IP addresses.
my $regex = / ^ ( '25' <[0..5]> # 25n - covers 250 to 255
| [
[ '2' <[0..4]> # 2n. – covers 20n to 24n
| '1' <[0..9]> # 1n. – covers 10n to 19n
| <[1..9]> # n. – covers 1n to 9n
]? # ^^ – optional
<[0..9]> ] # n – last digit for 0 to 249
) ** 4 % '.' $ /; # ^^^ x 4 separated by '.'
for <0.0.0.0 1.1.1.1 10.0.0.1 10.10.10.10 192.168.1.254 199.199.199.199 201.201.201.201
249.249.249.249 250.250.250.250 255.255.255.255 1.1.1.1. 1.01.1.1 1.2.3.256
256.1.1.1 1..3.4 1.2.3.4.5 1.1.1.01> -> $candidate {
say "{$candidate} is {$candidate ~~ $regex ?? "an IP address" !! "not valid"}";
}
0.0.0.0 is an IP address 1.1.1.1 is an IP address 10.0.0.1 is an IP address 10.10.10.10 is an IP address 192.168.1.254 is an IP address 199.199.199.199 is an IP address 201.201.201.201 is an IP address 249.249.249.249 is an IP address 250.250.250.250 is an IP address 255.255.255.255 is an IP address 1.1.1.1. is not valid 1.01.1.1 is not valid 1.2.3.256 is not valid 256.1.1.1 is not valid 1..3.4 is not valid 1.2.3.4.5 is not valid 1.1.1.01 is not valid
This is the classic regex for matching valid octets, combined with the Raku quantifier separator
%
to mach octets separated by .
.
A Simpler Regex
We should be able to simplify the regex by using a code block to validate the octet range, but sadly it's not quite strict enough so the code below allows leading zeros:
my $regex = / ^ ( \d ** 1..3 <?{ $/.Int < 256 }> ) ** 4 % '.' $ /;
for <192.168.1.1 1.01.1.1 1.2.3.256> -> $candidate {
say "{$candidate} is {$candidate ~~ $regex ?? "an IP address" !! "not valid"}";
}
192.168.1.1 is an IP address 1.01.1.1 is an IP address 1.2.3.256 is not valid
A Working Regex
This next avoids matching leading zeros by ensuring that the leftmost digit in a 2 or 3 digit number
will only match 1..9
.
my $regex = / ^ ( [ <[1..9]> \d? ]? \d <?{ $/.Int < 256 }> ) ** 4 % '.' $ /;
for <192.168.1.1 1.01.1.1 10.109.0.1 10.0.0.1> -> $candidate {
say "{$candidate} is {$candidate ~~ $regex ?? "an IP address" !! "not valid"}";
}
192.168.1.1 is an IP address 1.01.1.1 is not valid 10.109.0.1 is an IP address 10.0.0.1 is an IP address
The Challenge
Making use of this regex, I adapted it to the IPv4 partition problem by first dropping the .
separator and then using the :exhaustive
adverb to find all possible matches.
multi find-ips(Str $input where / \d ** 4..12 /) {
gather {
take .list.flat.join('.')
for $input ~~ m:exhaustive
/ ^ ( <[1..9]> ** 0..2 \d <?{ $/.Int < 256 }> ) ** 4 $ /
}
}
I added a multi
variant to catch invalid input and a MAIN
for running it.
multi find-ips(Str $input) {
note "Sorry: {$input} is not a valid input, it should be 4 to 12 digits.";
exit 1
}
sub MAIN(Str $input) {
CATCH { default { .say } }
my @ips = find-ips($input);
say "Found {+@ips} potential IP address{+@ips == 1 ?? '' !! 'es'} in {$input}:";
say @ips.join("\n").indent(4);
}
./ipv4-partition.raku 25525511135
Found 2 potential IP addresses in 25525511135: 255.255.111.35 255.255.11.135
./ipv4-partition.raku 4444
Found 1 potential IP address in 4444: 4.4.4.4
./ipv4-partition.raku 11
Sorry: 11 is not a valid input, it should be 4 to 12 digits.
./ipv4-partition.raku 19216812
Found 9 potential IP addresses in 19216812: 192.168.1.2 192.16.81.2 192.16.8.12 192.1.68.12 19.216.81.2 19.216.8.12 19.21.68.12 19.2.168.12 1.92.168.12