Perl Weekly Challenge: Complete Maximum Day Frequency

So, I don’t want to repeat my music selection from Seize The Day. I feel like this task needs another “day”… another day… oh. Another Day by James Taylor.

Well, with the music selection out of the way, let’s dive into Perl Weekly Challenge 276!

Task 1: Complete Day

You are given an array of integers, @hours.

Write a script to return the number of pairs that forms a complete day.

A complete day is defined as a time duration that is an exact multiple of 24 hours.

Example 1

Input: @hours = (12, 12, 30, 24, 24)
Output: 2

Pair 1: (12, 12)
Pair 2: (24, 24)

Example 2

Input: @hours = (72, 48, 24, 5)
Output: 3

Pair 1: (72, 48)
Pair 2: (72, 24)
Pair 3: (48, 24)

Example 3

Input: @hours = (12, 18, 24)
Output: 0

Approach

Ok, it says “return the number of pairs that forms a complete day” where a “complete day” is “an exact multiple of 24 hours”. And, in all of the samples, the pairs shown have two elements. And, looking at example 2, we can use elements in multiple pairs; note that even though 72, 48, and 24 only appear in the input array once, they each appear in two different pairs.

So this feels like the double loop I do so often in this challenge: loop over the list as an outer loop, then, for each element, loop over the elements AFTER that element and see if we can build a pair.

Raku

sub completeDays(@hours) {
  my @pairs;
  for 0 .. @hours.end - 1 -> $i {
    for $i + 1 .. @hours.end -> $j {
      @pairs.push([ @hours[$i], @hours[$j] ])
        if ( @hours[$i] + @hours[$j] ) % 24 == 0;
    }
  }
  my $explain = "";
  for @pairs.kv -> $i, @p {
    $explain ~= "\n" ~ "Pair " ~ ($i+1) ~ ": ("
             ~ @p.join(", ") ~ ")"
  }
  return @pairs.elems, $explain;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @hours = (12, 12, 30, 24, 24)
Output: 2

Pair 1: (12, 12)
Pair 2: (24, 24)

Example 2:
Input: @hours = (72, 48, 24, 5)
Output: 3

Pair 1: (72, 48)
Pair 2: (72, 24)
Pair 3: (48, 24)

Example 3:
Input: @hours = (12, 18, 24)
Output: 0

Perl

sub completeDays(@hours) {
  my @pairs;
  foreach my $i ( 0 .. $#hours - 1 ) {
    foreach my $j ( $i + 1 .. $#hours) {
      push @pairs, [ $hours[$i], $hours[$j] ]
        if ( $hours[$i] + $hours[$j] ) % 24 == 0;
    }
  }
  my $explain = "";
  foreach my $i ( 0 .. $#pairs ) {
    $explain .= "\n" . "Pair " . ($i+1) . ": ("
             . join(", ", @{ $pairs[$i] }) . ")"
  }
  return scalar(@pairs), $explain;
}

View the entire Perl script for this task on GitHub.

Python

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

def completeDays(hours):
    pairs = []
    for i in range(len(hours) - 1):
        for j in range(i+1, len(hours)):
            if ( hours[i] + hours[j] ) % 24 == 0:
                pairs.append([ hours[i], hours[j] ])

    explain = ""
    i = 1
    for p in pairs:
        explain += f"\nPair {i}: ({comma_join(p)})"
        i += 1

    return len(pairs), explain

View the entire Python script for this task on GitHub.

Elixir

  def makePair([], _, pairs), do: pairs
  def makePair([j], i, pairs) do
    pairs ++ [ [i, j] ]
  end
  def makePair([j | rest], i, pairs) do
    pairs = pairs ++ [ [i, j] ]
    makePair(rest, i, pairs)
  end

  def findPairs([], pairs), do: pairs
  def findPairs([i | rest], pairs) do
    canPair = Enum.filter(rest, fn j -> rem(i + j, 24) == 0 end)
    pairs = makePair(canPair, i, pairs)
    findPairs(rest, pairs)
  end

  def explainPairs([], _, explain), do: explain
  def explainPairs([pair | rest ], i, explain) do
    pairList = "(" <> Enum.join(pair, ", ") <> ")"
    pairNum  = "Pair #{to_string(i)}: "
    explainPairs(rest, i+1, explain <> "\n" <> pairNum <> pairList)
  end
  
  def completeDays(hours) do
    pairs = findPairs(hours, [])
    explain = explainPairs(pairs, 1, "")
    {length(pairs), explain}
  end

View the entire Elixir script for this task on GitHub.


Task 2: Maximum Frequency

You are given an array of positive integers, @ints.

Write a script to return the total number of elements in the given array which have the highest frequency.

Example 1

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

The maximum frequency is 2.
The elements 1 and 2 has the maximum frequency.

Example 2

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

The maximum frequency is 1.
The elements 1, 2, 3, 4 and 5 has the maximum frequency.

Approach

This feels familiar: counting up the occurrences of integers… we did this back in PWC 233.

Raku

I also remembered that Raku has a class just for counting collections of elements with integer weights: Bag. I’m also using the Lingua::Conjunction module I used back in PWC 231.

use Lingua::Conjunction;

sub maxFrequency(@ints) {
  my $freq = bag @ints;
  my $maxFreq = max($freq.values);
  my @atMax;
  for $freq.keys.sort -> $i {
    if ($freq{$i} == $maxFreq) {
      @atMax.push($i);
    }
  }
  my $explain = "The maximum frequency is $maxFreq.\n"
              ~ "The element[|s] |list| [has|have] "
              ~ "the maximum frequency.";
  return(
    $maxFreq * @atMax.elems,
    conjunction(@atMax, :str($explain))
  );
}

View the entire Raku script for this task on GitHub.

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

The maximum frequency is 2.
The elements 1 and 2 have the maximum frequency.

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

The maximum frequency is 1.
The elements 1, 2, 3, 4, and 5 have the maximum frequency.

Perl

For Perl, we can just use a Hash in place of a Bag, and the Lingua::EN::Inflexion module will handle making our list use commas and and properly, and inflecting our nouns and verbs.

use List::Util qw( max );
use Lingua::EN::Inflexion qw( inflect wordlist );

sub maxFrequency(@ints) {
  my %freq;
  map { $freq{$_}++ } @ints;
  my $maxFreq = max(values %freq);
  my @atMax;
  foreach my $i ( sort keys %freq ) {
    if ($freq{$i} == $maxFreq) {
      push @atMax, $i;
    }
  }
  my $list = wordlist(@atMax);
  my $count = scalar(@atMax);
  my $explain = "<#d:$count>The maximum frequency is $maxFreq.\n"
              . "The <N:element> $list <V:has> "
              . "the maximum frequency.";
  return(
    $maxFreq * $count,
    inflect($explain)
  );
}

View the entire Perl script for this task on GitHub.

Python

In Python, we want to use the Counter type in the collections module as our Bag stand-in, and I’m pulling in the conjuction function I wrote for PWC 247.

from collections import Counter

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

def conjunction(ints):
    if len(ints) < 2:
        return(ints)
    elif len(ints) == 2:
        return(f'{ints[0]} and {ints[1]}')
    else:
        last = ints.pop(-1)
        l = comma_join(ints)
        return(f'{l}, and {last}')

def maxFrequency(ints):
    freq = Counter(ints)
    maxFreq = max(freq.values())
    atMax = []
    for i in sorted(freq.keys()):
        if freq[i] == maxFreq:
            atMax.append(i)
    numList  = conjunction(atMax)
    elements = "elements" if len(atMax) > 1 else "element"
    have     = "have"     if len(atMax) > 1 else "has"
    explain = (
        f"The maximum frequency is {maxFreq}.\n" +
        f"The {elements} {numList} {have} " +
        f"the maximum frequency."
    )
    return (maxFreq * len(atMax), explain)

View the entire Python script for this task on GitHub.

Elixir

In Elixir, there’s an external module for working with Bags called Multiset, but because pulling an external module into a single-file Elixir script isn’t easy, I just lifted the code to implement the pieces of the module I needed right into my script.

  def conjunction(l) do
    if length(l) < 2 do
      l
    else
      if length(l) == 2 do
        to_string(List.first(l)) <> " and " <>
        to_string(List.last(l))
      else
        {last, rest} = List.pop_at(l, -1)
        Enum.join(rest, ", ") <> ", and " <> to_string(last)
      end
    end
  end

  def maxFrequency(ints) do
    freq = Multiset.new(ints)
    maxFreq = Enum.max(Multiset.multiplicities(freq))
    atMax = Enum.filter(Multiset.values(freq), fn i ->
      Multiset.multiplicity(freq, i) == maxFreq
    end)

    numList = conjunction(atMax)
    {elements, have} = if length(atMax) > 1 do
      {"elements", "have"}
    else
      {"element", "has"}
    end
    explain = "The maximum frequency is #{maxFreq}.\n" <>
        "The #{elements} #{numList} #{have} " <>
        "the maximum frequency."

    {maxFreq * length(atMax), explain}
  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-276/packy-anderson