Edited at

Delphi での新元号対応


はじめに

新元号 "令和" が発表されましたね!

See also:


Delphi での新元号対応

Windows 限定の話をすると、基本的には Windows Update でレジストリキーが降ってくれば終わりです。

追記: 2019/04/26

Windows 10 (1809) 以外は 4/26 のアップデートで元号レジストリに令和が追加されます。


エンバカさんの記事

事前にエンバカデロからも新元号対応の記事が出ていました。


レジストリファイル

こんなもんですよね。


SetNewEra.reg

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese\Eras]
"2019 05 01"="令和_令_Reiwa_R"

...ただ。


Note that this only impacts machines running Windows 7 and later or .NET Framework 4 and later.


XP とか Vista だと自前処理なんですかね?



XP / Vista いずれにも [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars] 以下がありません。

追記 2019/04/11

Windows Update により、Win32 API の一部は 2019/05/01 を過ぎるまでは上記レジストリを見なくなる事があるようです。Delphi だと DateTimeToString() や FormatDateTime() が影響を受けます。

OS
KB

Windows 10
KB4493509

Windows 8.1
KB4493446

Windows 7
KB4493472

上記アップデータの適用にはご注意ください。

追記: 2019/04/26

修正モジュールが公開されました。

オプション扱いなので自動更新はされませんが、Windows 10 は Windows Update の画面で[更新プログラムのチェック]ボタンを押してしまうと更新されてしまいますので、ご注意ください。

Windows
モジュール
モジュール
(SO)

Windows 10 (1809)
未提供
(後日提供)

Windows 10 (1803)
KB4493437

Windows 10 (1709)
KB4493440

Windows 10 (1703)
KB4493436

Windows 10 (1607)
Windows Server 2016
KB4493473

Windows 10 (RTM)
KB4498375

Windows 8.1
Windows Server 2012 R2
KB4493443
KB4496878

Windows Server 2012
KB4493462
KB4496877

Windows 7 SP1
Windows Server 2008 R2 SP1
KB4493453
KB4496880

Windows Server 2008 SP2
KB4493460
KB4496879

これらの修正モジュールをインストールすると元号レジストリに 令和 が追加されます。但し、これらの修正モジュールをインストールしても、Win32 API の一部は 2019/05/01 になるまで元号レジストリを参照しません。

See also:


VarToDateTime()

VarToDateTime() は上記レジストリを見ていないようなので注意が必要です。

※ VarToDateTime() は OleAut32.dll の VariantChangeTypeEx() を利用しています。

追記 2019/04/12

Windows Update により、VarToDateTime() がレジストリを参照するようになります。こちらは 2019/05/01 を過ぎていなくても変換されるようです。


VarToDateTime() に渡す文字列を細工する

VarToDateTime() は 平成 には対応しているのですから、日付文字列中に 令和 が出現したら 平成 に置換し、年も +30 して置換する関数を作ればよさそうです。

元号
Year

Month

Day

令和
02

12

22

↓ ↓ ↓

元号
Year

Month

Day

平成
(置換)
32
(+30)

12

22

...という事で、関数 CheckEraDateString() を作ってみました。Delphi 7 等の古い ANSI 版 Delphi でも動作します。

  uses

..., System.SysUtils;

function CheckEraDateString(EraDateString: string): string;
const
FIRST_YEAR = '元年';
ErasH: array [0..3] of string = ('平成', '平', 'Heisei', 'H');
ErasR: array [0..3] of string = ('令和', '令', 'Reiwa', 'R');
var
i, EraIdx, EraStrIdx, EraYearStart, Year: Integer;
P: PChar;
begin
result := EraDateString;
EraIdx := -1;
for i := Low(ErasR) to High(ErasR) do
begin
EraStrIdx := Pos(ErasR[i], EraDateString);
if EraStrIdx > 0 then
begin
EraIdx := i;
Break;
end;
end;
if EraIdx = -1 then
Exit;
if Pos(FIRST_YEAR, Result) > 0 then
Result := StringReplace(Result, FIRST_YEAR, '1年', []);
Delete(result, EraStrIdx, Length(ErasR[EraIdx]));
Insert(ErasH[EraIdx], result, EraStrIdx);
EraStrIdx := EraStrIdx + Length(ErasH[EraIdx]) - 1;
EraYearStart := -1;
Year := 0;
P := PChar(result);
Inc(P, EraStrIdx);
while P^ <> #$00 do
begin
Inc(EraStrIdx);
{$IFDEF UNICODE}
if not CharInSet(P^, ['0'..'9']) then
{$ELSE}
if not (P^ in ['0'..'9']) then
{$ENDIF}
begin
if EraYearStart < 0 then
begin
Inc(P);
Continue;
end;
Break;
end;
if EraYearStart < 0 then
EraYearStart := EraStrIdx;
Year := Year * 10 + (Ord(P^) - Ord('0'));
Inc(P);
end;
Delete(result, EraYearStart, EraStrIdx - EraYearStart);
Insert(IntToStr(Year + 30), result, EraYearStart);
end; { CheckEraDateString }

使い方は

  Dt := VarToDateTime('令和02年12月22日');

 ↓
Dt := VarToDateTime(CheckEraDateString('令和02年12月22日'));

VarToDateTime() の実パラメータに CheckEraDateString() を噛ますだけです。

令和 02 年 12 月 22 日

令和 02 / 12 / 22
令和 02 - 12 - 22
令和02年12月22日
令和02/12/22
令和02-12-22
令和 2 年 12 月 22 日
令和 2 / 12 / 22
令和 2 - 12 - 22
令和2年12月22日
令和2/12/22
令和2-12-22
令 02 年 12 月 22 日
令 02 / 12 / 22
令 02 - 12 - 22
令02年12月22日
令02/12/22
令02-12-22
令 2 年 12 月 22 日
令 2 / 12 / 22
令 2 - 12 - 22
令2年12月22日
令2/12/22
令2-12-22
R 02 年 12 月 22 日
R 02 / 12 / 22
R 02 - 12 - 22
R02年12月22日
R02/12/22
R02-12-22
R 2 年 12 月 22 日
R 2 / 12 / 22
R 2 - 12 - 22
R2年12月22日
R2/12/22
R2-12-22

上記のような文字列を変換できます。令和 対応の oleaut32.dll や locale.nls が降ってきたら、

  uses

..., System.SysUtils;

function CheckEraDateString(EraDateString: string): string;
begin
result := EraDateString;
end; { CheckEraDateString }

って書き換えればいいかと思います。


VarToDateTime() に渡す文字列を細工する (正規表現版)

正規表現を使った CheckEraDateString() 関数です。

uses

System.SysUtils, System.StrUtils, RegularExpressions;

function CheckEraDateString(EraDateString: string): string;
const
EXP = '(?<ERA>[^\d\s]*)\s*(?<YEAR>\d{1,4})\s*(/|-|年)\s*(?<MONTH>\d{1,2})\s*(/|-|月)\s*(?<DAY>\d{1,2})\s*日*';
ErasR: array [0..3] of string = ('令和', '令', 'Reiwa', 'R');
begin
result := EraDateString;
with TRegEx.Match(result, EXP) do
begin
if not Success then
Exit;
if AnsiIndexText(Groups.Item['ERA'].Value, ErasR) < 0 then
Exit;
CheckEraDateString := Format('平成%d年%d月%d日',
[Groups.Item['YEAR' ].Value.ToInteger + 30,
Groups.Item['MONTH'].Value.ToInteger,
Groups.Item['DAY' ].Value.ToInteger])
end;
end; { CheckEraDateString }

短くていいのですが、検証が大変そうです (w

See also:


FormatDateTime()

表示だけの問題だとは思いますが、

 s := FormatDateTime('ee/mm/dd', Dt);

みたいな処理があると、

'31/04/30' // <- 平成

'01/05/01' // <- 令和
'10/05/01' // <- 平成

そのうち判断付かなくなると思いますので早めの対応を行った方がいいと思います。ソートできませんしね

また、レジストリに頼らず元号を自前変換するアプリケーションでは FormatDateTime() の ggee を使ってはいけません。

「’令和’ee/mm/dd にして年を -30 すりゃいいのでは?」と思われたかもしれませんが、日付を誤魔化す事になりますのでダメです...理由を例で示します。

program EraTest;

{$APPTYPE CONSOLE}

uses
System.SysUtils;

var
s: string;
begin
// レジストリが対応しない場合、(平成) 31/05/01 と表示される
s := FormatDateTime('ee/mm/dd', EncodeDate(2019, 05, 01));
Writeln(s);

// 年から 30 引いて、見かけ上 (令和) 01/05/01 にする
s := FormatDateTime('ee/mm/dd', EncodeDate(2019 - 30, 05, 01));
Writeln(s);

Readln;
end.

実行結果は次の通りです。正しいように見えますね。

31/05/01 // (平成) 31/05/01

01/05/01 // (令和) 01/05/01

そこに颯爽と現れるのがうるう年です。

  // レジストリが対応しない場合、(平成) 32/02/29 と表示される

s := FormatDateTime('ee/mm/dd', EncodeDate(2020, 02, 29));
Writeln(s);

// 年から 30 引いて、見かけ上 (令和) 02/02/29 にする...?
s := FormatDateTime('ee/mm/dd', EncodeDate(2020 - 30, 02, 29));
Writeln(s);

令和2年2月29日 (平成32年2月29日) は存在しても、平成2年2月29日は存在しないのです。



VarToDateTime() の細工はあくまで同じ日を指しているので問題は発生しませんが、上記の処理は日付が異なる (30 年スライドさせている) ためにうるう年で問題が発生します。

※ FormatDateTime() や DateTimeToString() は Kernel32.dll の GetDateFormat() を利用しています。

See also:


StrToDate() / TFormatSettings

StrToDate() で TFormatSettings を指定すると和暦文字列 -> TDateTime 変換ができますが、例えば 令和01/05/01 のような日付文字列の変換に失敗すると、01 年が 2001 年と解釈され、2001/05/01 を返すので注意が必要です。

TFormatSettings は内部で EnumCalendarInfo() を呼んでおり、これは元号レジストリの影響を受けます。未来の日付の和暦であっても大丈夫です。

エンバカさんの記事 にあるように、和暦->西暦変換を行う場合には  を / で置換する必要があるのですが、それさえ行えば 元年 も変換します。元年レジストリには影響されません。

var

JPNEraFormat: TFormatSettings;
strDate: String;
timestamp: TDateTime;
begin
strDate := '令和元年5月1日';
JPNEraFormat := TFormatSettings.Create('ja-JP');
JPNEraFormat.ShortDateFormat := 'ggee/m/d';

strDate := StringReplace(strDate, '年', '/', []);
strDate := StringReplace(strDate, '月', '/', []);
strDate := StringReplace(strDate, '日', '', []);

timestamp := StrToDate(strDate,JPNEraFormat);
Writeln(strDate + '->' + FormatDateTime('yyyy/mm/dd', timestamp));

また、XE5 以降であれば TFormatSettings を使って元号とその開始年を列挙する事ができます。


EnumEras.pas

program EnumEras;

{$APPTYPE CONSOLE}

uses
SysUtils;

var
JpFormat: TFormatSettings;
EraInfo: TFormatSettings.TEraInfo;
begin
JpFormat := TFormatSettings.Create('ja-JP');
for EraInfo in JpFormat.EraInfo do
Writeln(Format('%s: %d', [EraInfo.EraName, EraInfo.EraOffset]));
end.


実行結果は次の通りです (新元号レジストリ適用済)。

See also:


Delphi で元号のレジストリキーを読む

こんな感じですかね。


EraTest.pas

program EraTest;

{$APPTYPE CONSOLE}

uses
System.Classes, System.StrUtils, System.SysUtils, System.Types,
System.Win.Registry, Winapi.Windows;

type
TEraRec =
record
EraName: string; // 長い元号 (日本語)
EraShortName: string; // 短い元号 (日本語)
EraEnName: string; // 長い元号 (英語)
EraEnShortName: string; // 短い元号 (英語)
StartDate: TDate; // 元号開始日付
end;

var
reg: TRegistry;
Value: string;
ValueNames: TStringList;
Era: TEraRec;
Eras: array of TEraRec;
i: Integer;
StrArr: TStringDynArray;

begin
reg := TRegistry.Create;
ValueNames := TstringList.Create;
try
reg.RootKey := HKEY_LOCAL_MACHINE;
if reg.OpenKeyReadOnly('SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese\Eras') then
begin
reg.GetValueNames(ValueNames);
ValueNames.Sort;
SetLength(Eras, ValueNames.Count);
for i := 0 to ValueNames.Count - 1 do
begin
Value := Reg.ReadString(ValueNames[i]);
with Eras[i] do
begin
StrArr := SplitString(Value, '_');
EraName := StrArr[0];
EraShortName := StrArr[1];
EraEnName := StrArr[2];
EraEnShortName := StrArr[3];
StrArr := SplitString(ValueNames[i], ' ');
StartDate := EncodeDate(StrArr[0].ToInteger, // 年
StrArr[1].ToInteger, // 月
StrArr[2].ToInteger); // 日
end;
end;
reg.CloseKey;
end;
finally;
ValueNames.Free;
reg.Free;
end;

// 出力
for Era in Eras do
begin
Write(Era.EraName);
Write(': ');
Writeln(FormatDateTime('YYYY/MM/DD', Era.StartDate));
end;
Readln;
end.


実行結果は次の通りです (新元号レジストリ適用済)。


元年

Windows 10 October 2018 Update (1809) 以前は新しい元号の最初の年は 1年 となりますが、Window 10 Insider Preview を適用していると 元年 がデフォルトになるようです。

※ Windows 10 の次のリリースででどちらがデフォルトになるのかは不明です。

元年の設定は [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese]InitialEraYear で切り替えられます。


InitialEraYear_1年ベース.reg

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese]
"InitialEraYear"="1年"


InitialEraYear_元年ベース.reg

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese]
"InitialEraYear"="元年"

但し、この設定は Delphi の FormatDateTime() や DateTimeToString() に影響を及ぼしません。しかしながら FormatDateTime() や DateTimeToString() 内で使われている GetDateFormat() API 自体は元年レジストリを参照しますので注意が必要です。


FirstEraYearTest.pas

program FirstEraYearTest;

uses
SysUtils, Windows;

var
st: TSystemTime;
Buffer: array [0..255] of Char;
FormatStr: string;
begin
DateTimeToSystemTime(EncodeDate(2019, 05, 01), st);
FormatStr := 'ggyy''年''MM''月''d''日''';
GetDateFormat(GetThreadLocale, DATE_USE_ALT_CALENDAR, @st, PChar(FormatStr), Buffer, Length(Buffer));

Writeln(StrPas(Buffer));
end.


実行結果は次の通りです。



何故同じ GetDateFormat() を使っておきながら FormatDateTime() や DateTimeToString() が元年レジストリの影響を受けないかというと、内部で元号と和暦年を別々に処理しているからです。

"ggy'年'M'月'd'日'"            -> 元号レジストリの影響を受ける

"gg" + "y" + "'年'M'月'd'日'" -> 元号レジストリの影響を受けない

日付書式文字列内に '年' が含まれる という条件を満たさないため元年表示されないという訳です。

See also:


合字

の令和版は Unicode のコードポイントで U+32FF です。フォントが対応しないと場所の確保だけになっちゃいますけれど。

合字は Shift_JIS (CP932 含む) ではサポートされません。


Microsoft Windows コード ページ 932 (MS932)、すなわちシフト JIS エンコーディングは、新元号の合字をサポートしません。Unicode の日本の新元号の合字 (漢字 1 文字) の文字をマルチ バイト文字に変換中に文字が正しく表示されないことがあります。MS932 エンコーディングでその逆を行う変換の場合も同様です。


ANSI 版 Delphi で元号の表示に合字を使っていたら厄介なことになります。このご時世に外字ファイルを作るのはちょっとヤですねぇ。

2019/04/26 追記:

4/26 リリースの修正モジュールにより、日本語フォントに合字の令和が追加されます。Windows 10 (1809) には修正モジュールが提供されていません。

2019/05/02 追記:

5/2 リリースの Windows 10 (1809) 用修正モジュールにより、日本語フォントに合字の令和が追加されます。



See also:


おわりに

1998 年に最初に作った和暦入力コンポーネントを 2019 年になって更新する事になろうとは思いませんでした (w


代替ルーチン

ver 0.90 にて、和暦操作ユニット EraUtils.pas を分離しました。次のルーチンの代替ルーチンが含まれます。


  • DateTimeToString() -> DateTimeToString_Era()

  • FormatDateTime() -> FormatDateTime_Era()

  • VarToDateTime() -> VarToDateTime_Era()

ver 1.00 にて DateTimeToString_Era() / FormatDateTime_Era() に元年対応パラメータを追加しました。IsFirstYearAsNumber を False に設定すると元年表示になります。

// 令和n年 -> 平成n+30年 変換

function CheckEraDateString(EraDateString: string): string;

// 日付から元号インデックスを得る
function DateToEraIndex(ADate: TDateTime): Integer;

// TDateTime から和暦へデコード
procedure DecodeEraDate(const DateTime: TDateTime; var EraIdx, Year, Month, Day: Word);

// 和暦から TDateTime へエンコード
function EncodeEraDate(EraIdx, Year, Month, Day: Word): TDateTime;

// DateTimeToString() 代替
procedure DateTimeToString_Era(var Result: string; const Format: string; DateTime: TDateTime; const IsFirstYearAsNumber: Boolean = True); overload;
procedure DateTimeToString_Era(var Result: string; const Format: string; DateTime: TDateTime; const FormatSettings: TFormatSettings; const IsFirstYearAsNumber: Boolean = True); overload;

// FormatDateTime() 代替
function FormatDateTime_Era(const Format: string; DateTime: TDateTime; const IsFirstYearAsNumber: Boolean = True): string; overload;
function FormatDateTime_Era(const Format: string; DateTime: TDateTime; const FormatSettings: TFormatSettings; const IsFirstYearAsNumber: Boolean = True): string; overload;

// VarToDateTime() 代替
function VarToDateTime_Era(const V: Variant): TDateTime;


  • Delphi 7 / 2007 / XE / 10.3 Rio で動作確認済です。

  • このコンポーネントは初出が古いので、(互換性のため) できる限り古い書き方で書いていますが、Delphi 7 よりも前の環境での動作は保証しません。

  • 和暦入力コンポーネントをインストールせず、EraUtils.pas 単独で使う事もできます。