На этой неделе в Perl Weekly Challenge (#19) два задания. Первый — найти все месяцы с пятью выходными в период с 1900 по 2019 год. Второй — запрограммировать реализацию переноса слов с помощью жадного алгоритма.

Обе задачи довольно просты, и их решения могут (и должны) быть такими же. Однако на этот раз я также собираюсь сделать обратное и постепенно превратить простое решение в излишне сложное. Потому что в этом конкретном случае мы можем узнать больше, делая вещи излишне сложными способами. Итак, в этом посте мы рассмотрим даты и манипулирование датами в Perl 6 на примере задачи 1 PWC #19:

Напишите сценарий для отображения месяцев с 1900 по 2019 год, где вы найдете 5 выходных, то есть 5 пятниц, 5 суббот и 5 воскресений.

Давайте начнем с простого поиска месяцев с пятью выходными:

#!/usr/bin/env perl6
say join "\n", grep *.day-of-week == 5, map { Date.new: |$_, 1 }, do 1900..2019 X 1,3,5,7,8,10,12;

Алгоритм решения этого вопроса прост. При условии, что не только суббота и воскресенье, но и пятница должны быть пять раз, у вас А) *должен* быть 31 день, чтобы втиснуть пять выходных. И когда вы знаете, что вы также увидите, что B) последний день месяца ДОЛЖЕН быть воскресеньем и C) первый день месяца ДОЛЖЕН быть пятницей (вам не нужно проверять оба; если A true и B истинно, C также автоматически истинно).

Приведенный выше код реализует B и использует несколько приемов. Вы читаете справа налево (если только вы не пишете слева направо, вот так… say do 1900..2019 X 1,3,5,7,8,10,12 ==> map { Date.new: |$_, 1 } ==> grep *.day-of-week == 5 ==> join “\n”; )

Используя оператор X, я создаю перекрестное произведение всех лет в диапазоне 1900–2019 и месяцев 1, 3, 5, 7, 8, 10, 12 (31-дневные месяцы). В ответ я получаю последовательность, содержащую все пары год-месяц периода.

Функция карты повторяет Seq. Там он создает объект Date. Нужна небольшая песня и танец: поскольку Date.new принимает три безымянных целочисленных параметра, год, месяц и день, мне нужно что-то сделать с тем, что у меня есть — Пара с годом и месяцем. Поэтому я использую | оператор, чтобы «взорвать» пару на два целочисленных параметра для года и месяца.

Вы всегда можете использовать это для вызова подпрограммы с фиксированными параметрами, используя массив со значениями параметров, а не отдельные переменные для каждого параметра. Код ниже иллюстрирует использование:

my @list = 1, 2, 3;
sub explode-parameters($one, $two, $three) { 
  …do something… 
}
#traditional call 
explode-parameters(@list[0], @list[1], @list[2]); 
# …or using | 
explode-parameters(|@list);

Вернемся к делу: .grep отфильтровывает месяцы, 1-е число которых приходится на пятницу, и это наши 5 выходных месяцев. Таким образом, вывод однострочного кода выше выглядит примерно так:

...
1997-08-01
1998-05-01
1999-01-01
...

Это решение не хуже любого другого, и если бы решение было всем, что нам нужно, мы могли бы остановиться на этом. Но на примере этой задачи я хочу изучить способы использования класса Date. Пример. Приведенная выше однострочная строка выполняет свою работу, но, строго говоря, выводит не месяцы, а первый день этих месяцев. Исправить это легко, потому что класс Date поддерживает так называемые средства форматирования и использует синтаксис sprintf. Для этого вы используете именованный параметр «formatter» при создании экземпляра объекта.

say join "\n", grep *.day-of-week == 5, map { Date.new: |$_, 1, formatter => { sprintf "%04d/%02d", .year, .month } }, do 1900..2019 X 1,3,5,7,8,10,12;

Каждый раз, когда подпрограмма извлекает строковую версию даты, вызывается объект форматирования. В нашем случае вывод был изменен на…

...
1997/08
1998/05
1999/01
...

Форматеры мощные. Посмотри в них.

Теперь к слишком сложному решению. Это решение бездумного программиста, так как мы ничего не предполагаем. Программе не сообщается, что 5 выходных месяцев могут приходиться только на 31 дневной месяц. Он не знает, что 1-й из таких месяцев должен быть пятницей. Все, что он знает, это то, что если последний день месяца не воскресенье, он вычисляет дату последнего воскресенья (это не очень важно при подсчете трехдневных выходных, но может быть, если вы хотите найти суббота+воскресенье выходные или только воскресенье).

#!/usr/bin/env perl6
my $format-it = sub ($self) {
  sprintf "%04d month %02d", .year, .month given $self;
}
sub MAIN(Int :$from-year = 1900, Int :$to-year where * > $from-year = 2019, Int :$weekend-length where * ~~ 1..3 = 3) {
  my $date-loop = Date.new($from-year, 1, 1, formatter => $format-it);
  while ($date-loop.year <= $to-year) {
    my $date = $date-loop.later(day => $date-loop.days-in-month);
    $date = $date.truncated-to('week').pred if $date.day-of-week != 7;
    my @weekend = do for 0..^$weekend-length -> $w { 
      $date.earlier(day => $w).weekday-of-month; 
    };
    say $date-loop if ([+] @weekend) / @weekend == 5;
    $date-loop = $date-loop.later(:1month);
  }
}

Этот код может решить задачу как для трехдневных выходных, так и для выходных, состоящих из субботы + воскресенья, а также только воскресенья. Вы управляете этим с помощью параметра командной строки week-length=[1..3].

Этот код находит последнее воскресенье каждого месяца и подсчитывает, произошло ли оно пять раз в этом месяце. То же самое делается для субботы (если длина выходного дня = 2) и пятницы (если длина выходного дня = 3). Нравится:

my @weekend = do for 0..^$weekend-length -> $w { 
  $date.earlier(day => $w).weekday-of-month; 
};

Затем код вычисляет средний будний день месяца для этих трех дней следующим образом:

say $date-loop if ([+] @weekend) / @weekend == 5;

В этой строке используется оператор редукции [+] в списке @weekend, чтобы найти сумму всех элементов. Эта сумма делится на количество элементов. Если результат равен 5, то у вас есть пятидневные выходные.

Что касается забавных вещей, связанных с объектом Date:

.later(day|month|year => Int) — добавляет заданное количество единиц времени к текущей дате. Существует также более ранний метод вычитания.

.days-in-months — сообщает, сколько дней в текущем месяце объекта Date. Значение может быть 31, 30, 29 (февраль, високосный год) или 28 (февраль).

.truncated-to(week|month|day|year) — возвращает дату к первому дню недели, месяца, дня или года.

.weekday-of-month — выясняет, какой день недели является текущей датой, и вычисляет, сколько таких дней было до сих пор в этом месяце.

Кроме того, вы увидите, что на этот раз я добавил средство форматирования по-другому. Это, вероятно, выглядит чище и проще в обслуживании.

В конце концов, этот пост, возможно, вовсе не о датах и ​​манипулировании датами, а, скорее, является призывом ко всем нам еще больше использовать документацию. Я часто думаю, что в Perl 6 должна быть функция для x, y или z — .weekday-of-month — один из таких примеров — и документация говорит мне, что это действительно так!

Очень легко взять Perl 6 и запрограммировать его так же, как вы программировали бы Perl 5 или другие языки, которые вы хорошо знаете. Но в документации есть много информации о вещах, которых у вас раньше не было, и это сделает программирование проще и интереснее, когда вы узнаете о них.

Я думаю, вам не нужно копаться в документации, но если вы примете участие в Perl Weekly Challenge, это отличный повод провести время за документами!