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.