毎月のプレミアムフライデーを算出するには?[C#/VB].NET TIPS

プレミアムフライデーを求めるには、月末の日付からさかのぼりながら金曜日を探す方法と、月末の日付が何曜日か調べて、それと金曜日との日数差から求める方法がある。

» 2017年05月31日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 2017年から始まった「プレミアムフライデー」は毎月最後の金曜日であるが、その日付を求めるにはどうしたらよいだろうか? 本稿ではその方法を2通り紹介するとともに、高速化のために難解になったロジックを検証する方法を解説する。

プレミアムフライデーを算出するには?

 まず月末の日付を求め、そこから金曜日との日数差を引けばよい(次のコード)。金曜日との日数差は、剰余演算で求められる。

static DateTimeOffset GetPremiumFriday(int year, int month)
{
  // 指定された年月の1日
  DateTimeOffset current1stDate
    = new DateTimeOffset(year, month, 1, 0, 0, 0, TimeSpan.Zero);
  // 指定された年月の末日
  DateTimeOffset currentLastDate = current1stDate.AddMonths(1).AddDays(-1.0);
  // その曜日(0=日曜〜6=土曜)
  int currentLastDayOfWeek = (int)currentLastDate.DayOfWeek;

  // 指定された年月の末日と金曜日との日数差(剰余演算)
  int diff = (currentLastDayOfWeek + 7 - (int)DayOfWeek.Friday) % 7;

  // 月の末日から日数差を引くとプレミアムフライデー
  return currentLastDate.AddDays(-diff);
}

Function GetPremiumFriday(year As Integer, month As Integer) As DateTimeOffset
  ' 指定された年月の1日
  Dim current1stDate As DateTimeOffset _
    = New DateTimeOffset(year, month, 1, 0, 0, 0, TimeSpan.Zero)
  ' 指定された年月の末日
  Dim currentLastDate As DateTimeOffset _
    = current1stDate.AddMonths(1).AddDays(-1.0)
  ' その曜日(0=日曜〜6=土曜)
  Dim currentLastDayOfWeek As Integer = currentLastDate.DayOfWeek

  ' 月の末日と金曜日との日数差(剰余演算)
  Dim diff As Integer = (currentLastDayOfWeek + 7 - DayOfWeek.Friday) Mod 7

  ' 月の末日から日数差を引くとプレミアムフライデー
  Return currentLastDate.AddDays(-diff)
End Function

プレミアムフライデーを算出するメソッドの例(上:C#、下:VB)
引数に年と月を与えると、プレミアムフライデーの日付を返す。
指定された年月の末日は、その月の1日を作り、そこから翌月1日を求め(「AddMonths(1)」)、さらにそこから1日を引くと求まる(「AddDays(-1.0)」)。

 上のコードで剰余演算している部分は、翌週の同じ曜日(=「+7」した日)から今週の金曜日を引き、結果が7以上だったら7を引いている。これで月の末日とその直前の金曜日との日数差が求められるのである。どうにも難解だが、これで正しいことは後ほど確かめることにしよう。

単純に算出するコード

 処理速度にこだわらず、単純なコードを書いてみよう。その月の最後の金曜日を見つけるには、末日から順にさかのぼりながらその日が金曜日かどうかを判定すればよい(次のコード)。

static DateTimeOffset GetPremiumFridayBasic(int year, int month)
{
  // 指定された年月の1日
  DateTimeOffset current1stDate
    = new DateTimeOffset(year, month, 1, 0, 0, 0, TimeSpan.Zero);
  // 指定された年月の末日
  DateTimeOffset currentLastDate = current1stDate.AddMonths(1).AddDays(-1.0);

  // 1日ずつ戻りながら、金曜日を探す
  for (int n = 0; n > -7; n--)
  {
    DateTimeOffset d = currentLastDate.AddDays(n);
    if (d.DayOfWeek == DayOfWeek.Friday)
      return d;
  }
  throw new Exception();
}

Private Function GetPremiumFridayBasic(year As Integer, month As Integer) _
    As DateTimeOffset
  ' 指定された年月の1日
  Dim current1stDate As DateTimeOffset = New DateTimeOffset(year, month, 1, 0, 0, 0, TimeSpan.Zero)
  ' 指定された年月の末日
  Dim currentLastDate As DateTimeOffset = current1stDate.AddMonths(1).AddDays(-1.0)

  ' 1日ずつ戻りながら、金曜日を探す
  For n As Integer = 0 To -6 Step -1
    Dim d As DateTimeOffset = currentLastDate.AddDays(n)
    If (d.DayOfWeek = DayOfWeek.Friday) Then
      Return d
    End If
  Next
  Throw New Exception()
End Function

プレミアムフライデーを算出する単純な実装の例(上:C#、下:VB)
指定された年月の末日を求めるところまでは、先のコードと同じである。
その末日から始めて1日ずつさかのぼりながら金曜日を探索する。金曜日は1週間以内に必ずあるので、7日間だけ調べれば十分である。
なお、例外を投げている最終行は実際には実行されることはないのだが、C#では書いておかないとコンパイルできない。VBでは書かなくてもよい(その場合はDateTimeOffsetのNothing、つまり西暦1年1月1日を返すコードがあると見なされる)。

2つのコードを検証する

 単純な実装の方のロジックは、コードを読めば正しいと確信できるだろう。剰余演算を使ったコードも同じ結果になれば、そちらも正しいと確信できる。

 例えば次のコンソールアプリのようにして1年分を並べて出力してみよう(次のコード)。コンソール出力を目視で確認すれば、同じ結果が得られていると分かる。

using System;
using static System.Console;

class Program
{

  ……省略(GetPremiumFridayBasicメソッドとGetPremiumFridayメソッド)……

  static void Main(string[] args)
  {
    // 2017年1〜12月のプレミアムフライデー
    for (int m = 1; m <= 12; m++)
    {
      DateTimeOffset pf1 = GetPremiumFridayBasic(2017, m); // 単純な実装
      DateTimeOffset pf2 = GetPremiumFriday(2017, m); // 剰余演算を使った実装
      WriteLine($"2017年{m:00}月:{pf1:dd}日/{pf2:dd}日");
    }
    // 出力:
    // 2017年01月:27日/27日
    // 2017年02月:24日/24日
    // 2017年03月:31日/31日
    // 2017年04月:28日/28日
    // 2017年05月:26日/26日
    // 2017年06月:30日/30日
    // 2017年07月:28日/28日
    // 2017年08月:25日/25日
    // 2017年09月:29日/29日
    // 2017年10月:27日/27日
    // 2017年11月:24日/24日
    // 2017年12月:29日/29日
#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

Module Module1

  ……省略(GetPremiumFridayBasicメソッドとGetPremiumFridayメソッド)……

  Sub Main()
    ' 2017年1〜12月のプレミアムフライデー
    For m As Integer = 1 To 12
      Dim pf1 As DateTimeOffset = GetPremiumFridayBasic(2017, m) '単純な実装
      Dim pf2 As DateTimeOffset = GetPremiumFriday(2017, m) ' 剰余演算を使った実装
      WriteLine($"2017年{m:00}月:{pf1:dd}日/{pf2:dd}日")
    Next
    ' 出力:
    ' 2017年01月:27日/27日
    ' 2017年02月:24日/24日
    ' 2017年03月:31日/31日
    ' 2017年04月:28日/28日
    ' 2017年05月:26日/26日
    ' 2017年06月:30日/30日
    ' 2017年07月:28日/28日
    ' 2017年08月:25日/25日
    ' 2017年09月:29日/29日
    ' 2017年10月:27日/27日
    ' 2017年11月:24日/24日
    ' 2017年12月:29日/29日
#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

2つのロジックの結果を並べて表示するコンソールアプリの例(上:C#、下:VB)
左側の日付は単純な実装、右側は剰余演算を使った実装である。

 実際には、目視で確認するだけでなく、自動的に検証するコードも書くとよい(次のコード、C#のみ)。自動検証なら、何千年分でも好きなだけ検証できるのみならず、剰余演算を使ったコードを開発している最中にも使える。剰余演算のロジックを取りあえず書いてみて検証すると、たぶんパスしないだろう。ロジックを間違えたのだ。そこで、ロジックを修正しては検証してみることを繰り返す。検証にパスするようになったら、その剰余演算のロジックは正しく書けたのだと分かる(実際に、先のコードはそのようにして作った)。

// 今世紀100年分のプレミアムフライデーをチェック
for (int y = 2001; y <= 2100; y++)
  for (int m = 1; m <= 12; m++)
  {
    DateTimeOffset pf1 = GetPremiumFridayBasic(y, m); // 単純な実装
    DateTimeOffset pf2 = GetPremiumFriday(y, m); // 剰余演算を使った実装
    if (pf1 != pf2)
      throw new Exception($"{pf1:yyyy/MM/dd} != {pf2:yyyy/MM/dd}");
  }
WriteLine("今世紀100年分のチェック完了");

2つのロジックの結果を自動検証する例(C#)
2つのロジックの結果が異なるときは例外を出す。全て一致していれば、「今世紀100年分のチェック完了」と出力される。
ちなみに、今世紀最後(=2100年12月)のプレミアムフライデーは大みそかである。

 なお、本稿では自動検証コードをコンソールアプリとして書いたが、Visual Studioにはもっと多彩な自動検証コードが書ける上に簡単に検証を実行できるユニットテストフレームワーク「MSTest(Microsoft単体テストフレームワーク)」が用意されている。また、Visual Studio 2017 Enterprise Editionには、自動検証コードをバックグラウンドで自動実行する「ライブユニットテスト」機能もある(コードがコンパイルできる状態になるたびに自動的にユニットテストが実行される)。

まとめ

 プレミアムフライデーを求めるコードは、剰余演算を使うと高速化できる。そのような難解なロジックを作るときは、単純な実装も別に作り、両者の結果を比較することでロジックの正しさを担保するとよい。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。