【C#】「第N〇曜日の日付」を取得する方法

以前に書いた[【C#】「〇曜日の日付」を取得する方法](https://qiita.com/nkojima/items/22b41efca35f0b2370e5)という記事では、「YYYY年の〇曜日の日付を全て取得する」という処理のサンプルコードを載せていますが、実際には「〇曜日の日付が欲しい」というシーンだけではないと思います。

今回は、例えば「毎月第2月曜日は休業」といったシーンに対応できるように、「第N番目の〇曜日の日付」を取得する方法を紹介したいと思います。

# コード

```c#:DateUtils.cs
public class DateUtils
{
    /// <summary>
    /// 対象年の指定された曜日に該当する日付を全て取得する。
    /// </summary>
    /// <param name="targetYear">対象年</param>
    /// <param name="wantDayOfWeek">指定曜日。この曜日に合致する日付を取得する。</param>
    /// <returns>指定曜日に合致する日付のリスト。</returns>
    public static List<DateTime> FindWantDayOfWeek(int targetYear, DayOfWeek wantDayOfWeek)
    {

        List<DateTime> days = new List<DateTime>();

        // 対象年の1月1日(元旦)の曜日を取得する。
        DateTime gantan = new DateTime(targetYear, 1, 1);

        // 対象年の1月において、引数で指定した曜日に該当する最初の日付を取得する
        DateTime date;

        if (gantan.DayOfWeek == wantDayOfWeek)
        {
            date = gantan;
        }
        else
        {
            int additionalDay = ((int)(DayOfWeek.Saturday - gantan.DayOfWeek + wantDayOfWeek) % 7) + 1;
            date = gantan.AddDays(additionalDay);
        }

        // 7日ずつ日付をずらして、対象年における指定した曜日の日付を全て取得する。
        while (date.Year == targetYear)
        {
            days.Add(date);
            date = date.AddDays(7);
        }

        return days;
    }

    /// <summary>
    /// 対象年の「第N〇曜日」に該当する日付を全て取得する。
    /// </summary>
    /// <param name="targetYear">対象年</param>
    /// <param name="wantDayOfWeek">指定曜日。この曜日に合致する日付を取得する。</param>
    /// <param name="nth">「各月のN番目の日」を表す数字。</param>
    /// <returns>「第N〇曜日」のリスト。「第N〇曜日」が存在しない時は空のリストが返される。</returns>
    public static List<DateTime> FindWantDayOfWeek(int targetYear, DayOfWeek wantDayOfWeek, int nth)
    {
        // 対象年の指定された曜日に該当する日付を全て取得する。
        var days = FindWantDayOfWeek(targetYear, wantDayOfWeek);

        // 日付を月単位でGroupByした後、Whereで「第N〇曜日が存在する月」に絞り込む。
        // さらに、ElementAtで各月のN番目の〇曜日の日付を抽出する。
        var newDays = days.Select(day => new { Day = day, Month = day.Month }).GroupBy(day => day.Month)
            .Where(group => group.Count()>=nth).Select(group => group.ElementAt(nth - 1).Day).ToList();

        return newDays;
    }

    /// <summary>
    /// 期間内の「第N〇曜日」に該当する日付を全て取得する。
    /// </summary>
    /// <param name="targetYear">対象年</param>
    /// <param name="wantDayOfWeek">指定曜日。この曜日に合致する日付を取得する。</param>
    /// <param name="nth">「各月のN番目の日」を表す数字。</param>
    /// <param name="start">期間開始日</param>
    /// <param name="end">期間終了日</param>
    /// <returns>期間内の「第N〇曜日」に該当する日付のリスト。「第N〇曜日」が存在しない時は空のリストが返される。</returns>
    public static List<DateTime> FindWantDayOfWeek(int targetYear, DayOfWeek wantDayOfWeek, int nth, DateTime start, DateTime end)
    {
        var days = new List<DateTime>();

        // 開始年～終了年の指定された曜日に該当する日付を全て取得する。
        int elapsedYear = end.Year - start.Year + 1;
        var years = Enumerable.Range(start.Year, elapsedYear);

        foreach (int year in years)
        {
            days.AddRange(FindWantDayOfWeek(year, wantDayOfWeek, nth));
        }

        // 得られた日付リストを、開始日～終了日の間に絞り込む。
        var newDays = days.Where(day => day >= start && day <= end).ToList();

        return newDays;
    }
}
```

# コードの実行

## テストコード

* 以下のテストコードでは、「2018/04/01から2019/03/31の間の第2火曜日」を取得・表示しています。

```c#:UnitTestDateUtils.cs
[TestClass]
public class UnitTestDateUtils
{
    [TestMethod]
    public void TestFindWantDayOfWeek()
    {
        // 2018年4月1日～2019年3月31日の第2火曜日の日付を取得する。
        var days = DateUtils.FindWantDayOfWeek(2019, DayOfWeek.Tuesday, 2, new DateTime(2018, 4, 1), new DateTime(2019, 3, 31));
        var secondTuesdays = string.Join(",", days.Select(day => day.ToString("yyyy/MM/dd")));
        System.Console.WriteLine(secondTuesdays);
    }
}
```

## テストコードの実行結果

```
2018/04/10,2018/05/08,2018/06/12,2018/07/10,2018/08/14,2018/09/11,2018/10/09,2018/11/13,2018/12/11,2019/01/08,2019/02/12,2019/03/12
```