Учитывая название дня недели, неделю месяца и год, как найти дату?

Вопрос или проблема

Я могу найти последний понедельник, используя date -d 'last-monday' +%F, но как найти дату, если дано имя дня недели, неделя месяца и год?

например: Monday, Week 4, June, 2022
вывод: 2022-06-20

например: Friday, Week 1, July, 2022
вывод: 2022-07-01

Следующий скрипт на языке perl использует модули Date::Parse и
Date::Format из коллекции TimeDate. А также некоторые базовые арифметические операции.

К сожалению, эти модули не являются стандартными для perl, поэтому они не включены в поставку perl. Вам нужно будет установить их самостоятельно. Они упакованы для большинства дистрибутивов – например, в debian выполните sudo apt-get install libtimedate-perl. Или установите их с помощью утилиты cpan для perl (которая включена в поставку perl).

$ cat decode-week.pl 
#!/usr/bin/perl -l

use strict;
use Date::Parse;
use Date::Format;

sub reformat_date {
  # разбор даты из первого аргумента и ее оформление.
  my ($day, $week, $month, $year) = split /\s*,\s*/, shift;
  $week =~ s/^Week //;  # Слово "Week" не нужно.
  $day =~ s/^(...).*/$1/; # Нужны только первые три буквы.

  # некоторые полезные константы
  use constant secs_per_day => 86400;        # достаточно точно
  use constant one_week => secs_per_day * 7;

  # Допускается английский язык. Измените при необходимости для других языков.
  my %day_names = (Sun => 0, Mon => 1, Tue => 2, Wed => 3,
                   Thu => 4, Fri => 5, Sat => 6);
  # Альтернативно, если хотите, чтобы понедельник был нулевым днем недели.
  #my %day_names = (Mon => 0, Tue => 1, Wed => 2, Thu => 3,
  #                 Fri => 4, Sat => 5, Sun => 6);

  # получение time_t для первого дня $month $year
  my $t = str2time("1 $month $year");
  # получение сокращенного названия дня для этого 1-го дня
  my $dn = time2str("%a",$t);

  # вычисление разницы в днях
  my $diff = $day_names{$day} - $day_names{$dn};

  if ($week == 1 && $diff < 0) {
      return "Invalid week";
  };

  my $new_t = $t + ($diff * secs_per_day) + (($week-1) * one_week);

  # Date::Format не поддерживает %F, используйте идентичный %Y-%m-%d вместо него.
  return time2str("%Y-%m-%d",$new_t);
};

foreach (@ARGV) {
  print "$_ ===> ", reformat_date($_);
};

Это было написано в виде функции для удобства повторного использования, если вы хотите извлечь дату из файла журнала или чего-то подобного. Пока что она просто принимает аргументы из командной строки.

Пример вывода:

$ ./decode-week.pl "Monday, Week 1, June, 2022" "Monday, Week 4, June, 2022" "Friday, Week 1, July, 2022"
Monday, Week 1, June, 2022 ===> Invalid week
Monday, Week 4, June, 2022 ===> 2022-06-20
Friday, Week 1, July, 2022 ===> 2022-07-01

Подпрограмму можно улучшить, добавив проверку данных, чтобы убедиться, что передана корректная дата или, по крайней мере, дата в правильном формате. Оставлено в качестве упражнения для читателя, это довольно простой пример.

Также, основной цикл, вероятно, должен проверять, является ли возвращаемое значение функции “Invalid week” или нет, а не просто выводить все, что он получает.

Вы можете сказать ncal, чтобы он предоставил вам календарь для заданного месяца, а затем извлечь число в соответствующей строке и столбце (и преобразовать названия месяцев и дней в номера) с помощью gawk:

#! /bin/sh -
wday=$1 week=$2 month=$3 year=$4
{
  echo "$wday $week $month $year"
  ncal -Shb "$month" "$year"
} | gawk '
  NR == 1 {
    wday = $1; week = $2; month = $3; year = $4
    for (i = 1; i <= 7; i++)
      nday[strftime("%A", (i+2)*86400, 1)] = i
    for (i = 1; i <= 12; i++)
      nmonth[strftime("%B", (i-0.5)*86400*30, 1)] = i
    FIELDWIDTHS = "3 3 3 3 3 3 3"
    next
  }
  NR == 3 + week {
    day = $ nday[wday]
    if (+day) {
      printf "%04d-%02d-%02d\n", year, nmonth[month], day
      ok = 1
    }
    exit
  }
  END {
    if (!ok) {
      print "There is no "wday" in week "week" of "month" "year > "/dev/stderr"
      exit 1
    }
  }'

Выше названия месяцев интерпретируются в соответствии с локалью пользователя.

Используя GNU awk для временных функций и gensub():

$ cat tst.awk
BEGIN { FS="[, ]+" }
{
    day   = $1
    week  = $3
    month = $4
    year  = $5

    mkMap(year,month)

    key = year SUBSEP month SUBSEP week SUBSEP day
    print $0, "=>", (key in map ? map[key] : "invalid date")
}

function mkMap(year,month,      mthAbbr,mthNr,wkNr,dayNr,date,secs,d) {
    if ( !seen[year,month]++ ) {
        mthAbbr = substr(month,1,3)
        mthNr = (index("JanFebMarAprMayJunJulAugSepOctNovDec",mthAbbr)+2)/3
        wkNr = 1
        for ( dayNr=1; dayNr<=31; dayNr++ ) {
            date = sprintf("%04d-%02d-%02d", year, mthNr, dayNr)
            secs = mktime(gensub(/-/," ","g",date) " 12 0 0")
            split(strftime("%F,%A",secs),d,",")
            if ( d[1] == date ) {
                # date -> secs -> same date means this is a valid date
                map[year,month,wkNr,d[2]] = date
                wkNr += ( d[2] == "Saturday" ? 1 : 0 )
            }
        }
    }
}

$ awk -f tst.awk file
Monday, Week 4, June, 2022 (should output: 2022-06-20) => 2022-06-20
Friday, Week 1, July, 2022 (should output: 2022-07-01) => 2022-07-01
Monday, Week 1, June, 2022 (should output: invalid date) => invalid date

Выше указано было запущено против следующего входного файла:

$ cat file
Monday, Week 4, June, 2022 (should output: 2022-06-20)
Friday, Week 1, July, 2022 (should output: 2022-07-01)
Monday, Week 1, June, 2022 (should output: invalid date)

Используя Raku (ранее известный как Perl_6)

raku -e '

    #объявление хэшей:
    my %months = (Jan => 1, Feb => 2, Mar => 3, Apr => 4, May => 5, Jun => 6, Jul => 7, Aug => 8, Sep => 9, Oct => 10, Nov =>  11, Dec => 12);
    my %antimonths = %months.antipairs;
    my %days = (Monday => 1, Tuesday => 2, Wednesday => 3, Thursday => 4, Friday => 5, Saturday => 6, Sunday => 7);
    my %antidays = %days.antipairs;
    my %ordinals = (1 => "st", 2 => "nd", 3 => "rd", 4 => "th", 5 => "th");

    #цикл по строкам:
    my @a; for lines.map: *.split(", ") {
        @a.push( .[3], sprintf( "%02u", %months{.[2].substr(0,3)} ), .[1].substr(*-1,1), %days{.[0]} )
    };

    #генерация дат:
    my @b = do for @a.rotor(4) { (.[0], .[1], "01").join("-").Date};
    my @week-desired = @a[2,6,10...*]; my @DOW-desired = @a[3,7,11...*];
    my @offset = do for @b>>.day-of-week Z @DOW-desired -> ($first-of-month_DOW, $DOW-desired) { 
        ($DOW-desired - $first-of-month_DOW) mod 7
    };

    #возврат ответа:
    for ([Z] @b, @week-desired, @DOW-desired, @offset) -> ($a,$b,$c,$d) {
        say "Для %antimonths{$a.month}_{$a.year}, $b%ordinals{$b} %antidays{$c} это:  " ~ $a + $d + 7*($b - 1)
    };'  file 

Пример входных данных:

Saturday, Occurrence 1, January, 2022, #output: 2022-01-01
Monday, Occurrence 1, January, 2022, #output: 2022-01-03
Monday, Occurrence 2, January, 2022, #output: 2022-01-10
Monday, Occurrence 4, June, 2022, #output: 2022-06-27
Friday, Occurrence 1, July, 2022, #output: 2022-07-01
Monday, Occurrence 1, August, 2022, #output: 2022-08-01

Пример вывода:

Для Jan_2022, 1-й Saturday это:  2022-01-01
Для Jan_2022, 1-й Monday это:  2022-01-03
Для Jan_2022, 2-й Monday это:  2022-01-10
Для Jun_2022, 4-й Monday это:  2022-06-27
Для Jul_2022, 1-й Friday это:  2022-07-01
Для Aug_2022, 1-й Monday это:  2022-08-01

Автор интерпретировал интересный вопрос и попытался ответить на него 1) используя Raku и 2) без использования любых внешних модулей.

Считая комментарии, кажется есть некоторая неопределенность относительно того, как должна рассчитываться “неделя-месяца”. Ясно, что насчитывая “неделю”, начинающуюся в воскресенье, получаются другие результаты, по сравнению с началом недели в понедельник. Не углубляясь в подробности подсчета “недели”, этот ответ упрощает расчет “неделя-месяца” до “nth-день недели в месяце”. Соответственно, пример входных данных был изменен для отражения Occurrence конкретного дня недели в заданном месяце. Надеюсь, этот код окажется полезным для автора вопроса (и других).

В первых 2-х заявлениях (первой строке) объявляются хэши %months и %antimonths. Во вторых 2-х заявлениях (вторая строка) объявляются хэши %days и %antidays. В третьем заявлении (третья строка) объявляется хэш %ordinals. В четвертом заявлении (четвертая строка) lines читаются из файла, сплитятся на ", " (запятая пробел), и push добавляется в массив @a, в общем плане от большего к меньшему временному измерению (например, года), за исключением переменной измерения.

В пятом заявлении (пятая строка) извлекаются месяц и год из @a и объединяются для заполнения массива @b, состоящего из действительных дат “первого-дня-месяца” ISO-8601.

В шестом/седьмом заявлениях (шестая строка) из элементов @a создаются массивы @week-desired и @DOW-desired. В седьмой строке с использованием mod рассчитывается @offset, который составляет количество дней от первого-в-месяце до желаемого дня недели в первую неделю месяца. Наконец, смещение (в днях) и нужная неделя (нужная неделя умноженная на 7, чтобы получить дни) используется в конечном расчете, чтобы получить результат даты ISO-8601, который печатается.

[Обратите внимание, что явной проверки ошибок нет: любая проверка ошибок будет выполнена классом Date Raku, который может действительно попытаться преобразовать данные не желательным для пользователя способом. Наконец, представленный здесь ‘в одну строку’ код Raku может быть переписан в виде скрипта, (надеюсь) похожего на прекрасно отформатированный ответ на Perl, представленный @cas].

https://docs.raku.org/type/Date
https://raku.org

.

Ответ или решение

Чтобы вычислить конкретную дату, зная название дня недели, номер недели в месяце и год, требуется применить несколько методов программирования и математических вычислений. Рассмотрим теоретическую часть, практические примеры и возможное применение.

Теория

В теоретическом аспекте, для решения задачи нам нужно разобраться с понятием «неделя месяца» и правильным расчетом даты для заданного дня недели. Одним из основных факторов является определение, какая именно неделя месяца учитывается: полная неделя или неполная, начиная с первого числа месяца или начиная с первого понедельника и так далее. Например, если месяц начинается с пятницы, то стоит определить, какая неделя будет считаться первой для упрощения нашего вычисления.

При вычислении даты определенного дня недели в месяце, следует помнить о различиях в продолжительности месяцев, а также о том, что начало недели может различаться в зависимости от местных стандартов.

Пример

Предположим, нам дано следующее: «Понедельник, 4 неделя, июнь, 2022 год». Для этой задачи нам нужно определить, какой будет дата для предложенного условия. Мы используем несколько языков программирования для демонстрации возможных решений.

  1. Использование Perl

Пользователи Perl могут применять модули Date::Parse и Date::Format для достижения результата. Пример программы, которая помогает в вычислениях, представлен выше. Она включает такие важные моменты, как преобразование даты, вычитание номеров дней недели, добавление недель и другие операции, которые по итогу возвращают нужную дату в формате ISO 8601.

  1. Использование GNU Awk

GNU Awk может быть также использован для обработки строк команд и вычислений даты, прибегая к функциям времени. Скрипт awk выполняет разбор строки, рассчитывает смещения и выдает результат после применения преобразований даты.

  1. Использование Raku

Raku (ранее Perl 6) также предоставляет мощные инструменты для работы с датами без использования внешних модулей. В Raku используется несколько хешей для хранения месяцев и дней, а затем вычисляется необходимая дата с учетом недели и дня.

Применение

Это задание может найти применение в различных сферах, включая планирование событий на основе стандартных графиков, подготовка отчетов, автоматизация процессов работы, где требуется интеграция конкретных дат в процессе. Работая с системами, где необходима точность в расчете дат, подобный сценарий может быть особенно полезен.

Заключение

Понимание и умение найти дату на основании предоставленной информации является важным навыком для IT-специалистов, работающих с автоматизацией и системами планирования. Выбор языка программирования, набора инструментов и методов зависит от конкретных требований задач и технического окружения. Независимо от выбранного инструмента, важно подкрепить решение строгой валидацией и тестированием, чтобы обеспечить корректность вычислений и избежать неверных интерпретаций.

Оцените материал
Добавить комментарий

Капча загружается...