国民の祝日には、日付が決まっているものもあれば、その年によって変化するものもある。本稿では法律に従って、これらを算出する方法を示す。
対象:.NET 3.5以降
カレンダーを表示するプログラムを作ろうとしたとき、厄介なのは祝日だ。日本の祝日には、特定の日に固定されていないものがあるからだ。法律に書いてある通りにロジックを組めば祝日を決定できるとはいうものの、なかなか面倒なコーディングになる。本稿では、2007年以降のある年の祝日を求める方法を紹介しよう。なお、本稿のサンプルは「Windows desktop code samples:.NET Tips #1112」からダウンロードできる。
本稿で扱う祝日とは、国民の祝日/振替休日/国民の休日の三種類である。
一つ目の国民の祝日は、さらに3種類に分類できる。
これらのうち春分の日/秋分の日は、その前年の2月に官報で発表されるまでは確定しない*1。
祝日をプログラムに組み込む最も確実な方法は、官報での発表を見て、その翌年の祝日テーブルを作成することである。しかし実際には、翌年以降の祝日が必要になることも多いだろう。そこで、以下に説明するようなロジックで祝日を算出したり、それで得られた結果をテーブルに格納したりする。
*1 例えば2016年の祝日は、2015年2月2日付けの官報本紙6463号に、暦要項(れきようこう)として掲載された。同じ内容が国立天文台のWebページにもPDFで載っている。
上で説明した法令に従ってロジックを組めばよい。
とはいうものの、かなり面倒なコードになる。ここでは、まず祝日データを格納しておくクラスを定義し、メインメソッドを作り始めよう。それから、項目を分けて祝日データを作成するロジックを追加していく。
1件の祝日データは、次のコードに示す「Holiday」クラスに格納しよう。
public class Holiday
{
public DateTime Date { get; set; } // 日付
public HolidayKind Kind { get; set; } // 種類
public string Name { get; set; } // 名称
public string Definition { get; set; } // 祝日の定義
}
Public Class Holiday
Public Property [Date] As DateTime ' 日付
Public Property Kind As HolidayKind ' 種類
Public Property Name As String ' 名称
Public Property Definition As String ' 祝日の定義
End Class
上のコードで祝日の種類を表している「HolidayKind」列挙体は、次のコードのように定める。
public enum HolidayKind
{
平日 = 0,
国民の祝日 = 1,
振替休日 = 2,
国民の休日 = 3,
}
Public Enum HolidayKind
平日 = 0
国民の祝日 = 1
振替休日 = 2
国民の休日 = 3
End Enum
本稿ではコンソールプログラムを作っていく。Mainメソッドの先頭で、「Holiday」クラスのインスタンスを格納するコレクションを用意する。Mainメソッドの末尾では、そのコレクションに入っている「Holiday」オブジェクトをコンソールに出力しよう(次のコード)。
using System;
using System.Collections.Generic;
using System.Linq;
……省略……
static void Main(string[] args)
{
// 祝日を計算する年
const int Year = 2015;
// 祝日を格納するコレクション
SortedDictionary<DateTime, Holiday> holidays
= new SortedDictionary<DateTime, Holiday>();
// ここに祝日を求めるロジックを書く
// 祝日を出力する
Console.WriteLine("{0}年の休日", Year);
foreach (var d in holidays.Values)
Console.WriteLine("{0:MM/dd(ddd)} {1}({2}、{3})",
d.Date, d.Name, d.Definition, d.Kind);
Console.WriteLine();
#if DEBUG
// デバッグ実行時にコンソールがすぐに閉じてしまうのを防ぐ
Console.ReadKey();
#endif
}
Sub Main(args As String())
' 祝日を計算する年
Const Year As Integer = 2015
' 祝日を格納するコレクション
Dim holidays As SortedDictionary(Of DateTime, Holiday) _
= New SortedDictionary(Of DateTime, Holiday)()
' ここに祝日を求めるロジックを書く
' 祝日を出力する
Console.WriteLine("{0}年の休日", Year)
For Each d In holidays.Values
Console.WriteLine("{0:MM/dd(ddd)} {1}({2}、{3})",
d.Date, d.Name, d.Definition, d.Kind)
Next
Console.WriteLine()
#If DEBUG Then
' デバッグ実行時にコンソールがすぐに閉じてしまうのを防ぐ
Console.ReadKey()
#End If
End Sub
これは簡単である。法令で指定されている日付をコレクションに加えていくだけだ(次のコード)。ただし、きちんと算出するためには、次のコードの「山の日」にあるように、法令の施行年を判別する必要がある。
// 元日:1月1日
var 元日 = new Holiday()
{
Date = new DateTime(Year, 1, 1),
Kind = HolidayKind.国民の祝日,
Name = "元日", Definition = "1月1日",
};
holidays.Add(元日.Date, 元日);
……省略……
// 山の日:8月11日 (2016年より)
if (2016 <= Year)
{
var 山の日 = new Holiday()
{
Date = new DateTime(Year, 8, 11),
Kind = HolidayKind.国民の祝日,
Name = "山の日", Definition = "8月11日",
};
holidays.Add(山の日.Date, 山の日);
}
……省略……
' 元日:1月1日
Dim 元日 = New Holiday() With
{
.Date = New DateTime(Year, 1, 1),
.Kind = HolidayKind.国民の祝日,
.Name = "元日", .Definition = "1月1日"
}
holidays.Add(元日.Date, 元日)
……省略……
' 山の日:8月11日 (2016年より)
If (2016 <= Year) Then
Dim 山の日 = New Holiday() With
{
.Date = New DateTime(Year, 8, 11),
.Kind = HolidayKind.国民の祝日,
.Name = "山の日", .Definition = "8月11日"
}
holidays.Add(山の日.Date, 山の日)
End If
……省略……
現在のところそのような祝日は全て月曜日になっているので、「○月の第N月曜日」が算出できればよい。そのようなロジックを「GetNthMonday」メソッドとしてまとめると、次のコードのようになる。
private static DateTime GetNthMonday(int nth, int year, int month)
{
// 指定された月の日数
int days = DateTime.DaysInMonth(year, month);
// 指定された月の全ての日から、月曜日だけを取り出す
IEnumerable<DateTime> allMondays
= Enumerable.Range(1, days) // 1日〜月末の日付(int型のコレクション)を作り
.Select(d => new DateTime(year, month, d)) // DateTime型のコレクションに変換して
.Where(dt => dt.DayOfWeek == DayOfWeek.Monday); // そこから月曜日だけを取り出す
// N番目の月曜日を求める
return allMondays.ElementAt(nth - 1);
}
Private Function GetNthMonday(nth As Integer, year As Integer, month As Integer) As DateTime
' 指定された月の日数
Dim days As Integer = DateTime.DaysInMonth(year, month)
' 指定された月の全ての日から、月曜日だけを取り出す
Dim allMondays As IEnumerable(Of DateTime) _
= Enumerable.Range(1, days) _
.Select(Function(d) New DateTime(year, month, d)) _
.Where(Function(dt) dt.DayOfWeek = DayOfWeek.Monday)
' Enumerable.Rangeで1日〜月末の日付(int型のコレクション)を作り
' SelectでDateTime型のコレクションに変換して
' Whereでそこから月曜日だけを取り出す
' N番目の月曜日を求める
Return allMondays.ElementAt(nth - 1)
End Function
このメソッドを使って、法令で指定されている曜日指定の祝日をコレクションに加えるコードは、次のようになる。なお、コレクションとしてSortedDictionaryクラスを使っているので、Holidayインスタンスをコレクションに追加すると日付順に自動的にソートされる。
// 成人の日:1月の第2月曜日
var 成人の日 = new Holiday()
{
Date = GetNthMonday(2, Year, 1),
Kind = HolidayKind.国民の祝日,
Name = "成人の日", Definition = "1月の第2月曜日",
};
holidays.Add(成人の日.Date, 成人の日);
……省略……
' 成人の日:1月の第2月曜日
Dim 成人の日 = New Holiday() With
{
.Date = GetNthMonday(2, Year, 1),
.Kind = HolidayKind.国民の祝日,
.Name = "成人の日", .Definition = "1月の第2月曜日"
}
holidays.Add(成人の日.Date, 成人の日)
……省略……
春分日/秋分日は、実験式から求める*2。そのようなロジックを「CalcVernalEquinoxDay」メソッド(春分日を求める)/「CalcAutumnalEquinoxDay」メソッド(秋分日を求める)としてまとめると、次のコードのようになる。
// 春分日を求める(2099年まで有効な実験式)
private static DateTime CalcVernalEquinoxDay(int year)
{
// 1. 2000年の太陽の春分点通過日
double 基準日 = 20.69115;
// 2. 春分点通過日の移動量=(西暦年−2000年)×0.242194
double 移動量 = (year - 2000) * 0.242194;
// 3. 閏年によるリセット量=INT{(西暦年−2000年)/ 4}
int 閏年補正 = (int)((year - 2000) / 4.0);
// 求める年の春分日=INT{(1)+(2)−(3)}
int 春分日 = (int)(基準日 + 移動量 - 閏年補正);
return new DateTime(year, 3, 春分日);
}
// 秋分日を求める(2099年まで有効な実験式)
private static DateTime CalcAutumnalEquinoxDay(int year)
{
// 1. 2000年の太陽の秋分点通過日
double 基準日 = 23.09; // 秋分点の揺らぎ補正済みの値
// 2. 秋分点通過日の移動量=(西暦年−2000年)×0.242194
double 移動量 = (year - 2000) * 0.242194;
// 3. 閏年によるリセット量=INT{(西暦年−2000年)/ 4}
int 閏年補正 = (int)((year - 2000) / 4.0);
// 求める年の秋分日=INT{(1)+(2)−(3)}
int 秋分日 = (int)(基準日 + 移動量 - 閏年補正);
return new DateTime(year, 9, 秋分日);
}
' 春分日を求める(2099年まで有効な実験式)
Private Function CalcVernalEquinoxDay(year As Integer) As DateTime
' 1. 2000年の太陽の春分点通過日
Dim 基準日 As Double = 20.69115
' 2. 春分点通過日の移動量=(西暦年−2000年)×0.242194
Dim 移動量 As Double = (year - 2000) * 0.242194
' 3. 閏年によるリセット量=INT{(西暦年−2000年)/ 4}
Dim 閏年補正 As Integer = CType(Int((year - 2000) / 4.0), Integer)
' 求める年の春分日=INT{(1)+(2)−(3)}
Dim 春分日 As Integer = CType(Int(基準日 + 移動量 - 閏年補正), Integer)
Return New DateTime(year, 3, 春分日)
End Function
' 秋分日を求める(2099年まで有効な実験式)
Private Function CalcAutumnalEquinoxDay(year As Integer) As DateTime
' 1. 2000年の太陽の秋分点通過日
Dim 基準日 As Double = 23.09 ' 秋分点の揺らぎ補正済みの値
' 2. 秋分点通過日の移動量=(西暦年−2000年)×0.242194
Dim 移動量 As Double = (year - 2000) * 0.242194
' 3. 閏年によるリセット量=INT{(西暦年−2000年)/ 4}
Dim 閏年補正 As Integer = CType(Int((year - 2000) / 4.0), Integer)
' 求める年の秋分日=INT{(1)+(2)−(3)}
Dim 秋分日 As Integer = CType(Int(基準日 + 移動量 - 閏年補正), Integer)
Return New DateTime(year, 9, 秋分日)
End Function
*2 春分日/秋分日とは、太陽が春分点/秋分点(=天の赤道と黄道が交差する点)を通過する瞬間を含んだ日。実験式とは、過去の観測データから導き出された式。未来の春分点/秋分点は天体の運動を計算すれば求められるように思える。しかし、まだ見つかっていない天体が将来において影響を及ぼす可能性がないとはいえないため、未来の春分点/秋分点を正確に決定することは不可能である。ここでは、「暦と天文の雑学〜将来の春分日・秋分日の計算」に掲載されている実験式を用いた。これは2099年まで合っているとされる。なお、国立天文台のWebページには2030年までの表が掲載されているので、それを利用するのも一案だ。
* 2015/07/24追記 [参考]
春分の日と秋分の日を求める計算式が「int 閏年補正 = (int)((year - 2000) / 4.0);」となっている(C#の場合。VBも同様)。これは以下の条件から実験式が有効な範囲では常に4年に一度閏年があるためだ。
なお、これは実験式であるので、その適用範囲外での正当性は保証されない。実際、1917年の秋分点は9月24日であったが、この実験式を無理に当てはめてみると9月23日という結果になった。
これらのメソッドを使って、法令で指定されている春分の日/秋分の日をコレクションに加えるコードは、次のようになる。なお念を押しておくと、春分の日/秋分の日は公式にはその前年の2月に官報で発表されるまで確定しない。
// 春分の日
var 春分の日 = new Holiday()
{
Date = CalcVernalEquinoxDay(Year),
Kind = HolidayKind.国民の祝日,
Name = "春分の日", Definition = "春分日",
};
holidays.Add(春分の日.Date, 春分の日);
// 秋分の日
var 秋分の日 = new Holiday()
{
Date = CalcAutumnalEquinoxDay(Year),
Kind = HolidayKind.国民の祝日,
Name = "秋分の日", Definition = "秋分日",
};
holidays.Add(秋分の日.Date, 秋分の日);
' 春分の日
Dim 春分の日 = New Holiday() With
{
.Date = CalcVernalEquinoxDay(Year),
.Kind = HolidayKind.国民の祝日,
.Name = "春分の日", .Definition = "春分日"
}
holidays.Add(春分の日.Date, 春分の日)
' 秋分の日
Dim 秋分の日 = New Holiday() With
{
.Date = CalcAutumnalEquinoxDay(Year),
.Kind = HolidayKind.国民の祝日,
.Name = "秋分の日", .Definition = "秋分日"
}
holidays.Add(秋分の日.Date, 秋分の日)
法に定められた規則をそのままロジックに書き直せばよい。
国民の祝日に関する法律第三条2項には、「『国民の祝日』が日曜日に当たるときは、その日後においてその日に最も近い「国民の祝日」でない日を休日とする」と定められている。このロジックを「GetSubstituteHolidays」メソッドとしてまとめると、次のコードのようになる。
// 振替休日を全て求める
private static IEnumerable<Holiday> GetSubstituteHolidays(
SortedDictionary<DateTime, Holiday> holidays)
{
// 振替休日を格納するためのコレクション
List<Holiday> substituteHolidays = new List<Holiday>();
// これまでに求めた祝日を全部チェックする
foreach (var holiday in holidays.Values)
{
if (holiday.Date.DayOfWeek != DayOfWeek.Sunday)
continue; // 日曜でなければ除外する
// 翌日(=月曜日)を仮に振替休日とする
DateTime substitute = holiday.Date.AddDays(1.0);
// その日がすでに祝日ならば振替休日はさらにその翌日
while (holidays.ContainsKey(substitute))
substitute = substitute.AddDays(1.0);
// 見つかった振替休日をコレクションに追加する
var substituteHoliday = new Holiday()
{
Date = substitute,
Kind = HolidayKind.振替休日,
Name = "振替休日",
Definition = string.Format("{0}の振替休日", holiday.Name),
};
substituteHolidays.Add(substituteHoliday);
}
return substituteHolidays;
}
' 振替休日を全て求める
Private Function GetSubstituteHolidays(
holidays As SortedDictionary(Of DateTime, Holiday)) As IEnumerable(Of Holiday)
' 振替休日を格納するためのコレクション
Dim substituteHolidays As List(Of Holiday) = New List(Of Holiday)()
' これまでに求めた祝日を全部チェックする
For Each holiday In holidays.Values
If (holiday.Date.DayOfWeek <> DayOfWeek.Sunday) Then
Continue For ' 日曜でなければ除外する
End If
' 翌日(=月曜日)を仮に振替休日とする
Dim substitute As DateTime = holiday.Date.AddDays(1.0)
' その日がすでに祝日ならば振替休日はさらにその翌日
While (holidays.ContainsKey(substitute))
substitute = substitute.AddDays(1.0)
End While
' 見つかった振替休日をコレクションに追加する
Dim substituteHoliday = New Holiday() With
{
.Date = substitute,
.Kind = HolidayKind.振替休日,
.Name = "振替休日",
.Definition = String.Format("{0}の振替休日", holiday.Name)
}
substituteHolidays.Add(substituteHoliday)
Next
Return substituteHolidays
End Function
このメソッドを使って、法令で指定されている振替休日をコレクションに加えるコードは、次のようになる。
var substituteHolidays = GetSubstituteHolidays(holidays);
foreach (var s in substituteHolidays)
holidays.Add(s.Date, s);
Dim substituteHolidays = GetSubstituteHolidays(holidays)
For Each s In substituteHolidays
holidays.Add(s.Date, s)
Next
法に定められた規則をそのままロジックに書き直せばよい。
国民の祝日に関する法律第三条3項には、「その前日及び翌日が『国民の祝日』である日(『国民の祝日』でない日に限る。)は、休日とする」と定められている。このロジックを「GetSandwichedHolidays」メソッドとしてまとめると、次のコードのようになる。
// 国民の休日を全て求める
private static IEnumerable<Holiday> GetSandwichedHolidays(
SortedDictionary<DateTime, Holiday> holidays)
{
List<Holiday> sandwichedHolidays = new List<Holiday>();
// これまでに求めた祝日を全部チェックする
foreach (var holiday0 in holidays.Values)
{
if (holiday0.Kind != HolidayKind.国民の祝日)
continue; // その休日が国民の祝日でなければ除外する
var day0 = holiday0.Date;
var day2 = day0.AddDays(2.0); // 2日後
if (!holidays.ContainsKey(day2))
continue; // 2日後が祝日でないときは除外する
var holiday2 = holidays[day2];
if (holiday2.Kind != HolidayKind.国民の祝日)
continue; // 2日後が祝日であっても国民の祝日でなければ除外する
var day1 = day0.AddDays(1.0); // 1日後=国民の祝日で挟まれた日
if (day1.DayOfWeek == DayOfWeek.Sunday)
continue; // その日が日曜(=もともと休日)のときは除外する
if (holidays.ContainsKey(day1))
continue; // その日がすでに祝日のときは除外する
// 見つかった国民の休日をコレクションに追加する
var sandwichedHoliday = new Holiday()
{
Date = day1,
Kind = HolidayKind.国民の休日,
Name = "国民の休日",
Definition = string.Format("{0}と{1}の間の日", holiday0.Name, holiday2.Name),
};
sandwichedHolidays.Add(sandwichedHoliday);
}
return sandwichedHolidays;
}
' 国民の休日を全て求める
Private Function GetSandwichedHolidays(
holidays As SortedDictionary(Of DateTime, Holiday)) As IEnumerable(Of Holiday)
Dim sandwichedHolidays As List(Of Holiday) = New List(Of Holiday)()
' これまでに求めた祝日を全部チェックする
For Each holiday0 In holidays.Values
If (holiday0.Kind <> HolidayKind.国民の祝日) Then
Continue For ' その休日が国民の祝日でなければ除外する
End If
Dim day0 = holiday0.Date
Dim day2 = day0.AddDays(2.0) ' 2日後
If (Not holidays.ContainsKey(day2)) Then
Continue For ' 2日後が祝日でないときは除外する
End If
Dim holiday2 = holidays(day2)
If (holiday2.Kind <> HolidayKind.国民の祝日) Then
Continue For ' 2日後が祝日であっても国民の祝日でなければ除外する
End If
Dim day1 = day0.AddDays(1.0) ' 1日後=国民の祝日で挟まれた日
If (day1.DayOfWeek = DayOfWeek.Sunday) Then
Continue For ' その日が日曜(=もともと休日)のときは除外する
End If
If (holidays.ContainsKey(day1)) Then
Continue For ' その日がすでに祝日のときは除外する
End If
' 見つかった国民の休日をコレクションに追加する
Dim sandwichedHoliday = New Holiday() With
{
.Date = day1,
.Kind = HolidayKind.国民の休日,
.Name = "国民の休日",
.Definition = String.Format("{0}と{1}の間の日", holiday0.Name, holiday2.Name)
}
sandwichedHolidays.Add(sandwichedHoliday)
Next
Return sandwichedHolidays
End Function
このメソッドを使って、法令で指定されている国民の休日をコレクションに加えるコードは、次のようになる。
var sandwichedHolidays = GetSandwichedHolidays(holidays);
foreach (var s in sandwichedHolidays)
holidays.Add(s.Date, s);
Dim sandwichedHolidays = GetSandwichedHolidays(holidays)
For Each s In sandwichedHolidays
holidays.Add(s.Date, s)
Next
以上でようやく完成である。出来上がったプログラムの実行結果を次の画像に示す。
日本の祝日を求めるロジックは厄介ではあるが、場合分けしてロジックを個別に組み立てていけば作り上げられる。ただし、法改正があるたびにロジックの修正が必要になる。本稿で示したコードは過去の法改正を取り込んでいないため、2007年以前には適用できない(過去の法改正を調べ上げてロジックを調整するよりも、過去の祝日はテーブルを作ってしまう方が楽だと思われる*3)。また、春分の日/秋分の日は、実験式で求めた春分日/秋分日と異なることはないとは思うが、前年2月の公式発表を確認するのを忘れないでほしい。
*3 日本の祝日を定めた過去の法改正を全て調べ上げるとなると、少なくとも「明治6年10月14日太政官第344号布告」(近代デジタルライブラリー収蔵)までさかのぼることになるだろう。昭和以降に限ったとしても大正元年勅令19号まで、戦後に限っても昭和2年勅令25号までさかのぼる必要がある。
利用可能バージョン: .NET Framework 3.5以降
カテゴリ: クラスライブラリ 処理対象:日付と時刻
使用ライブラリ: DateTime構造体(System名前空間)
使用ライブラリ: SortedDictionaryクラス(System.Collections.Generic名前空間)
関連TIPS: 指定した月から特定の曜日の日付を取得するには?[C#、VB]
関連TIPS: 日時や時間間隔の加減算を行うには?
Copyright© Digital Advantage Corp. All Rights Reserved.