Узнать первый и последний понедельник месяца

Как на С# узнать первый и последний понедельник текущего месяца, а также первую и последнюю субботу текущего месяца. Использую язык Linq. Нужен пример, т.к. сам не могу догадаться в каком направлении двигаться.


Ответы (3 шт):

Автор решения: A K

Давайте напишем метод, который найдёт все понедельники для интервала:

private static IEnumerable<DateTime> SpecificDaysOfWeek(DayOfWeek dayOfWeek, DateTime start, DateTime end)
{
    var current = start;
    
    while (current <= end)
    {
        if (current.DayOfWeek == dayOfWeek)
            break;

        current = current.AddDays(1);
    }

    while (current <= end)
    {
        yield return current;
        current = current.AddDays(7);
    }
}

Логика следующая. В цикле while находим первый понедельник, потом начинаем прибавлять к найденному значению по 7 дней.

Ну и теперь осталось применить к понедельникам, субботам или нужным дням недели:

var currentDate = DateTime.Today;

var firstDayOfMonth = new DateTime(currentDate.Year, currentDate.Month, 1);
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);

var days = SpecificDaysOfWeek(DayOfWeek.Monday, firstDayOfMonth, lastDayOfMonth);
days.Dump();
days.First().Dump();
days.Last().Dump();

Вот вам и результат для ноября 2020:

введите сюда описание изображения

Также если очень надо именно вот только linq style, то вот то альтернативный вариант этой же функции:

private static IEnumerable<DateTime> SpecificDaysOfWeek(DayOfWeek dayOfWeek, DateTime start, DateTime end)
{
    return Enumerable.Range(0, end.Subtract(start).Days)
                     .Select(x => start.AddDays(x))
                     .Where(x => x.DayOfWeek == dayOfWeek);
}
→ Ссылка
Автор решения: Андрей NOP

Пусть у нас есть какая-то дата и день недели, который мы хотим найти:

var date = new DateTime(2020, 11, 1);
var dayOfWeek = DayOfWeek.Monday;

Вариант с циклом, здесь нам достаточно перебрать не более 7 ближайших дней, включая текущий:

var nextMonday = Enumerable.Range(0, 7)
    .Select(x => date.AddDays(x))
    .First(d => d.DayOfWeek == dayOfWeek);
var prevMonday = Enumerable.Range(0, 7)
    .Select(x => date.AddDays(-x))
    .First(d => d.DayOfWeek == dayOfWeek);
Console.WriteLine(nextMonday);
Console.WriteLine(prevMonday);

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

var nextMonday = date.AddDays((dayOfWeek - date.DayOfWeek + 7) % 7);
var prevMonday = date.AddDays((dayOfWeek - date.DayOfWeek - 7) % 7);
Console.WriteLine(nextMonday);
Console.WriteLine(prevMonday);

Здесь используется мелкая стандартная магия с остатками, прибавляя 7 перед вычислением остатка мы гарантируем, что получим положительное значение или ноль после (0..6). И, наоборот, отнимая 7, гарантируем, что получим отрицательное значение или ноль (-6..0)

В обоих случаях вывод:

02.11.2020 0:00:00
26.10.2020 0:00:00

И да, я здесь смотрю вперед и назад от одной и той же даты, если вам нужно смотреть назад от даты конца месяца, то ее получить легко:

var firstDay = new DateTime(2020, 11, 1);
var lastDay = firstDay.AddMonths(1).AddDays(-1);
→ Ссылка
Автор решения: DiD

Я не очень хорошо помню C#, но постараюсь донести код, который будет одинаково подходить (может быть адаптирован) для всех языков программирования, поддерживающих простые арифметические операции.

На языке JavaScript моё решение выглядело бы так:

const flwom=(y,m,w)=>[
  w=7-(y=>~~(23*m/9)+(m<3?y--:y-2)+4+~~(y/4)-~~(y/100)+~~(y/400)-w)(y)%7,
  w+7*~~((28+(62648012+16*!(y%4)>>m*2&3)-w)/7)
];

Код создаёт функцию, которая возвращает массив с датами первого и последнего заданного аргументом w дня недели для месяца m года y.

Я не фанат однобуквенных переменных, но за длинными названиями переменных теряется простота формул.


Попробую написать код на C#.

Пусть наши входящие данные будут такими:

int year = 2020;
int month = 12;       // Декабрь
int specWeekDay = 2;  // Вторник

Сперва определим, является ли год високосным.

bool leap = 0 == year % 4;

Далее нам нужно определить день недели на первое число месяца. /Формула была взята из википедии/.

static int day = 1;
int dayOfWeek = (day += month < 3 ? year-- : year - 2, 
     23 * (month/9) + day + 4 + year/4 - year/100 + year/400 ) % 7;

Обратите внимание! В этих формулах используется целочисленное деление, то есть деление с отбрасыванием (не округлением!) дробной части.

В переменной dayOfWeek в зависимости от переданных выше параметров будет число от 0 (воскресение) до 6 (Суббота). Не сложно посчитать, например, тот же первый вторник декабря (или любой другой день недели на заданные месяц и год).

int firstWeekday = 1 + specWeekDay - dayOfWeek;
if(dayOfWeek > specWeekDay) firstWeekday += 7;

Дальше для определения последнего дня в месяце, нам нужно получить количество дней в месяце.

static int[] daysInMonth = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int daysInSpecifiedMonth = daysInMonth[month];
if( month == 2 && leap ) daysInSpecifiedMonth ++;

И осталось лишь посчитать последний вторник декабря (или любой другой день недели на заданные месяц и год):

int lastWeekday = daysInSpecifiedMonth - (daysInSpecifiedMonth - firstWeekday) % 7;

Вот и всё, задача решена чистой математикой. Без использования DateTime. Без использования циклов.


PS В коде могут быть очепятки, код не проверялся, если увидели ошибку в коде - сразу правьте мой пост, не обязательно писать в комментариях. Не ругайте сильно, всё таки я больше 11 лет не притрагивался к C#.


Кстати! Есть множество способов как посчитать день недели на заданную дату. Если не нравится формула выше, можно воспользоваться более консервативным методом:

int dayOfWeek(int y, int m, int d)
{
    static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
    y -= m < 3;
    return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}    

А вот этот метод вообще убил:

dow(m,d,y) { y-=m<3; return(y+y/4-y/100+y/400+"-bed=pen+mad."[m]+d)%7; }

В русской википедии тоже есть хорошие статьи:

→ Ссылка