Christmas Twelfth
Cometh Perl 6

by Arne Sommer

Christmas Twelfth Cometh Perl 6

Published 17. October 2019. Updated 3. November 2019.

It has just been announced that «Perl 6» will be renamed to «Raku». This article uses «Perl 6», as that is still the official name.

This is my response to the Perl Weekly Challenge #30.

Challenge 30.1

Write a script to list dates for Sunday Christmas between 2019 and 2100. For example, 25 Dec 2022 is Sunday.

This is easy. We start with a list (or rather a sequence) of the years, and create a new «Date» object for the 25. December of each year. The «day-of-week» method gives us the (you guessed it) day of the week, where 1 is Monday and so on up to 7 for Sunday.

File: sun-x-mas-where
unit sub MAIN (Int :$from = 2019, Int :$to where $to >= $from = 2100);

for $from .. $to -> $year
{
  say "25 Dec $year is Sunday." if Date.new($year, 12, 25).day-of-week == 7;
}

I have added support for user specifed start and end years, and have added a «where» clause on the end year demanding that it is the same or greater than the start year.

Running it:

$ perl6 sun-x-mas-where
25 Dec 2022 is Sunday.
25 Dec 2033 is Sunday.
25 Dec 2039 is Sunday.
25 Dec 2044 is Sunday.
25 Dec 2050 is Sunday.
25 Dec 2061 is Sunday.
25 Dec 2067 is Sunday.
25 Dec 2072 is Sunday.
25 Dec 2078 is Sunday.
25 Dec 2089 is Sunday.
25 Dec 2095 is Sunday.

With user specified upper and lower limits:

$ perl6 sun-x-mas-where --from=1982 --to=2010
25 Dec 1983 is Sunday.
25 Dec 1988 is Sunday.
25 Dec 1994 is Sunday.
25 Dec 2005 is Sunday.

See docs.perl6.org/type/Date for more information about the «Date» class.

We can get rid of the «where» clause by using «...» (a range) instead of «..» (a sequence) as the latter can count down as well. I have also changed the type of the variables from «Int» to «UInt» (Unsigned Int), to prevent negative values.

File: sun-x-mas
unit sub MAIN (UInt :$from = 2019, UInt :$to = 2100);

for $from ... $to -> $year
{
  say "25 Dec $year is Sunday." if Date.new($year, 12, 25).day-of-week == 7;
}

See docs.perl6.org/type/UInt for more information about the «UInt» type.

As a one-liner, just for fun (with a couple of newlines added for readability):

File: sun-x-mas-oneliner
say "25 Dec $_ is Sunday."
  if Date.new($_, 12, 25).day-of-week == 7
    for @*ARGS[0] // 2019 ... @*ARGS[1] // 2100;

Note that you loose the named arguments, and must specify the limits as positional arguments. Specify none, the start, or both start and stop:

$ perl6 sun-x-mas-oneliner 2090
25 Dec 2095 is Sunday.

If you don't think that «if» and (especially) «for» belong in a one-liner, use «map» and «grep». This is also a one-liner, but again with newlines added for readability:

File: sun-x-mas-grep
(@*ARGS[0] // 2019 ... @*ARGS[1] // 2100)
  .grep({ Date.new($_, 12, 25).day-of-week == 7 })
  .map({ say "25 Dec $_ is Sunday." });

The traditional «if» and «for» approach is easier to understand.

All versions of the program give the same result, but the error handling (and reporting) differ.

Challenge 30.2

Write a script to print all possible series of 3 numbers, where in each series at least one of the number is even and sum of the three numbers is always 12. For example, 3,4,5.

The «at least one of the number is even» clause is redundant, as it is necessary to get an even sum out of three integers:

  • odd + odd + odd = odd
  • even + odd + odd = even
  • even + even + odd = odd
  • even + even + even = even

The number of solutions is infinite if we allow non-integers or negative integers, so I have chosen to disregard them.

We can write this as a one-liner:

File: series-3
.say for (1..10, 1..10, 1..10).flat.combinations(3).unique(:with(&[eqv]))
# 6  # 1 ####################### # 2 ## 3 ############ 4 ##################
  .grep(*.sum == 12);
  # 5 ##############

[1] The numbers can occur several times, so we set up a list with three instances of each of them. I stop at 10, as that is the highest number that works (10 + 1 + 1 = 12).

[2] The parens gave us a list with three elemenst (each consisting of a sequence 1..10), so we apply «flat» to get a single list.

[3] This gives is all the possible combinations of three elements from the list. The combinations are unique, as in from unique positions in the input list. When we have duplicates, as we have here, we get duplicates in the result.

[4] This removes duplicate lists. A bare «unique» doesn't work on lists, so we specify that the comparison should use the «eqv» operator.

[5] The we use «grep» to choose the lists where the sum is 12.

[5] And finally we print the lists, one by one.

See docs.perl6.org/type/List#routine_combinations for more information about the «combinations» method.

See docs.perl6.org/routine/unique for more information about the «unique» method.

See docs.perl6.org/routine/eqv for more information about the «eqv» operator.

Running it:

$ perl6 series-3
(1 2 9)
(1 3 8)
(1 4 7)
(1 5 6)
(1 6 5)
(1 7 4)
(1 8 3)
(1 9 2)
(1 10 1)
(1 1 10)
(2 3 7)
(2 4 6)
(2 5 5)
(2 6 4)
(2 7 3)
(2 8 2)
(2 9 1)
(2 1 9)
(2 2 8)
(3 4 5)
(3 5 4)
(3 6 3)
(3 7 2)
(3 8 1)
(3 1 8)
(3 2 7)
(3 3 6)
(4 5 3)
(4 6 2)
(4 7 1)
(4 1 7)
(4 2 6)
(4 3 5)
(4 4 4)
(5 6 1)
(5 1 6)
(5 2 5)
(5 3 4)
(5 4 3)
(5 5 2)
(6 1 5)
(6 2 4)
(6 3 3)
(6 4 2)
(6 5 1)
(7 1 4)
(7 2 3)
(7 3 2)
(7 4 1)
(8 1 3)
(8 2 2)
(8 3 1)
(9 1 2)
(9 2 1)
(10 1 1)

Duplicates?

Note that we do get duplicates, if we disregard the order, actually 5/6 of the result (e.g. «(1 2 9)», «(1 9 2)», «(2 1 9)», «(2 9 1)», «(9 1 2)» and «(9 2 1)») are equal.

So let us remove them:

File: series-3-unduplicated
.say for (1..10, 1..10, 1..10).flat.combinations(3)>>.sort.unique(:with(&[eqv])).grep(*.sum == 12);

This doesn't work:

$ perl6 series-3-unduplicated
The iterator of this Seq is already in use/consumed by another Seq
(you might solve this by adding .cache on usages of the Seq, or
by assigning the Seq into an array)
  in block <unit> at series-3-unduplicated line 3

The «>>.sort» part sorts the order of the sublists, so that we can ge trid of duplicates.

I am unable to sort it out. Explicit array assignment at each step doesn't help:

File: series-3-unduplicated2
my @source = (1..10, 1..10, 1..10).flat;
my @comb   = @source.combinations(3)>>.sort;

say @comb.WHAT;

my @unique = @comb.unique(:with(&[eqv]));
my @result = @unique.grep(*.sum == 12);

.say for @result;
$ perl6 series-3-unduplicated2 
(Array)
The iterator of this Seq is already in use/consumed by another Seq
(you might solve this by adding .cache on usages of the Seq, or
by assigning the Seq into an array)
  in block <unit> at series-3-unduplicated2 line 8

The error comes from the «@unique ...» line. I don't get it, as the input is an array (as shown by the «say» line above it, and not a Sequence. (I'll update the article if anybody helps me out.)

I can cheat:

File: series-3-cheating
my %seen;

for (1 .. 10, 1..10, 1..10).flat.combinations(3).unique(:with(&[eqv])).grep(*.sum == 12)
{
  my @sorted = $_.sort;

  next if %seen{@sorted.Str};
  say @sorted;
  %seen{@sorted.Str} = True;
}

Running it:

$ perl6 series-3-cheating 
[1 2 9]
[1 3 8]
[1 4 7]
[1 5 6]
[1 1 10]
[2 3 7]
[2 4 6]
[2 5 5]
[2 2 8]
[3 4 5]
[3 3 6]
[4 4 4]

Note that the lines in the output isn't sorted. That can be fixed, but I'll leave that as an excercise for the readers.

And that's it.

Addendum by David Hoekman

When I saw the problem you were having, by initial reaction was look for a syntactic fix: How to wedge in the necessary slip operation? (obvious difficulty being there's no good place to put '|'...) However, there's a pretty clean resolution to the issue, as 'slip' is also available as a routine, and can be coerced into the method call chain with '&':

my @comb = (1..10, 1..10, 1..10).flat.combinations(3)>>.sort>>.&slip;

And because I love the cross operator (which also has the advantage of returned the results in sorted order):

my @comb = (1..10 X[xx] 3).flat.combinations(3)>>.sort>>.&slip;

Having done that, I figured I should try to understand why this was needed, and why it worked. According to the docs, both 'combinations' and 'sort' return a Seq:

multi method combinations(List:D: Int() $of --> Seq:D)
multi method sort(List:D: --> Seq:D)

So the story would be: >>.sort consumes the sequences emitted by combinations and, one by one, emits a new set of sequences. To handle each one of those in turn, another 'hyper' operation is needed, calling 'slip' on each element of that list: >>.&slip.

Very much enjoy reading your solutions of the PWC! (especially when it makes me think...)