Delphi
Pascal
embarcadero
objectpascal
新元号


はじめに

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

See also:


Delphi での新元号対応

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


エンバカさんの記事

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


レジストリファイル

こんなもんですよね。


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 だと自前処理なんですかね?

image.png

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

image.png

See also:

追記 2019/04/11

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

OS
KB

Windows 10
KB4493509

Windows 8.1
KB4493446

Windows 7
KB4493472

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


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日は存在しないのです。

image.png

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.


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

image.png

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.


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

image.png


元年

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.


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

image.png

何故同じ 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 で元号の表示に合字を使っていたら厄介なことになります。このご時世に外字ファイルを作るのはちょっとヤですねぇ。

See also:


おわりに

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

image.png


代替ルーチン

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 単独で使う事もできます。