Perl Weekly Challenge: Counting to the Max!

Tonight’s music doesn’t have anything to do with the tasks; it’s just the soundtrack while I was writing up, as my wife calls it, “my cooking blog”.

Onward to the solutions for Perl Weekly Challenge 262!

Task 1: Max Positive Negative

You are given an array of integers, @ints.

Write a script to return the maximum number of either positive or negative integers in the given array.

Example 1

Input: @ints = (-3, 1, 2, -1, 3, -2, 4)
Output: 4

Count of positive integers: 4
Count of negative integers: 3
Maximum of count of positive and negative integers: 4

Example 2

Input: @ints = (-1, -2, -3, 1)
Output: 3

Count of positive integers: 1
Count of negative integers: 3
Maximum of count of positive and negative integers: 3

Example 3

Input: @ints = (1,2)
Output: 2

Count of positive integers: 2
Count of negative integers: 0
Maximum of count of positive and negative integers: 2

Approach

Really, this is two loops over the array of integers. One to count positive ints, one to count negative ints. If I do a map of the array each time and return 1 for each int I want to count and 0 for each int I don’t want to count, I can do the counting with a sum operator.

Raku

Like last week, we use Raku’s Reduction Metaoperator with addition ([+]) for the summation, and the max routine on the Any class to pick the maximum.

sub maxPosNeg(@ints) {
  my $pos = [+] @ints.map({ $_ > 0 ?? 1 !! 0 });
  my $neg = [+] @ints.map({ $_ < 0 ?? 1 !! 0 });
  my $max = max $pos, $neg;
  return (
    $max,
    (
      "Count of positive integers: $pos",
      "Count of negative integers: $neg",
      "Maximum of count of positive and " ~
      "negative integers: $max"
    ).join("\n")
  );
}

Yes, it looks like Perl.

$ raku/ch-1.raku
Example 1:
Input: @arr = (-3, 1, 2, -1, 3, -2, 4)
Output: 4

Count of positive integers: 4
Count of negative integers: 3
Maximum of count of positive and negative integers: 4

Example 2:
Input: @arr = (-1, -2, -3, 1)
Output: 3

Count of positive integers: 1
Count of negative integers: 3
Maximum of count of positive and negative integers: 3

Example 3:
Input: @arr = (1, 2)
Output: 2

Count of positive integers: 2
Count of negative integers: 0
Maximum of count of positive and negative integers: 2

View the entire Raku script for this task on GitHub.

Perl

In Perl, we can get max and sum from List::Util.

sub maxPosNeg(@ints) {
  my $pos = sum map { $_ > 0 ? 1 : 0 } @ints;
  my $neg = sum map { $_ < 0 ? 1 : 0 } @ints;
  my $max = max $pos, $neg;
  return (
    $max,
    join("\n",
      "Count of positive integers: $pos",
      "Count of negative integers: $neg",
      "Maximum of count of positive and " .
      "negative integers: $max"
    )
  );
}

View the entire Perl script for this task on GitHub.

Python

For Python, sum and max are built in and don’t need to be pulled in from a library.

def maxPosNeg(ints):
    pos = sum([1 for i in ints if i > 0])
    neg = sum([1 for i in ints if i < 0])
    maxCount = max(pos, neg)
    return (
      maxCount,
      "\n".join([
          f"Count of positive integers: {pos}",
          f"Count of negative integers: {neg}",
          f"Maximum of count of positive and " +
          f"negative integers: {maxCount}"
      ])
    )

View the entire Python script for this task on GitHub.


Task 2: Count Equal Divisible

You are given an array of integers, @ints and an integer $k.

Write a script to return the number of pairs (i, j) where

a) 0 <= i < j < size of @ints
b) ints[i] == ints[j]
c) i x j is divisible by k

Example 1

Input: @ints = (3,1,2,2,2,1,3) and $k = 2
Output: 4

(0, 6) => ints[0] == ints[6] and 0 x 6 is divisible by 2
(2, 3) => ints[2] == ints[3] and 2 x 3 is divisible by 2
(2, 4) => ints[2] == ints[4] and 2 x 4 is divisible by 2
(3, 4) => ints[3] == ints[4] and 3 x 4 is divisible by 2

Example 2

Input: @ints = (1,2,3) and $k = 1
Output: 0

Approach

Ok, let’s look at these criteria:

0 <= i < j < size of @ints. For a 0-indexed array, it means that both i and j are indices of the array (the 0 <= and < size of @ints parts) and that i < j. Not a big deal.

ints[i] == ints[j] means the numbers at these indices are the same. So Example 2 fails this criteria because none of the numbers are the same.

i x j is divisible by k. Really, this is the big condition.

As with the last task, we’re counting.

Raku

Here I’m leaning into the Raku looking like Perl.

sub countEquDiv($k, @ints) {
  my @explain;
  my $cnt = 0;
  for 0 .. @ints.end - 1 -> $i {
    for $i + 1 .. @ints.end -> $j {
      # does ints[i] == ints[j]?
      next unless @ints[$i] == @ints[$j];
      # is i x j divisible by k?
      next unless ( ($i * $j) mod $k ) == 0;
      # count the pair and explain why
      $cnt++;
      @explain.push(
        "($i, $j) => ints[$i] == ints[$j] " ~
        "and $i x $j is divisible by $k"
      );
    }
  }
  return($cnt, @explain.join("\n"));
}
$ raku/ch-2.raku
Example 1:
Input: @arr = (3, 1, 2, 2, 2, 1, 3) and $k = 2
Output: 4

(0, 6) => ints[0] == ints[6] and 0 x 6 is divisible by 2
(2, 3) => ints[2] == ints[3] and 2 x 3 is divisible by 2
(2, 4) => ints[2] == ints[4] and 2 x 4 is divisible by 2
(3, 4) => ints[3] == ints[4] and 3 x 4 is divisible by 2

Example 2:
Input: @arr = (1, 2, 3) and $k = 1
Output: 0

View the entire Raku script for this task on GitHub.

Perl

sub countEquDiv($k, @ints) {
  my @explain;
  my $cnt = 0;
  foreach my $i ( 0 .. $#ints - 1 ) {
    foreach my $j ( $i + 1 .. $#ints ) {
      # does ints[i] == ints[j]?
      next unless $ints[$i] == $ints[$j];
      # is i x j divisible by k?
      next unless ( ($i * $j) % $k ) == 0;
      # count the pair and explain why
      $cnt++;
      push @explain,
        "($i, $j) => ints[$i] == ints[$j] " .
        "and $i x $j is divisible by $k";
    }
  }
  return($cnt, join("\n", @explain));
}

View the entire Perl script for this task on GitHub.

Python

Here I’m leaning into the Python looking like Perl. I mean, except for the lack of block delimiters and sigils, how can you NOT think this looks like perl?

def countEquDiv(k, ints):
    explain = []
    cnt = 0
    for i in range(len(ints) - 1):
        for j in range(i+1, len(ints)):
            # does ints[i] == ints[j]?
            if not ints[i] == ints[j]: break
            # is i x j divisible by k?
            if not ( (i * j) % k ) == 0: break
            # count the pair and explain why
            cnt += 1
            explain.append(
                f"({i}, {j}) => ints[{i}] == ints[{j}] " +
                f"and {i} x {j} is divisible by {k}"
            )
    return(cnt, "\n".join(explain))

View the entire Python script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-262/packy-anderson

Perl Weekly Challenge: Two Elements, Multiplied by Digit Sum

No music this week, only solutions to Perl Weekly Challenge 261!

Task 1: Element Digit Sum

You are given an array of integers, @ints.

Write a script to evaluate the absolute difference between element and digit sum of the given array.

Example 1

Input: @ints = (1,2,3,45)
Output: 36

Element Sum: 1 + 2 + 3 + 45 = 51
Digit Sum: 1 + 2 + 3 + 4 + 5 = 15
Absolute Difference: | 51 - 15 | = 36

Example 2

Input: @ints = (1,12,3)
Output: 9

Element Sum: 1 + 12 + 3 = 16
Digit Sum: 1 + 1 + 2 + 3 = 7
Absolute Difference: | 16 - 7 | = 9

Example 3

Input: @ints = (1,2,3,4)
Output: 0

Element Sum: 1 + 2 + 3 + 4 = 10
Digit Sum: 1 + 2 + 3 + 4 = 10
Absolute Difference: | 10 - 10 | = 0

Example 4

Input: @ints = (236, 416, 336, 350)
Output: 1296

Approach

To me, this seems like an exercise in treating a list of numbers like integers in one case (element sum) and as a string of characters in another (digit sum).

Raku

Ok, I’ve been accused of writing my Raku like Perl, so I need to really lean into thinking in Raku, not Perl. Our solution function should accept a list, and we should probably use Raku’s Reduction Metaoperator to create our sums and our string of characters, and abs is a method on the Numeric role that numeric objects have.

sub elementDigitSum(@ints) {
  # [+] sums all the elements of @ints
  my $elementSum = [+] @ints;

  my $explain = 'Element Sum: '
              ~ @ints.join(' + ')
              ~ ' = ' ~ $elementSum;

  # use [~] to concatenate all the integers together
  # into a single string, then use split() to get the
  # individual digits
  my @digits   = ([~] @ints).split('', :skip-empty);
  # [+] sums all the elements of @digits
  my $digitSum = [+] @digits;

  $explain ~= "\n" ~ 'Digit Sum: '
              ~ @digits.join(' + ')
              ~ ' = ' ~ $digitSum;

  my $abs = ($elementSum - $digitSum).abs;

  $explain ~= "\nAbsolute Difference: "
           ~ "| $elementSum - $digitSum | = $abs";

  return ($abs, $explain);
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @ints = (1, 2, 3, 45)
Output: 36

Element Sum: 1 + 2 + 3 + 45 = 51
Digit Sum: 1 + 2 + 3 + 4 + 5 = 15
Absolute Difference: | 51 - 15 | = 36

Example 2:
Input: @ints = (1, 12, 3)
Output: 9

Element Sum: 1 + 12 + 3 = 16
Digit Sum: 1 + 1 + 2 + 3 = 7
Absolute Difference: | 16 - 7 | = 9

Example 3:
Input: @ints = (1, 2, 3, 4)
Output: 0

Element Sum: 1 + 2 + 3 + 4 = 10
Digit Sum: 1 + 2 + 3 + 4 = 10
Absolute Difference: | 10 - 10 | = 0

Example 4:
Input: @ints = (236, 416, 336, 350)
Output: 1296

Element Sum: 236 + 416 + 336 + 350 = 1338
Digit Sum: 2 + 3 + 6 + 4 + 1 + 6 + 3 + 3 + 6 + 3 + 5 + 0 = 42
Absolute Difference: | 1338 - 42 | = 1296

Perl

Sigh. Even though I tried to lean into making the Raku version use more Raku features, it turns out that [+] is just a built-in version of List::Util’s sum, and [~] is just a neater version of join('', @array).

use List::Util qw( sum );

sub elementDigitSum(@ints) {
  my $elementSum = sum @ints;

  my $explain = 'Element Sum: '
              . join(' + ',  @ints)
              . ' = ' . $elementSum;

  # use join() to concatenate all the integers together
  # into a single string, then use split() to get the
  # individual digits
  my @digits   = split //, join('', @ints);
  my $digitSum = sum @digits;

  $explain .= "\nDigit Sum: "
           . join(' + ',  @digits)
           . ' = ' . $digitSum;

  my $abs = abs($elementSum - $digitSum);

  $explain .= "\nAbsolute Difference: "
           . "| $elementSum - $digitSum | = $abs";

  return ($abs, $explain);
}

View the entire Perl script for this task on GitHub.

Python

I will, however, gladly cop to my Python looking like Perl, because it does. That’s because I don’t believe there’s a lot of difference between the languages. Though, when I’m writing my Python, I start with the Raku version because both Raku and Python have the same “everything is an object” edict at their heart.

def plus_join(arr):
    return ' + '.join(map(lambda i: str(i), arr))

def elementDigitSum(ints):
    elementSum = sum(ints)

    explain = f'Element Sum: {plus_join(ints)} = {elementSum}'

    # concatenate all the integers together into a single
    # string
    digitStr = ''.join([ str(i) for i in ints ])
    # loop over the individual digits
    digits = [ int(d) for d in digitStr ]
    digitSum = sum(digits)

    explain += "\n"
    explain += f'Digit Sum: {plus_join(digits)} = {digitSum}'

    absVal = abs(elementSum - digitSum)

    explain += "\n"
    explain += 'Absolute Difference: '
    explain += f'| {elementSum} - {digitSum} | = {absVal}'

    return (absVal, explain)

View the entire Python script for this task on GitHub.


Task 2: Multiply by Two

You are given an array of integers, @ints and an integer $start..

Write a script to do the followings:

a) Look for $start in the array @ints, if found multiply the number by 2
b) If not found stop the process otherwise repeat

In the end return the final value.

Example 1

Input: @ints = (5,3,6,1,12) and $start = 3
Output: 24

Step 1: 3 is in the array so 3 x 2 = 6
Step 2: 6 is in the array so 6 x 2 = 12
Step 3: 12 is in the array so 12 x 2 = 24

24 is not found in the array so return 24.

Example 2

Input: @ints = (1,2,4,3) and $start = 1
Output: 8

Step 1: 1 is in the array so 1 x 2 = 2
Step 2: 2 is in the array so 2 x 2 = 4
Step 3: 4 is in the array so 4 x 2 = 8

8 is not found in the array so return 8.

Example 3

Input: @ints = (5,6,7) and $start = 2
Output: 2

2 is not found in the array so return 2.

Approach

Well, this is a fairly straightforward loop, the interesting part is checking to see if $start is in the array. The boring way would be to loop over the elements of the array, but each of the languages I’m using have more interesting ways to

Raku

In Raku, we have a data type called a Set, and it has an infix (elem) operator which can be written with the unicode character ∈.

sub multiplyByTwo(@ints, $s) {
  my $start = $s; # so we can modify the value
  my $ints = Set(@ints);
  my @explain;
  my $step = 0;

  while ($start ∈ $ints) {
    $step++;
    my $old = $start;
    $start *= 2;
    @explain.push(
      "Step $step: $old is in the array so $old x 2 = $start"
    );
  }
  @explain.push(
    "$start is not in the array so return $start."
  );
  return ($start, @explain.join("\n"));
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: @ints = (5, 3, 6, 1, 12) and $start = 3
Output: 24

Step 1: 3 is in the array so 3 x 2 = 6
Step 2: 6 is in the array so 6 x 2 = 12
Step 3: 12 is in the array so 12 x 2 = 24
24 is not in the array so return 24.

Example 2:
Input: @ints = (1, 2, 4, 3) and $start = 1
Output: 8

Step 1: 1 is in the array so 1 x 2 = 2
Step 2: 2 is in the array so 2 x 2 = 4
Step 3: 4 is in the array so 4 x 2 = 8
8 is not in the array so return 8.

Example 3:
Input: @ints = (5, 6, 7) and $start = 2
Output: 2

2 is not in the array so return 2.

Perl

Perl, on the other hand, doesn’t have a Set data type, but we can easily do the same thing with a hash!

sub multiplyByTwo($start, @ints) {
  my %ints = map { $_ => 1 } @ints;
  my @explain;
  my $step = 0;

  while ( exists $ints{$start} ) {
    $step++;
    my $old = $start;
    $start *= 2;
    push @explain,
      "Step $step: $old is in the array so $old x 2 = $start";
  }
  push @explain,
    "$start is not in the array so return $start.";
  return ($start, join("\n", @explain));
}

View the entire Perl script for this task on GitHub.

Python

Python, however, does have a set datatype. In this case, because we don’t need to change the set after we create it, I’m going to use a frozenset.

def multiplyByTwo(ints, start):
    intSet = frozenset(ints)
    explain = []
    step = 0

    while start in intSet:
        step += 1
        old = start
        start *= 2
        explain.append(
          f"Step {step}: {old} is in the array " +
          f"so {old} x 2 = {start}"
        )
    explain.append(
      f"{start} is not in the array so return {start}."
    )
    return (start, "\n".join(explain))

View the entire Python script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-261/packy-anderson

Perl Weekly Challenge: Unique Dictionary Occurrences are Rank

Rank? Take note, this is probably the only time I’m evoking Lynyrd Skynyrd.

Onward to Perl Weekly Challenge 260!

Task 1: Unique Occurrences

You are given an array of integers, @ints.

Write a script to return 1 if the number of occurrences of each value in the given array is unique or 0 otherwise.

Example 1

Input: @ints = (1,2,2,1,1,3)
Output: 1

The number 1 occurred 3 times.
The number 2 occurred 2 times.
The number 3 occurred 1 time.

All occurrences are unique, therefore the output is 1.

Example 2

Input: @ints = (1,2,3)
Output: 0

Example 3

Input: @ints = (-2,0,1,-2,1,1,0,1,-2,9)
Output: 1

Approach

This immediately says hashes to me: a hash to count the number of times each integer occurs, and another hash to track whether a particular integer count occurs more than once.

Raku

sub uniqueOccurrences(@ints) {
  my %counts;
  for @ints -> $i {
    # count how many time each int occurs
    %counts{$i}++;
  }
  my %seen;
  for %counts.kv -> $i, $c {
    # if we've seen this count before, return 0
    return 0 if %seen{$c}:exists;
    %seen{$c} = $i;
  }
  # each count was unique
  return 1;
}
$ raku/ch-1.raku
Example 1:
Input: @ints = (1, 2, 2, 1, 1, 3)
Output: 1

Example 2:
Input: @ints = (1, 2, 3)
Output: 0

Example 3:
Input: @ints = (-2, 0, 1, -2, 1, 1, 0, 1, -2, 9)
Output: 1

View the entire Raku script for this task on GitHub.

Perl

The big change from Raku to Perl is going from for %counts.kv -> $i, $c to while ( my($i, $c) = each %counts ):

sub uniqueOccurrences(@ints) {
  my %counts;
  foreach my $i ( @ints ) {
    # count how many time each int occurs
    $counts{$i}++;
  }
  my %seen;
  while ( my($i, $c) = each %counts ) {
    # if we've seen this count before, return 0
    return 0 if exists $seen{$c};
    $seen{$c} = $i;
  }
  # each count was unique
  return 1;
}

View the entire Perl script for this task on GitHub.

Python

As always, when I’m counting things in Python, I use the Counter type in the collections module.

from collections import Counter

def uniqueOccurrences(ints):
  counts = Counter()
  for i in ints:
    # count how many time each int occurs
    counts[i] += 1
  seen = {}
  for i, c in counts.items():
    # if we've seen this count before, return 0
    if c in seen: return 0 
    seen[c] = i
  # each count was unique
  return 1

View the entire Python script for this task on GitHub.


Task 2: Dictionary Rank

You are given a word, $word.

Write a script to compute the dictionary rank of the given word.

Example 1

Input: $word = 'CAT'
Output: 3

All possible combinations of the letters:
CAT, CTA, ATC, TCA, ACT, TAC

Arrange them in alphabetical order:
ACT, ATC, CAT, CTA, TAC, TCA

CAT is the 3rd in the list.
Therefore the dictionary rank of CAT is 3.

Example 2

Input: $word = 'GOOGLE'
Output: 88

Example 3

Input: $word = 'SECRET'
Output: 255

Approach

This feels akin to the first task: operate on the list (of characters, this time) to produce another list, and the analyze the second list in some way. Here, we’re breaking a string into characters, producing all the permutations of those characters as new strings, then sorting them and seeing how far down the sorted list the original string appears.

Raku

In Raku, there’s a permutations method on the List type to do the heavy lifting:

sub dictionaryRank($word) {
  # split the string into an array of characters
  my @letters = $word.split('', :skip-empty);

  # find the permutations of the letters
  my @perms;
  for @letters.permutations -> @l {
    @perms.append(@l.join(''));
  }

  # find where in the sorted list of 
  # permutations the word is
  my $rank = 1;
  for @perms.unique.sort -> $p {
    return $rank if $p eq $word;
    $rank++;
  }
}
$ raku/ch-2.raku | less
Example 1:
Input: $word = 'CAT'
Output: 3

Example 2:
Input: $word = 'GOOGLE'
Output: 349

Example 3:
Input: $word = 'SECRET'
Output: 509

But wait! GOOGLE and SECRET are supposed to be 88 and 255, not 349 and 509. What gives? Let’s look at what @perms.sort looks like by adding say @perms.sort.raku; right after we build the array…

Example 2:
Input: $word = 'GOOGLE'
("EGGLOO", "EGGLOO", "EGGLOO", "EGGLOO", "EGGOLO", "EGGOLO",
 "EGGOLO", "EGGOLO", "EGGOOL", "EGGOOL", "EGGOOL", "EGGOOL",
 "EGLGOO", "EGLGOO", "EGLGOO", "EGLGOO", "EGLOGO", "EGLOGO",
 "EGLOGO", "EGLOGO", "EGLOOG", "EGLOOG", "EGLOOG", "EGLOOG",
 "EGOGLO", "EGOGLO", "EGOGLO", "EGOGLO", "EGOGOL", "EGOGOL",
...

Oh! I see what’s happening! It wants all the unique combinations! Fortunately, Raku has a unique method for just that.

sub dictionaryRank($word) {
  # split the string into an array of characters
  my @letters = $word.split('', :skip-empty);

  # find the permutations of the letters
  my @perms;
  for @letters.permutations -> @l {
    @perms.append(@l.join(''));
  }

  # find where in the sorted list of 
  # UNIQUE permutations the word is
  my $rank = 1;
  for @perms.unique.sort -> $p {
    return $rank if $p eq $word;
    $rank++;
  }
}
$ raku/ch-2.raku | less
Example 1:
Input: $word = 'CAT'
Output: 3

Example 2:
Input: $word = 'GOOGLE'
Output: 88

Example 3:
Input: $word = 'SECRET'
Output: 255

View the entire Raku script for this task on GitHub.

Perl

Perl, however, doesn’t have the same built-in features, so we need to rely on CPAN modules. For uniqueness, I’m using uniq from List::Util, and for permutations, I’m using  Algorithm::Combinatorics’ permutations function, like I did back in PWC 244.

use Algorithm::Combinatorics qw( permutations );
use List::Util qw( uniq );

sub dictionaryRank($word) {
  # split the string into an array of characters
  my @letters = split //, $word;

  # find the permutations of the letters
  my @perms;
  foreach my $l ( permutations(\@letters) ) {
    push @perms, join('', @$l);
  }

  # find where in the sorted list of 
  # UNIQUE permutations the word is
  my $rank = 1;
  foreach my $p ( sort { $a cmp $b } uniq @perms ) {
    return $rank if $p eq $word;
    $rank++;
  }
}

View the entire Perl script for this task on GitHub.

Python

Again, like I did in PWC 244, I’m using itertools, but this time it’s the permutations function.

def dictionaryRank(word):
  # we don't need to split the string, because
  # permutations() will accept a string as a parameter
  #
  # set() produces a set of unique elements
  #
  # sorted() returns a sorted list
  perms = sorted(
    set([ ''.join(l) for l in permutations(word) ])
  )
  rank = 1
  for p in perms:
    if p == word: return rank
    rank += 1

View the entire Python script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-260/packy-anderson

Perl Weekly Challenge: Ba-a-nking Day! Ba-a-nking Day! That’s all I really wanted to say…

Oh, I’m weary from waiting
In Washington, D.C.
I’m coming to see my congressman
But he’s avoiding me
Weary from waiting down in Washington, D.C.

Oh, Congresswoman
Won’t you tell that congressman
I’ve waited such a long time
I’ve about waited all I can…

La plus ça change, plus c’est la même chose. But this week, things are not the same with Perl Weekly Challenge 259!

Task 1: Banking Day Offset

You are given a start date and offset counter. Optionally you also get bank holiday date list.

Given a number (of days) and a start date, return the number (of days) adjusted to take into account non-banking days. In other words: convert a banking day offset to a calendar day offset.

Non-banking days are:

a) Weekends
b) Bank holidays

Example 1

Input: $start_date = '2018-06-28', $offset = 3, $bank_holidays = ['2018-07-03']
Output: '2018-07-04'

Thursday bumped to Wednesday (3 day offset, with Monday a bank holiday)

Example 2

Input: $start_date = '2018-06-28', $offset = 3
Output: '2018-07-03'

Approach

We’re back to date manipulation, which were the crux of PWC 227 and 237, so we’re probably going to be using date manipulation modules…

Raku

In Raku, the manipulation we need is built in, so we only need Tom Browder’s Date::Names module for getting the day-of-the-week names I use in my explanation.

use Date::Names;

sub bankingDayOffset($start, $offset, @holidays) {
  my $date = Date.new($start); # convert string to Date
  my $off  = $offset;
  my $cnt  = 0;

  # convert holidays to Date objects
  @holidays = map { Date.new($_) }, @holidays;

  # instantiate a Date::Names object
  my $dn = Date::Names.new;

  my @explain;
  my $this_day = $dn.dow($date.day-of-week);
  while ($off) {
    $date++;
    my $next_day = $dn.dow($date.day-of-week);
    if (
      $date.day-of-week == 6 || # it's a Saturday
      $date.day-of-week == 7    # it's a Sunday
    ) { 
      @explain.push:
        "$next_day skipped because it's a weekend";
    }
    elsif ($date == @holidays.any) { # it's a Holiday
      @explain.push:
        "$next_day skipped because it's a holiday";
    }
    else {
      $off--; $cnt++;
      @explain.push:
        "$this_day bumped to $next_day (offset $cnt)";
      $this_day = $next_day;
    }
  }
  return $date.gist, @explain.join("\n");
}
$ raku/ch-1.raku
Example 1:
Input: $start_date = '2018-06-28', $offset = 3,
       $bank_holidays = ['2018-07-03']
Output: '2018-07-04'

Thursday bumped to Friday (offset 1)
Saturday skipped because it's a weekend
Sunday skipped because it's a weekend
Friday bumped to Monday (offset 2)
Tuesday skipped because it's a holiday
Monday bumped to Wednesday (offset 3)

Example 2:
Input: $start_date = '2018-06-28', $offset = 3
Output: '2018-07-03'

Thursday bumped to Friday (offset 1)
Saturday skipped because it's a weekend
Sunday skipped because it's a weekend
Friday bumped to Monday (offset 2)
Monday bumped to Tuesday (offset 3)

Example 3:
Input: $start_date = '2023-12-29', $offset = 5,
       $bank_holidays = ['2024-01-01']
Output: '2024-01-08'

Saturday skipped because it's a weekend
Sunday skipped because it's a weekend
Monday skipped because it's a holiday
Friday bumped to Tuesday (offset 1)
Tuesday bumped to Wednesday (offset 2)
Wednesday bumped to Thursday (offset 3)
Thursday bumped to Friday (offset 4)
Saturday skipped because it's a weekend
Sunday skipped because it's a weekend
Friday bumped to Monday (offset 5)

View the entire Raku script for this task on GitHub.

Perl

As usual, we don’t have to adjust the approach for converting from Raku to Perl, only the particulars. Perl doesn’t have a built-in module for Date manipulation, but Time::Piece and Time::Seconds are in the core modules, and Time::Piece has a method for generating the name of the day of the week. any we get from List::Util.

use List::Util qw( any );
use Time::Piece;
use Time::Seconds qw( ONE_DAY );

sub bankingDayOffset($start, $offset, @holidays) {
  # convert string to Date
  my $date = Time::Piece->strptime($start, "%Y-%m-%d")
                        ->truncate(to => 'day');
  my $cnt  = 0;

  # convert holidays to Date objects
  @holidays = map {
    Time::Piece->strptime($_, "%Y-%m-%d")
               ->truncate(to => 'day')
  } @holidays;

  my @explain;
  my $this_day = $date->fullday;
  while ($offset) {
    $date += ONE_DAY; # add 1 day
    my $next_day = $date->fullday;
    if (
      $date->wday == 7 || # it's a Saturday
      $date->wday == 1    # it's a Sunday
    ) { 
      push @explain,
        "$next_day skipped because it's a weekend";
    }
    elsif (any { $date == $_ } @holidays) { # it's a Holiday
      push @explain,
        "$next_day skipped because it's a holiday";
    }
    else {
      $offset--; $cnt++;
      push @explain,
        "$this_day bumped to $next_day (offset $cnt)";
      $this_day = $next_day;
    }
  }
  return $date->strftime('%F'), join("\n", @explain);
}

View the entire Perl script for this task on GitHub.

Python

Again, I’m using Python’s really robust datetime module.

from datetime import date, timedelta

def bankingDayOffset(start, offset, holidays):
  d = date.fromisoformat(start) # convert string to Date
  cnt = 0

  # convert holidays to Date objects
  holidays = [ date.fromisoformat(h) for h in holidays ]

  explain = []
  this_day = d.strftime('%A')
  while offset:
    d += timedelta(days = 1) # add 1 day
    next_day = d.strftime('%A')
    if (
      d.isoweekday() == 6 or # it's a Saturday
      d.isoweekday() == 7    # it's a Sunday
    ): 
      explain.append(
        f"{next_day} skipped because it's a weekend"
      )
    elif any([d == h for h in holidays]): # it's a Holiday
      explain.append(
        f"{next_day} skipped because it's a holiday"
      )
    else:
      offset -= 1
      cnt += 1
      explain.append(
        f"{next_day} bumped to {next_day} (offset {cnt})"
      )
      this_day = next_day
  return d.strftime('%F'), "\n".join(explain)

View the entire Python script for this task on GitHub.


Task 2: Line Parser

You are given a line like below:

{%  id   field1="value1"    field2="value2"  field3=42 %}

Where

a) "id" can be \w+.
b) There can be 0  or more field-value pairs.
c) The name of the fields are \w+.
b) The values are either number in which case we don't need
   double quotes or string in which case we need double quotes
   around them.

The line parser should return structure like below:

{
       name => id,
       fields => {
           field1 => value1,
           field2 => value2,
           field3 => value3,
       }
}

It should be able to parse the following edge cases too:

{%  youtube title="Title \"quoted\" done" %}

and

{%  youtube title="Title with escaped backslash \\" %}

BONUS: Extend it to be able to handle multiline tags:

{% id  filed1="value1" ... %}
LINES
{% endid %}

You should expect the following structure from your line parser:

{
       name => id,
       fields => {
           field1 => value1,
           field2 => value2,
           field3 => value3,
       }
       text => LINES
}

Approach

Wow! This seems like a pretty big task for a PWC, but I’m up for it.

Raku

It seems pretty obvious to me that, in Raku at least, the tool for this job is a grammar. I’ve never actually written a Raku grammar before, so I went through the grammar tutorial.

The really tricky part was allowing for strings that contained escaped quotes and escaped backslashes. Finally, after a lot of confusion, I stopped trying to keep the data I was parsing in the program file itself, since I couldn’t be certain that "Title with escaped backslash \\" was indeed an escaped backslash and not being interpreted by Raku as escaping the quote. You can look at my input files here.

grammar Parser {
  rule TOP { [ <line> | <text> ] }

  rule line { '{%' <id> [ <field-value> ]* '%}' }

  # negative lookbehind and negative lookahead
  rule text { <!after 「{%」 > <-[ \n ]>+ <!before 「%}」 >}

  token id    { \w+ }
  token field { \w+ }

  token number { \d+ [ \. \d+ ]? }

  token quoted-string { '"' <string> '"' }
  token string {
    [
      <-[ " ]> # any character not a quote
      |
      「\\」     # an escaped backslash
      |
      \\\"     # an escaped quote
    ]*
    }
  
  rule field-value { <field> '=' [<number> | <quoted-string>] }
}

sub MAIN($file) {
  my %data;
  my @ids;
  my $in_id = '';
  for $file.IO.lines -> $line {
    # parse this line of the file
    my $p = Parser.parse($line);

    # is there a line with {% ... %} ?
    if ($p<line>) {
      my $id = $p<line><id>.Str;
      # is the id the end of a block?
      if (my $c = ($id ~~ / end(\w+) /)) { # capture after end
        if (%data{$c[0]}:exists) { # it is!
          $id = $c[0]; 
          $in_id = ''; # clear the id we're processing
          if (%data{$id}{'text'}) {
            # if there's text, remove the final "newline"
            %data{$id}{'text'} ~~ s/\\n$//;
          }
          next; # skip to next line of file
        }
      }
      @ids.push($id); # keep list of ids in order
      $in_id = $id; # keep track of the current id for text
      # initialize base data for this id
      %data{$id} = { name => $id };
      # if we have fields...
      if ($p<line><field-value>) {
        # loop over them and store them in the data
        for $p<line><field-value> -> $fv {
          my $field = $fv<field>;
          my $value = $fv<number> ?? $fv<number> 
                   !! $fv<quoted-string><string>;
          %data{$id}{'fields'}{$field} = $value;
        }
      }
    }
    # if we have non-{% ... %} lines and we have an ID
    elsif ($p<text> && $in_id) {
      # append a "newline" to the end
      %data{$in_id}{'text'} ~= $p<text> ~ "\\n";
    }
  }

  # dump the data
  for @ids -> $id {
    my %group = %data{$id};
    say "\{";
    say "  name => %group{'name'},";
    say "  fields => \{";
    for %group{'fields'}.keys.sort -> $k {
      say "    $k => %group{'fields'}{$k},";
    }
    say "  }";
    say "  text => %group{'text'}" if %group{'text'};
    say "\}";
  }
}

And here’s my output:

$ raku/ch-2.raku data/parser-1.txt
{
  name => id,
  fields => {
    field1 => value1,
    field2 => value2,
    field3 => 42,
  }
}

$ raku/ch-2.raku data/parser-2.txt
{
  name => youtube,
  fields => {
    title => Title \"quoted\" done,
  }
}

$ raku/ch-2.raku data/parser-3.txt
{
  name => youtube,
  fields => {
    title => Title with escaped backslash \\,
  }
}

$ raku/ch-2.raku data/parser-4.txt
{
  name => id,
  fields => {
    filed1 => value1,
  }
  text => LINES
}

$ raku/ch-2.raku data/parser-5.txt
{
  name => id,
  fields => {
    filed1 => value1,
  }
  text => LINES\nLINES\nLINES
}
{
  name => foo,
  fields => {
    foonum => 3,
  }
  text => FOO\nBAR\nBAZ
}

View the entire Raku script for this task on GitHub.

Perl

It’s after the submission deadline, but I finally got around to implementing the parser in Perl:

use v5.38;

my $ID     = qr/ (?<ID> \w+) /x;
my $FIELD  = qr/ (?<FIELD> \w+) /x;
my $NUMBER = qr/ (?<NUMBER> \d+ [ \. \d+ ]? ) /x;
my $STRING = qr/ (?<STRING> ([^"] | \\ | \\\" )+ ) /x;

my $QUOTED_STRING = qr/ (?<QUOTED_STRING> " $STRING " ) /x;
my $FIELD_VALUE = qr/ $FIELD \s* = \s* ( $NUMBER | $QUOTED_STRING ) \s* /x;
my $FIELD_VALUES = qr/ (?<FIELD_VALUES> (?: $FIELD_VALUE \s* )* ) /x;

# negative lookbehind and negative lookahead
my $TEXT = qr/ (?<TEXT> (?<! {% ) .+ (?! %} ) ) /x;
my $LINE = qr/ (?<LINE> \{% \s* $ID \s* $FIELD_VALUES \s* %\} ) /x;

my $TOP  = qr/^ (?: $LINE | $TEXT ) $/x;

my $file = shift @ARGV;
open my $fh, '<', $file;

my %data;
my @ids;
my $in_id;

while (my $line = <$fh>) {
  $line =~ /$TOP/;

  # is there a line with {% ... %} ?
  if ($+{LINE}) {
    my $id = $+{ID};
    # is the id the end of a block?
    if ($id =~ /^ end(\w+) $/x) { # capture after end
      if (exists $data{$1}) { # it is!
        $id = $1; 
        undef $in_id; # clear the id we're processing
        if ($data{$id}{'text'}) {
          # if there's text, remove the final "newline"
          $data{$id}{'text'} =~ s/\\n$//;
        }
        next; # skip to next line of file
      }
    }
    push @ids, $id; # keep list of ids in order
    $in_id = $id; # keep track of the current id for text
    # initialize base data for this id
    $data{$id} = { name => $id };
    # if we have fields...
    my $field_values = $+{FIELD_VALUES};
    # loop over field values and store them in the data
    while ($field_values =~ /$FIELD_VALUE/g) {
      my $value = $+{STRING} ? $+{STRING} : $+{NUMBER};
      if ($+{NUMBER}) {
        $value =~ s/\s+$//; # we're picking up trailing spaces
      }
      $data{$id}->{'fields'}->{ $+{FIELD} } = $value;
    }

  }
  # if we have non-{% ... %} lines and we have an ID
  elsif ($+{TEXT} && $in_id) {
    # append a "newline" to the end
    $data{$in_id}{'text'} .= $+{TEXT} . "\\n";
  }
}

use Data::Dumper::Concise;
foreach my $id (@ids) {
  print Dumper($data{$id});
}
$ perl/ch-2.pl data/parser-1.txt
{
  fields => {
    field1 => "value1",
    field2 => "value2",
    field3 => 42,
  },
  name => "id",
}

$ perl/ch-2.pl data/parser-2.txt
{
  fields => {
    title => "Title \\",
  },
  name => "youtube",
}

$ perl/ch-2.pl data/parser-3.txt
{
  fields => {
    title => "Title with escaped backslash \\\\",
  },
  name => "youtube",
}

$ perl/ch-2.pl data/parser-4.txt
{
  fields => {
    filed1 => "value1",
  },
  name => "id",
  text => "LINES",
}

$ perl/ch-2.pl data/parser-5.txt
{
  fields => {
    filed1 => "value1",
  },
  name => "id",
  text => "LINES\\nLINES\\nLINES",
}
{
  fields => {
    foonum => 3,
  },
  name => "foo",
  text => "FOO\\nBAR\\nBAZ",
}

View the entire Perl script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-259/packy-anderson