Perl Weekly Challenge: Defanged Addresses & String Scores

This week, we’re “defanging” IP addresses and calculating string scores, but both these tasks are easy enough we’ll be there in a minute.

Onward to Perl Weekly Challenge 272!

Task 1: Defang IP Address

You are given a valid IPv4 address.

Write a script to return the defanged version of the given IP address.

A defanged IP address replaces every period “.” with “[.]”.

Example 1

Input: $ip = "1.1.1.1"
Output: "1[.]1[.]1[.]1"

Example 2

Input: $ip = "255.101.1.0"
Output: "255[.]101[.]1[.]0"

Approach

I have to admit I’d never heard of this process called “defanging” and IP address. My first thought was this was a process to turn the string into a regular expression that would match the IP address itself instead of any string with the numeric parts of the IP address separated by practically any character (since . matches most single characters in a regex), but a quick google search on the term shows it’s also used to “prevent the IP address from being accidentally used in a context where it might represent an actionable hyperlink, such as in documentation or logs.”

I had to look away quickly, though, because the next thing I started seeing was how to accomplish that in several languages. Fortunately, I’d already decided how to do it—since [.] looks like one of the ways to escape a period in a regular expression, I knew I wanted to use regular expressions to do this transformation.

Raku

We’re capturing the output of the regular expression match in Raku’s Match Variable, $/.

sub defang($ip) {
  $ip ~~ / (\d+) '.' (\d+) '.' (\d+) '.' (\d+) /;
  return @$/.join('[.]');
}
$ raku/ch-1.raku
Example 1:
Input: $ip = "1.1.1.1"
Output: "1[.]1[.]1[.]1"

Example 2:
Input: $ip = "255.101.1.0"
Output: "255[.]101[.]1[.]0"

View the entire Raku script for this task on GitHub.

Perl

But in Perl, we can use the easier to read @{^CAPTURE} variable.

sub defang($ip) {
  $ip =~ / (\d+) [.] (\d+) [.] (\d+) [.] (\d+) /x;
  return join('[.]', @{^CAPTURE});
}

View the entire Perl script for this task on GitHub.

Python

Python returns a Match object that we can then call the method .group on to get the captured strings.

import re

def defang(ip):
    match = re.search(
        r'(\d+) [.] (\d+) [.] (\d+) [.] (\d+)',
        ip,
        re.X
    )
    return '[.]'.join(match.group(1,2,3,4))

View the entire Python script for this task on GitHub.

Elixir

In Elixir, Regex.run/3 returns “all captured subpatterns including the complete matching string” unless you pass in the :capture option specifying :all_but_first, which is “all but the first matching subpattern, i.e. all explicitly captured subpatterns, but not the complete matching part of the string.”

  def defang(ip) do
    Regex.run(
      ~r/(\d+)[.](\d+)[.](\d+)[.](\d+)/,
      ip,
      capture: :all_but_first
    )
    |> Enum.join("[.]")
  end

View the entire Elixir script for this task on GitHub.


Task 2: String Score

You are given a string, $str.

Write a script to return the score of the given string.

The score of a string is defined as the sum of the absolute difference between the ASCII values of adjacent characters.

Example 1

Input: $str = "hello"
Output: 13

ASCII values of characters:
h = 104
e = 101
l = 108
l = 108
o = 111

Score => |104 - 101| + |101 - 108| + |108 - 108| + |108 - 111|
      => 3 + 7 + 0 + 3
      => 13

Example 2

Input: "perl"
Output: 30

ASCII values of characters:
p = 112
e = 101
r = 114
l = 108

Score => |112 - 101| + |101 - 114| + |114 - 108|
      => 11 + 13 + 6
      => 30

Example 3

Input: "raku"
Output: 37

ASCII values of characters:
r = 114
a = 97
k = 107
u = 117

Score => |114 - 97| + |97 - 107| + |107 - 117|
      => 17 + 10 + 10
      => 37

Approach

This is another straightforward problem: split the string into a list of characters, get the ASCII values of each of the characters, pull the first value off the list and store it in a $last value, then loop over the remaining list pulling the next value off the list and putting it in $next, calculating the absolute value of $last - $next, adding that to a running sum, moving $next to $last and repeating until the list is empty.

Raku

I’m using a bunch of intermediate variables because I want to be able to use those values in different places, i.e, both the calculations and the running explanation.

sub score($str) {
  my @chars = $str.comb;
  my @vals  = @chars.map: { .ord };
  my @explain = ("ASCII values of characters:");
  for @chars Z @vals -> ($c, $v) {
    @explain.push: "$c = $v";
  }
  my @line1;
  my @line2;

  my $last = @vals.shift;
  while (my $next = @vals.shift) {
    @line1.push: "| $last - $next |";
    @line2.push: abs($last - $next);
    $last = $next;
  }
  @explain.push: "Score => " ~ @line1.join(" + ");
  @explain.push: "      => " ~ @line2.join(" + ");
  my $score = [+] @line2;
  @explain.push: "      => " ~ $score;

  return $score, @explain.join("\n");
}
$ raku/ch-2.raku
Example 1:
Input: $str = "hello"
Output: 13

ASCII values of characters:
h = 104
e = 101
l = 108
l = 108
o = 111
Score => | 104 - 101 | + | 101 - 108 | + | 108 - 108 | + | 108 - 111 |
      => 3 + 7 + 0 + 3
      => 13

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

ASCII values of characters:
p = 112
e = 101
r = 114
l = 108
Score => | 112 - 101 | + | 101 - 114 | + | 114 - 108 |
      => 11 + 13 + 6
      => 30

Example 3:
Input: $str = "raku"
Output: 37

ASCII values of characters:
r = 114
a = 97
k = 107
u = 117
Score => | 114 - 97 | + | 97 - 107 | + | 107 - 117 |
      => 17 + 10 + 10
      => 37

View the entire Raku script for this task on GitHub.

Perl

The Perl looks a lot like the Raku (or is that the other way around), but we need to pull in List::Util’s sum and zip functions to substitute for Raku’s built-in [+] and Z.

use List::Util qw( sum zip );

sub score($str) {
  my @chars = split //, $str;
  my @vals  = map { ord($_) } @chars;
  my @explain = ("ASCII values of characters:");
  foreach my $z ( zip \@chars, \@vals ) {
    my ($c, $v) = @$z;
    push @explain, "$c = $v";
  }
  my @line1;
  my @line2;

  my $last = shift @vals;
  while (my $next = shift @vals) {
    push @line1, "| $last - $next |";
    push @line2, abs($last - $next);
    $last = $next;
  }
  push @explain, "Score => " . join(" + ", @line1);
  push @explain, "      => " . join(" + ", @line2);
  my $score = sum @line2;
  push @explain, "      => " . $score;

  return $score, join("\n", @explain);
}

View the entire Perl script for this task on GitHub.

Python

def score(strVal):
    chars = [ c for c in strVal ]
    vals  = [ ord(c) for c in chars ]
    explain = [ "ASCII values of characters:" ]
    for c, v in zip(chars, vals):
        explain.append(f"{c} = {v}")

    line1 = []
    line2 = []

    last = vals.pop(0)
    while vals:
        next = vals.pop(0)
        line1.append(f"| {last} - {next} |")
        line2.append(abs(last - next))
        last = next

    explain.append("Score => " + " + ".join(line1))
    explain.append("      => " + " + ".join(map(lambda i: str(i), line2)))
    score = sum(line2)
    explain.append(f"      => {score}")

    return score, "\n".join(explain)

View the entire Python script for this task on GitHub.

Elixir

This time, I remembered that to process a list and not produce a map of that list, I needed to process it recursively. Note that each recursive function has a halting form that when the list feeding it is empty, it returns the values we’ve been building.

  # build up the lines like "h = 104"
  def explain_mapping([], [], explain), do: explain
  def explain_mapping(, [v | vals], explain) do
    explain_mapping(chars, vals, explain ++ ["#{c} = #{v}"])
  end

  # process the list of codepoints and explain our calculations
  def process_list([], _, line1, line2), do: {line1, line2}
  def process_list([next | vals], last, line1, line2) do
    line1 = line1 ++ ["| #{last} - #{next} |"]
    line2 = line2 ++ [ abs(last - next) ]
    process_list(vals, next, line1, line2)
  end

  def score(str) do
    chars = String.graphemes(str)
    vals  = String.to_charlist(str)

    # generate the first part of the explanation
    explain = explain_mapping(
      chars, vals, ["ASCII values of characters:"]
    )

    # get the first codepoint off the list
    {last, vals} = List.pop_at(vals, 0)
    # process the rest of the codepoints
    {line1, line2} = process_list(vals, last, [], [])

    # now format the last part of the explanation
    line1str = Enum.join(line1, " + ")
    explain  = explain ++ ["Score => #{line1str}"]
    line2str = Enum.join(line2, " + ")
    explain  = explain ++ ["      => #{line2str}"]
    scoreVal = Enum.sum(line2)
    explain  = explain ++ ["      => #{scoreVal}"]

    { scoreVal, Enum.join(explain, "\n") }
  end

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-272/packy-anderson