日付の年号を表示するには?[独自テーブル参照編]:.NET TIPS
Windows 10 1803で改元に伴う年号表示に混乱が起こっている(将来も同様なことが起こるかもしれない)。そこで、自前で年号表示を行う方法を解説する。
.NET Framework 4以降の年号(=元号)に関するAPIは、Windowsのレジストリに依存している。そのため、改元されたときにはWindows Updateによりレジストリが更新され、APIが返す値にも自動的に新しい年号が使われるようになる。逆に、レジストリに誤ったデータを入れられてしまうことがあると、APIが返す結果もおかしなものになってしまう*1。
本稿では、Windowsのレジストリに依存せずに独自のテーブルを使って日付から年号の文字列と和暦の年を得る方法を、2通り紹介する。レジストリに依存する方法については「日付の年号を略称で表示するには?[C#、VB]」をご覧いただきたい。
*1 Window 10バージョン1803では、平成の次の新元号が公布されるまでの間、2019年5月1日以降の年号を取得すると「??」などとなる(公式ブログ参照)。その次以降の改元のときにも同様な措置を取られる可能性もあるだろう。
年号が「??」などと表示されることがあっても構わないのであれば、自動的に新年号対応がされるレジストリに依存する方法が得策だ。年号が「??」などと表示されてはならない場合は、メンテナンスが必要になるものの本稿のような手法が適しているだろう。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2015以降が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using static System.Console;
Imports System.Console
DataTableを使う方法
この方法は、.NET Framwork 1.1から使える(サンプルコードにはVisual Studio 2015以降の機能も使っている)。
データベースにアクセスする業務アプリでは、年号テーブルをデータベースに持っていることも多いだろう。その年号テーブルのデータをDataTableオブジェクト(System.Data名前空間)に取り込んだような場合を考えよう。
そのようなDataTableオブジェクトをメンバに持つJapaneseEraTableクラス(VBではモジュール)は、次のコードのような形になる。ここではサンプルということで、DataTableオブジェクトにコードで直接データをセットしている。大切なのは、テーブルに入れる年号を降順に(新しい年号を先頭に)しておくことだ。そうしておくことで、日付から年号を検索する処理が簡単になる。
public static class JapaneseEraTable
{
// 年号テーブル
private static DataTable _eraTable;
// 静的コンストラクタ:テーブルを初期化する
static JapaneseEraTable()
{
_eraTable = new DataTable();
_eraTable.Columns.Add("StartDate", typeof(DateTime));
_eraTable.Columns.Add("Name", typeof(string));
_eraTable.Columns.Add("EnglishName", typeof(string));
// 必ず日付の降順に並べる
_eraTable.Rows.Add(new DateTime(1989, 1, 8), "平成", "Heisei");
_eraTable.Rows.Add(new DateTime(1926, 12, 25), "昭和", "Showa");
_eraTable.Rows.Add(new DateTime(1912, 7, 30), "大正", "Taisho");
_eraTable.Rows.Add(new DateTime(1868, 1, 25), "明治", "Meiji");
}
}
Public Module JapaneseEraTable
' 年号テーブル
Private _eraTable As DataTable
' 静的コンストラクタ:テーブルを初期化する
Sub New()
_eraTable = New DataTable()
_eraTable.Columns.Add("StartDate", GetType(DateTime))
_eraTable.Columns.Add("Name", GetType(String))
_eraTable.Columns.Add("EnglishName", GetType(String))
' 必ず日付の降順に並べる
_eraTable.Rows.Add(New DateTime(1989, 1, 8), "平成", "Heisei")
_eraTable.Rows.Add(New DateTime(1926, 12, 25), "昭和", "Showa")
_eraTable.Rows.Add(New DateTime(1912, 7, 30), "大正", "Taisho")
_eraTable.Rows.Add(New DateTime(1868, 1, 25), "明治", "Meiji")
End Sub
End Module
ここではサンプルということで、DataTableオブジェクトにコードで直接データをセットしている。データベースから取得する場合は、この静的コンストラクタは削除し、テーブルを初期化する静的メソッドを別途設けるようにする。その際には、開始日での並べ替えをクエリに付け忘れないようにしよう。
なお、明治元年の開始日はその年の1月1日であるが、それは旧暦であり、現在の西暦(グレゴリオ暦)で表すと1868年1月25日に当たる。明治の改暦によって西暦と一致するようになったのは明治6年1月1日(=1873年1月1日)からであり、それ以前の旧暦による日付を扱うときには注意が必要である。
日付から年号を得るには、この年号テーブルの先頭から順にStartDateカラム(=年号の開始日)と日付を比較していく。その日付以前の開始日が見つかったら、それが該当する年号である(冒頭の図を参照)。
年号テーブルから日付に該当するRowオブジェクト(System.Data名前空間)を取り出すメソッドは次のコードのようになる。
public static class JapaneseEraTable
{
……省略……
private static DataRow GetEraRow(DateTime theDate)
{
// 新しい元号から順に見ていく
for (int i = 0; i < _eraTable.Rows.Count; i++)
{
var row = _eraTable.Rows[i];
// その日以前の元号開始日が見つかったら、それがその日の元号
if ((DateTime)row["StartDate"] <= theDate)
return row;
}
throw new ArgumentOutOfRangeException();
}
}
Public Module JapaneseEraTable
……省略……
Private Function GetEraRow(theDate As DateTime) As DataRow
' 新しい元号から順に見ていく
For i As Integer = 0 To (_eraTable.Rows.Count - 1)
Dim row = _eraTable.Rows(i)
' その日以前の元号開始日が見つかったら、それがその日の元号
If (CType(row("StartDate"), DateTime) <= theDate) Then
Return row
End If
Next
Throw New ArgumentOutOfRangeException()
End Function
End Module
年号テーブルを年号の降順に作ったので、開始日との比較だけで済む。
もしもテーブル内での並び順が不定であるならば、データとして開始日だけでなく終了日も持たせる必要があり、比較も開始日だけでなく終了日とも行うことになる。
後は、取り出したRowオブジェクトから年号の文字列を得るだけだ(次のコード)。
public static class JapaneseEraTable
{
……省略……
// 日付から年号の文字列を得る
public static string GetName(DateTime theDate)
{
return GetEraRow(theDate)["Name"] as string;
}
// 日付から年号の頭文字を得る
public static string GetFirstLetter(DateTime theDate)
{
return GetName(theDate).Substring(0, 1);
}
// 日付から年号の文字列を得る(ローマ字)
public static string GetEnglishName(DateTime theDate)
{
return GetEraRow(theDate)["EnglishName"] as string;
}
// 日付から年号の頭文字を得る(ローマ字)
public static string GetEnglishFirstLetter(DateTime theDate)
{
return GetEnglishName(theDate).Substring(0, 1);
}
}
Public Module JapaneseEraTable
……省略……
' 日付から年号の文字列を得る
Public Function GetName(theDate As DateTime) As String
Return CStr(GetEraRow(theDate)("Name"))
End Function
' 日付から年号の頭文字を得る
Public Function GetFirstLetter(theDate As DateTime) As String
Return GetName(theDate).Substring(0, 1)
End Function
' 日付から年号の文字列を得る(ローマ字)
Public Function GetEnglishName(theDate As DateTime) As String
Return CStr(GetEraRow(theDate)("EnglishName"))
End Function
' 日付から年号の頭文字を得る(ローマ字)
Public Function GetEnglishFirstLetter(theDate As DateTime) As String
Return GetEnglishName(theDate).Substring(0, 1)
End Function
End Module
また、次のコードのような、その年号での年(西暦2018年なら平成30年の30)を計算するメソッドも必要だ。
public static class JapaneseEraTable
{
……省略……
// 日付から和暦の年を計算する
public static int GetEraYear(DateTime theDate)
{
var eraStartDate = (DateTime)GetEraRow(theDate)["StartDate"];
return theDate.Year - eraStartDate.Year + 1;
}
}
Public Module JapaneseEraTable
……省略……
' 日付から和暦の年を計算する
Public Function GetEraYear(theDate As DateTime) As Integer
Dim eraStartDate = CType(GetEraRow(theDate)("StartDate"), DateTime)
Return theDate.Year - eraStartDate.Year + 1
End Function
End Module
まず、年号テーブルから日付に該当する年号の開始日を得る(例えば、平成なら1989/1/ 8)。指定日付の西暦年(例えば2018)から開始日の西暦年(例えば1989)を引いて、そこに1を足せば和暦での年(例えば30)になる。
さらに進めて、渡した日付を指定したフォーマットの文字列にして返すメソッドなども実装すると便利であるが、ここでは割愛する。
以上のメソッドをコンソールアプリから呼び出した例を、次のコードに示す。
var d1 = new DateTime(1926, 12, 24, 23, 59, 59);
string n1 = JapaneseEraTable.GetName(d1);
int y1 = JapaneseEraTable.GetEraYear(d1);
WriteLine($"{d1:yyyy/MM/dd} {n1}{y1:00}年{d1:MM月dd日}");
// 出力:1926/12/24 大正15年12月24日
var d2 = new DateTime(1926, 12, 25);
string n2 = JapaneseEraTable.GetFirstLetter(d2);
int y2 = JapaneseEraTable.GetEraYear(d2);
WriteLine($"{d2:yyyy/MM/dd} {n2}{y2}年{d2:M月d日}");
// 出力:1926/12/25 昭1年12月25日
var d3 = new DateTime(1989, 1, 8);
string n3 = JapaneseEraTable.GetEnglishName(d3);
int y3 = JapaneseEraTable.GetEraYear(d3);
WriteLine($"{d3:yyyy/MM/dd} {n3}{y3:00}/{d3:MM/dd}");
// 出力:1989/01/08 Heisei01/01/08
var d4 = new DateTime(2019, 5, 1);
string n4 = JapaneseEraTable.GetEnglishFirstLetter(d4);
int y4 = JapaneseEraTable.GetEraYear(d4);
WriteLine($"{d4:yyyy/MM/dd} {n4}{y4}/{d4:M/d}");
// 出力:2019/05/01 H31/5/1
Dim d1 = New DateTime(1926, 12, 24, 23, 59, 59)
Dim n1 As String = JapaneseEraTable.GetName(d1)
Dim y1 As Integer = JapaneseEraTable.GetEraYear(d1)
WriteLine($"{d1:yyyy/MM/dd} {n1}{y1:00}年{d1:MM月dd日}")
' 出力:1926/12/24 大正15年12月24日
Dim d2 = New DateTime(1926, 12, 25)
Dim n2 As String = JapaneseEraTable.GetFirstLetter(d2)
Dim y2 As Integer = JapaneseEraTable.GetEraYear(d2)
WriteLine($"{d2:yyyy/MM/dd} {n2}{y2}年{d2:M月d日}")
' 出力:1926/12/25 昭1年12月25日
Dim d3 = New DateTime(1989, 1, 8)
Dim n3 As String = JapaneseEraTable.GetEnglishName(d3)
Dim y3 As Integer = JapaneseEraTable.GetEraYear(d3)
WriteLine($"{d3:yyyy/MM/dd} {n3}{y3:00}/{d3:MM/dd}")
' 出力:1989/01/08 Heisei01/01/08
Dim d4 = New DateTime(2019, 5, 1)
Dim n4 As String = JapaneseEraTable.GetEnglishFirstLetter(d4)
Dim y4 As Integer = JapaneseEraTable.GetEraYear(d4)
WriteLine($"{d4:yyyy/MM/dd} {n4}{y4}/{d4:M/d}")
' 出力:2019/05/01 H31/5/1
このDataTableクラスを使う方法は、年号の名称や和暦の年を求めるその都度、元号の探索と型キャストが発生してしまう。型付きデータセットにして、その行オブジェクトをそのまま返してしまうようにすれば、そういった問題は解決できるものの大掛かりになり過ぎる。それくらいなら、次に示すように独自の型を定義してしまった方がよいだろう。
独自の型とLINQを使う方法
上記のDataTableクラスを使う方法でも実用上の問題はあまりないだろうが、年号を表す独自の型を定義した方がスマートに書ける。また、.NET Framework 3.5以降ならば、LINQを使うと年号を取り出す処理も1行で書けてしまう(サンプルコードではVisual Studio 2015以降の機能も使っている)。
年号を表す独自の型として、次のコードのようなJapaneseEraクラスを考えよう。公開する読み取り専用プロパティとして、年号の開始日と名称を持たせた。プロパティの初期化は、privateなコンストラクタで行うようにした(後ほど、このクラス内にコンストラクタを呼び出すコードを追加する)。
public class JapaneseEra
{
// 元号の開始日
public DateTime StartDate { get; }
// 元号の名称
public string Name { get; }
// 元号の頭文字
public string FirstLetter { get; }
// ローマ字表記の名称
public string EnglishName { get; }
// ローマ字表記の頭文字
public string EnglishFirstLetter { get; }
// コンストラクタ
private JapaneseEra(DateTime startDate, string name, string englishName)
{
StartDate = startDate;
Name = name;
FirstLetter = Name.Substring(0, 1);
EnglishName = englishName;
EnglishFirstLetter = EnglishName.Substring(0, 1);
}
}
Public Class JapaneseEra
' 元号の開始日
Public ReadOnly Property StartDate As DateTime
' 元号の名称
Public ReadOnly Property Name As String
' 元号の頭文字
Public ReadOnly Property FirstLetter As String
' ローマ字表記の名称
Public ReadOnly Property EnglishName As String
' ローマ字表記の頭文字
Public ReadOnly Property EnglishFirstLetter As String
' コンストラクタ
Private Sub New(startDate As DateTime, name As String, englishName As String)
Me.StartDate = startDate
Me.Name = name
Me.FirstLetter = Me.Name.Substring(0, 1)
Me.EnglishName = englishName
Me.EnglishFirstLetter = Me.EnglishName.Substring(0, 1)
End Sub
End Class
コンストラクタをprivateにしているが、それを呼び出すコードを後ほどこのクラス内に追加する。
ところで、日付から該当する年号を知りたいので、このクラスには年号テーブルを表す静的プロパティも欲しい。前述した方法のDataTableオブジェクトに相当するものだ。それをJapaneseEraクラスの読み取り専用コレクションにしておけば、publicにしても問題ない。
ここではサンプルということで、年号テーブルを表す静的プロパティの宣言部分で、コレクションの初期化も(従って、格納するJapaneseEraインスタンスの生成も)やってしまおう(次のコード)。年号のデータを設定ファイルやデータベースなどから取得する場合は、コレクションを初期化する静的メソッドを別途設けて、アプリの初期化時に呼び出すようにする。
public class JapaneseEra
{
……省略……
// 静的プロパティ:JapaneseEraインスタンスを収めた読み取り専用のリスト
public static IList<JapaneseEra> Table { get; }
= new List<JapaneseEra>() // 順序が保証されるコレクションを使う
{
// 必ず降順に並べる
new JapaneseEra(new DateTime(1989,1,8), "平成", "Heisei"),
new JapaneseEra(new DateTime(1926,12,25), "昭和", "Showa"),
new JapaneseEra(new DateTime(1912,7,30), "大正", "Taisho"),
new JapaneseEra(new DateTime(1868,1,25), "明治", "Meiji"),
}
.AsReadOnly(); // リストの内容を変更できないようにする
}
Public Class JapaneseEra
……省略……
' 静的プロパティ:JapaneseEraインスタンスを収めた読み取り専用のリスト
' ・順序が保証されるコレクションを使う
' ・必ず降順に並べる
Public Shared ReadOnly Property Table As IList(Of JapaneseEra) _
= New List(Of JapaneseEra) From
{
New JapaneseEra(New DateTime(1989, 1, 8), "平成", "Heisei"),
New JapaneseEra(New DateTime(1926, 12, 25), "昭和", "Showa"),
New JapaneseEra(New DateTime(1912, 7, 30), "大正", "Taisho"),
New JapaneseEra(New DateTime(1868, 1, 25), "明治", "Meiji")
} _
.AsReadOnly() ' リストの内容を変更できないようにする
End Class
DataTableを利用する方法と同じく、年号の並び順は必ず降順にしておく。
さて、上記のコレクションから日付に該当するJapaneseEraインスタンスを得るには、次のコードに示す静的メソッドを用意すればよい。LINQを使って1行で書ける。開始日との比較だけで正しい年号が選択できる理由は、本稿冒頭の図をご覧いただきたい。
public class JapaneseEra
{
……省略……
// 日付に該当するJapaneseEraインスタンスを得る
public static JapaneseEra GetEra(DateTime theDate)
=> Table.FirstOrDefault(era => (era.StartDate <= theDate));
}
Public Class JapaneseEra
……省略……
' 日付に該当するJapaneseEraインスタンスを得る
Public Shared Function GetEra(theDate As DateTime) As JapaneseEra
Return Table.FirstOrDefault(Function(era) era.StartDate <= theDate)
End Function
End Class
コレクションを年号の降順に作ったので、開始日との比較だけで済む。
もしもコレクション内での並び順が不定であるならば、データとして開始日だけでなく終了日も持たせる必要があり、比較も開始日だけでなく終了日とも行うことになる。
日付から該当する年号での年(西暦2018年なら平成30年)を求める計算は、インスタンスメソッドとして実装しよう(次のコード)。
public class JapaneseEra
{
……省略……
// 日付から和暦の年を得る
public int GetEraYear(DateTime theDate)
=> theDate.Year - this.StartDate.Year + 1;
}
Public Class JapaneseEra
……省略……
' 日付から和暦の年を得る
Public Function GetEraYear(theDate As DateTime) As Integer
Return theDate.Year - Me.StartDate.Year + 1
End Function
End Class
その日付の西暦年(例えば2018)から開始日の西暦年(例えば1989)を引いて、そこに1を足せば和暦での年(例えば30)になる。
さらに進めて、渡した日付を指定した書式の文字列にして返すメソッドなども実装すると便利であるが、ここでは割愛する。
以上のメソッドをコンソールアプリから呼び出した例を、次のコードに示す。
var d1 = new DateTime(1926, 12, 24, 23, 59, 59);
var e1 = JapaneseEra.GetEra(d1);
WriteLine($"{d1:yyyy/MM/dd} {e1.Name}{e1.GetEraYear(d1):00}年{d1:MM月dd日}");
// 出力:1926/12/24 大正15年12月24日
var d2 = new DateTime(1926, 12, 25);
var e2 = JapaneseEra.GetEra(d2);
WriteLine($"{d2:yyyy/MM/dd} {e2.FirstLetter}{e2.GetEraYear(d2)}年{d2:M月d日}");
// 出力:1926/12/25 昭1年12月25日
var d3 = new DateTime(1989, 1, 8);
var e3 = JapaneseEra.GetEra(d3);
WriteLine($"{d3:yyyy/MM/dd} {e3.EnglishName}{e3.GetEraYear(d3):00}/{d3:MM/dd}");
// 出力:1989/01/08 Heisei01/01/08
var d4 = new DateTime(2019, 5, 1);
var e4 = JapaneseEra.GetEra(d4);
WriteLine($"{d4:yyyy/MM/dd} {e4.EnglishFirstLetter}{e4.GetEraYear(d4)}/{d4:M/d}");
// 出力:2019/05/01 H31/5/1
Dim d1 = New DateTime(1926, 12, 24, 23, 59, 59)
Dim e1 = JapaneseEra.GetEra(d1)
WriteLine($"{d1:yyyy/MM/dd} {e1.Name}{e1.GetEraYear(d1):00}年{d1:MM月dd日}")
' 出力:1926/12/24 大正15年12月24日
Dim d2 = New DateTime(1926, 12, 25)
Dim e2 = JapaneseEra.GetEra(d2)
WriteLine($"{d2:yyyy/MM/dd} {e2.FirstLetter}{e2.GetEraYear(d2)}年{d2:M月d日}")
' 出力:1926/12/25 昭1年12月25日
Dim d3 = New DateTime(1989, 1, 8)
Dim e3 = JapaneseEra.GetEra(d3)
WriteLine($"{d3:yyyy/MM/dd} {e3.EnglishName}{e3.GetEraYear(d3):00}/{d3:MM/dd}")
' 出力:1989/01/08 Heisei01/01/08
Dim d4 = New DateTime(2019, 5, 1)
Dim e4 = JapaneseEra.GetEra(d4)
WriteLine($"{d4:yyyy/MM/dd} {e4.EnglishFirstLetter}{e4.GetEraYear(d4)}/{d4:M/d}")
' 出力:2019/05/01 H31/5/1
GetEraメソッドでJapaneseEraオブジェクトを取得したら、後はそのインスタンスを使って年号の名称を得たり和暦の年を計算したりできる。
まとめ
年号をその開始日の降順になるようにテーブルを作っておけば、日付から該当する年号を取り出す処理が簡単になる。その年号テーブルには、DataTableクラスを使ってもよいが、独自の型のコレクションにした方がスマートに書ける。
利用可能バージョン:.NET Framework 1.1以降(2番目の方法は.NET Framework 3.5以降)
カテゴリ:クラスライブラリ 処理対象:日付と時刻
使用ライブラリ:DataTableクラス(System.Data名前空間)
関連TIPS:日付の年号を略称で表示するには?[C#、VB]
関連TIPS:西暦と和暦を変換するには?
関連TIPS:日付や時刻を文字列に変換するには?
関連TIPS:日付や時刻の文字列をDateTimeオブジェクトに変換するには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0/7.0]
関連TIPS:構文:コレクションのインスタンス化と同時に要素を追加するには?[C#/VB]
関連TIPS:構文:定数(変更できないもの)を作るには?[C#/VB]
Copyright© Digital Advantage Corp. All Rights Reserved.