プレミアムフライデーを求めるには、月末の日付からさかのぼりながら金曜日を探す方法と、月末の日付が何曜日か調べて、それと金曜日との日数差から求める方法がある。
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
上のコードで剰余演算している部分は、翌週の同じ曜日(=「+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
単純な実装の方のロジックは、コードを読めば正しいと確信できるだろう。剰余演算を使ったコードも同じ結果になれば、そちらも正しいと確信できる。
例えば次のコンソールアプリのようにして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
実際には、目視で確認するだけでなく、自動的に検証するコードも書くとよい(次のコード、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年分のチェック完了");
なお、本稿では自動検証コードをコンソールアプリとして書いたが、Visual Studioにはもっと多彩な自動検証コードが書ける上に簡単に検証を実行できるユニットテストフレームワーク「MSTest(Microsoft単体テストフレームワーク)」が用意されている。また、Visual Studio 2017 Enterprise Editionには、自動検証コードをバックグラウンドで自動実行する「ライブユニットテスト」機能もある(コードがコンパイルできる状態になるたびに自動的にユニットテストが実行される)。
プレミアムフライデーを求めるコードは、剰余演算を使うと高速化できる。そのような難解なロジックを作るときは、単純な実装も別に作り、両者の結果を比較することでロジックの正しさを担保するとよい。
利用可能バージョン:.NET Framework 2.0以降(サンプルコードにはそれ以降の機能/構文も含む)
カテゴリ:クラス・ライブラリ 処理対象:日付と時刻
使用ライブラリ:DateTimeOffset 構造体(System名前空間)
関連TIPS:月初/月末の日付を求めるには?[C#、VB]
関連TIPS:日時や時間間隔の加減算を行うには?
関連TIPS:週の始まりの日付を求めるには?
関連TIPS:西暦年が閏(うるう)年かどうかを判別するには?[C#、VB]
関連TIPS:n日後、nカ月後、n年後の日付を求めるには?[C#、VB]
関連TIPS:UTC(世界協定時)を取得するには?[C#、VB]
関連TIPS:指定した月から特定の曜日の日付を取得するには?[C#、VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.