Magical Together
with Raku

by Arne Sommer

Magical Together with Raku

[207] Published 23. October 2022.

This is my response to The Weekly Challenge #187.

Challenge #187.1: Days Together

Two friends, Foo and Bar gone on holidays seperately to the same city. You are given their schedule i.e. start date and end date.

To keep the task simple, the date is in the form DD-MM and all dates belong to the same calendar year i.e. between 01-01 and 31-12. Also the year is non-leap year and both dates are inclusive.

Write a script to find out for the given schedule, how many days they spent together in the city, if at all.

Example 1:
Input: Foo => SD: '12-01' ED: '20-01'
       Bar => SD: '15-01' ED: '18-01'

Output: 4 days
Example 2:
Input: Foo => SD: '02-03' ED: '12-03'
       Bar => SD: '13-03' ED: '14-03'

Output: 0 day
Example 3:
Input: Foo => SD: '02-03' ED: '12-03'
       Bar => SD: '11-03' ED: '15-03'

Output: 2 days
Example 4:
Input: Foo => SD: '30-03' ED: '05-04'
       Bar => SD: '28-03' ED: '02-04'

Output: 4 days

This is relatively easy, using Date objects and the day-of-year method.

File: days-together
#! /usr/bin/env raku

unit sub MAIN (:v(:$verbose));

my $year = 2022;                                                     # [1] 

say days-together({ 'Foo' => { 'SD' => '12-01', 'ED' => '20-01' },   # [2]
                    'Bar' => { 'SD' => '15-01', 'ED' => '18-01' }});


say days-together({ 'Foo' => { 'SD' => '02-03', 'ED' => '12-03' },
                    'Bar' => { 'SD' => '13-03', 'ED' => '14-03' }});


say days-together({ 'Foo' => { 'SD' => '02-03', 'ED' => '12-03' },
                    'Bar' => { 'SD' => '11-03', 'ED' => '15-03' }});

say days-together({ 'Foo' => { 'SD' => '30-03', 'ED' => '05-04' },
                    'Bar' => { 'SD' => '28-03', 'ED' => '02-04' }});


sub days-together ($struct)                                          # [3]
{
  my $sf = Date.new($year ~ "-"                                      # [4]
    ~ $struct<Foo>.<SD>.split("-").reverse.join("-")).day-of-year;

  my $ef = Date.new($year ~ "-"
    ~ $struct<Foo>.<ED>.split("-").reverse.join("-")).day-of-year;

  my $sb = Date.new($year ~ "-"
    ~ $struct<Bar>.<SD>.split("-").reverse.join("-")).day-of-year;

  my $eb = Date.new($year ~ "-"
    ~ $struct<Bar>.<ED>.split("-").reverse.join("-")).day-of-year;

  say ": $sf, $ef, $sb, $eb" if $verbose;                            # [5]

  return 0 if $sf > $eb;                                             # [6]
  return 0 if $sb > $ef;                                             # [6a]

  my $start = max($sf, $sb);                                         # [7]
  my $end   = min($ef, $eb);                                         # [8]

  return $end - $start + 1;                                          # [9]
}

[1] We need a non-leap year for correct calculation of the February/March transition, and 2022 (the current year) just happens to be one.

[2] I am unsure if we should actually take the input format literally, but it sure was fun to so so.

[3] The procedure doing the job.

[4] Create a Date object. Note the syntax; first the year (from [1]), then the month and day of month - which is the reverse of the input format. So we split the input, reverse the two values, and join them together again. Then we apply the day-of-year method to get the accumulated day number in the year.

sf stands for «start Foo», ef stands for «end Foo». The b in the third and fourth variables stands for Bar. Why not use $start-foo and so on, you may ask. Why not indeed...

See docs.raku.org/type/Date for information about the Date class.

See docs.raku.org/type/Date#(Dateish)_method_day-of-year for more for information about the day-of-year method.

[5] You can use verbose mode to get the four day-of-year values, if you want to see them.

[6] The two periods do not overlap if the Foo one starts after the Bar one has ended. (And vice versa [6a].)

[7] If we get here, we know that the periods do overlap. Get the latest start date.

[8] and the latest end date. This is the overlap period.

[9] The actual number of days is the difference + 1, as both the start and end dates should be included in the tally.

Running it:

$ ./days-together 
4
0
2
4

Looking good.

With verbose mode:

$ ./days-together -v
: 12, 20, 15, 18
4
: 61, 71, 72, 73
0
: 61, 71, 70, 74
2
: 89, 95, 87, 92
4

Challenge #187.2: Magical Triplets

You are given a list of positive numbers, @n, having at least 3 numbers.

Write a script to find the triplets (a, b, c) from the given list that satisfies the following rules.

1. a + b > c
2. b + c > a
3. a + c > b
4. a + b + c is maximum.
In case, you end up with more than one triplets having the maximum then pick the triplet where a >= b >= c.

Example 1:
    Input: @n = (1, 2, 3, 2);
    Output: (3, 2, 2)
Example 2:
    Input: @n = (1, 3, 2);
    Output: ()
Example 3:
    Input: @n = (1, 1, 2, 3);
    Output: ()
Example 4:
    Input: @n = (2, 4, 3);
    Output: (4, 3, 2)

Let us start with rule 4; the highest sum of any triplet selection. Let us do it in REPL:

combinations(3) gives us all the possible cominations of three values from a list with (in this case) five elements:

> <1 2 3 4 5>.combinations(3)
((1 2 3) (1 2 4) (1 2 5) (1 3 4) (1 3 5) (1 4 5) (2 3 4) (2 3 5) (2 4 5) (3 4 5))

See docs.raku.org/routine/combinations for more information about combinations.

The sum of each list (or combination):

> <1 2 3 4 5 6>.combinations(3)>>.sum
(6 7 8 8 9 10 9 10 11 12)

And finally the highest value:

> <1 2 3 4 5>.combinations(3)>>.sum.max
12

This just happens to be the last element in the list of sums, as the input list was sorted in ascending order - and combinations seems to retain that order. We should not assume that this is will always be the case. (And it is indeed not the case in three of the examples.)

Then the triplets. The first list we got (see above) includes all the combinations (as a set, where order does not matter), but order do matter to us. We can slap on permutations to get the permutations of each list.

> <1 2 3 4 5>.combinations(3)>>.permutations
(((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1)) \
 ((1 2 4) (1 4 2) (2 1 4) (2 4 1) (4 1 2) (4 2 1)) \
 ((1 2 5) (1 5 2) (2 1 5) (2 5 1) (5 1 2) (5 2 1)) \
 ((1 3 4) (1 4 3) (3 1 4) (3 4 1) (4 1 3) (4 3 1)) \
 ((1 3 5) (1 5 3) (3 1 5) (3 5 1) (5 1 3) (5 3 1)) \
 ((1 4 5) (1 5 4) (4 1 5) (4 5 1) (5 1 4) (5 4 1)) \
 ((2 3 4) (2 4 3) (3 2 4) (3 4 2) (4 2 3) (4 3 2)) \
 ((2 3 5) (2 5 3) (3 2 5) (3 5 2) (5 2 3) (5 3 2)) \
 ((2 4 5) (2 5 4) (4 2 5) (4 5 2) (5 2 4) (5 4 2)) \
 ((3 4 5) (3 5 4) (4 3 5) (4 5 3) (5 3 4) (5 4 3)))

See docs.raku.org/routine/permutations for more information about permutations.

Each row (I have added the newlines) correspond to a sublist in the original combination result, as shown above.

Two levels (of lists inside lists) would be better. We can use [*;*] to flatten the two levels down to one. (The number of semicolons gives the number of levels to flatten, and the only other character allowed is the * (a whatever star).)

> <1 2 3 4 5>.combinations(3)>>.permutations[*;*]
((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1) (1 2 4) (1 4 2) (2 1 4) \
 (2 4 1) (4 1 2) (4 2 1) (1 2 5) (1 5 2) (2 1 5) (2 5 1) (5 1 2) (5 2 1) \
 (1 3 4) (1 4 3) (3 1 4) (3 4 1) (4 1 3) (4 3 1) (1 3 5) (1 5 3) (3 1 5) \
 (3 5 1) (5 1 3) (5 3 1) (1 4 5) (1 5 4) (4 1 5) (4 5 1) (5 1 4) (5 4 1) \
 (2 3 4) (2 4 3) (3 2 4) (3 4 2) (4 2 3) (4 3 2) (2 3 5) (2 5 3) (3 2 5) \
 (3 5 2) (5 2 3) (5 3 2) (2 4 5) (2 5 4) (4 2 5) (4 5 2) (5 2 4) (5 4 2) \
 (3 4 5) (3 5 4) (4 3 5) (4 5 3) (5 3 4) (5 4 3))

See docs.raku.org/language/subscripts#index-entry-flattening_ for more information about [*;*].

So far, so good. Byt what about duplicates - which we get if we have duplicate values in the input list:

> <1 1 1 1 2>.combinations(3)>>.permutations[*;*]
((1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) \
 (1 1 1) (1 1 1) (1 1 1) (1 1 2) (1 2 1) (1 1 2) (1 2 1) (2 1 1) (2 1 1) \
 ...

Getting rid of duplicates with a plain unique does not work:

> <1 1 1 1 2>.combinations(3)>>.permutations[*;*].unique
((1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) (1 1 1) \
 (1 1 1) (1 1 1) (1 1 1) (1 1 2) (1 2 1) (1 1 2) (1 2 1) (2 1 1) (2 1 1) \
 ...

The Equvivalence Operator eqv is the thing. We can specify that unique, which by default considers scalar values only, should use that to compare them:

> <1 1 1 1 2>.combinations(3)>>.permutations[*;*].unique(:with(&[eqv]))
((1 1 1) (1 1 2) (1 2 1) (2 1 1))

See docs.raku.org/routine/eqv for more information about the Equivalence Operator eqv.

Then we can do the progran:

File: magical-triplets
#! /usr/bin/env raku

unit sub MAIN (*@n where @n.elems >= 3, :v(:$verbose));   # [1]

my $max = @n.combinations(3)>>.sum.max;

say ":Max: $max" if $verbose;

my @candidates                                            # [2]
  = @n>>.Int.combinations(3)>>.permutations[*;*].unique(:with(&[eqv]));

say ":Permutations: { @candidates.raku }" if $verbose;

my @ok = @candidates.grep( { $_[0] + $_[1] > $_[2] &&     # [3]
                             $_[1] + $_[2] > $_[0] &&
	                     $_[0] + $_[2] > $_[1] &&
	  		     $_.sum == $max });

say ":Rule 1-4 applied: { @ok.raku }" if $verbose;

if @ok.elems == 0                                         # [4]
{
  say "()";
}
elsif @ok.elems == 1                                      # [5]
{
  say  "(", @ok[0].join(", "), ")";
}
else                                                      # [6]
{
  say "(", @ok.grep( { $_[0] >= $_[1] >= $_[2] })[0].join(", "), ")";
}

[1] Ensure that we get at least 3 values. Note the missing argument type check.

[2] The >>. part is there as we get the input from the command line, and that gives us values of the IntStr type - which will lead to massive output from the raku method in the verbose output. This enforcer can be removed if you do not care about that. The result will be correct either way.

See docs.raku.org/type/IntStr for more information about the IntStr type.

[3] Apply the four rules, with grep.

[4] No result? Say so.

[5] Exactly one result? Print it. Note that we have a list with one element, which itself is a list. Thus the [0] index. (The first method is perhaps nicer on the eyes, but it is longer...)

[6] More than one result? Apply the bonus rule (with grep) and print the first (and hopefully only) element in the resulting list.

Running it:

$ ./magical-triplets 1 2 3 2
(3, 2, 2)

$ ./magical-triplets 1 3 2
()

$ ./magical-triplets 1 1 2 3
()

$ ./magical-triplets 2 4 3
(4, 3, 2)

Looking good.

With verbose mode:

$ ./magical-triplets -v 1 2 3 2
:Max: 7
:Permutations: [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), \
  (3, 2, 1), (1, 2, 2), (2, 1, 2), (2, 2, 1), (2, 3, 2), (2, 2, 3), \
  (3, 2, 2)]
:Rule 1-4 applied: [(2, 3, 2), (2, 2, 3), (3, 2, 2)]
(3, 2, 2)

$ ./magical-triplets -v 1 3 2
:Max: 6
:Permutations: [(1, 3, 2), (1, 2, 3), (3, 1, 2), (3, 2, 1), (2, 1, 3), \
  (2, 3, 1)]
:Rule 1-4 applied: []
()

$ ./magical-triplets -v 1 1 2 3
:Max: 6
:Permutations: [(1, 1, 2), (1, 2, 1), (2, 1, 1), (1, 1, 3), (1, 3, 1), \
  (3, 1, 1), (1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), \
  (3, 2, 1)]
:Rule 1-4 applied: []
()

$ ./magical-triplets -v 2 4 3
:Max: 9
:Permutations: [(2, 4, 3), (2, 3, 4), (4, 2, 3), (4, 3, 2), (3, 2, 4), \
  (3, 4, 2)]
:Rule 1-4 applied: [(2, 4, 3), (2, 3, 4), (4, 2, 3), (4, 3, 2), (3, 2, 4), \
  (3, 4, 2)]
(4, 3, 2)

A final one, with one result so that we do not have to apply the bonus rule:

$ ./magical-triplets -v 1 1 1
:Max: 3
:Permutations: [(1, 1, 1),]
:Rule 1-4 applied: [(1, 1, 1),]
(1, 1, 1)

And that's it.