14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Delphi での新元号対応

Last updated at Posted at 2019-04-01

はじめに

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

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 だと自前処理なんですかね?
image.png
XP / Vista いずれにも [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars] 以下がありません。
image.png

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

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

2019/05/02 追記:
5/2 リリースの Windows 10 (1809) 用修正モジュールにより、日本語フォントに合字の令和が追加されます。
image.png
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 単独で使う事もできます。
14
7
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?