旧暦から新暦への変換プログラム

# はじめに
この記事では、以下のソースコードを実装します。

* 旧暦から新暦への変換プログラム
* 和暦から新暦への変換プログラム

# .NET APIの利用
`.NET API`には、[JapaneseCalendar](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.japanesecalendar?view=netcore-3.1)クラスや[JapaneseLunisolarCalendar](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.japaneselunisolarcalendar?view=netcore-3.1)クラスが用意されており、これらを利用します。

# D言語から.NET API呼び出しの実現方法
私が調べた限り、`C++/CLIラッピング`を経由する方法が簡単だと思いました。
今回はこの方法を採用します。

※参考までに、記事を書くために調べた情報リンクを最後に掲載します。
　`C#`でDLLを作成し、**D言語**から呼び出す方法です。

# 前準備１（C#での実装）

**D言語**で実装する前に、`C#`で旧暦や和暦を取得する処理を実装しました。
実行時にパラメータなしだと今日の日付の旧暦や和暦、年月日パラメータを指定するとその日付の旧暦や和暦を表示します。

```c#:JCalendar.cs
using System;
using System.Globalization;

public class JCalendar {
	static string[] sEra    = // 元号
		{ "", "明治", "大正", "昭和", "平成", "令和" };
	static string[] sRokuyo = // 六曜
		{ "大安", "赤口", "先勝", "友引", "先負", "仏滅" };
	static string[] sKanshi = // 天干
		{ "", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" };
	static string[] sChishi = // 地支
		{ "", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" };
	
	public static void Main(string[] args)
	{
		DateTime newDate;
		if ( args.Length >= 3 ){
			newDate = new DateTime(
				int.Parse(args[0]), int.Parse(args[1]), int.Parse(args[2]));
		} else {
			newDate = DateTime.Now;
		}
		Console.WriteLine("西暦 : {0:d}", newDate);
		if ( newDate >= new DateTime(1868, 9, 8) ){
			printJapaneseCalendar(newDate);
		}
		if ( newDate >= new DateTime(1960, 1, 28)
		  && newDate <= new DateTime(2050, 1, 22) ){
			printJapaneseLunisolarCalendar(newDate);
		}
	}
	
	static void printJapaneseCalendar(DateTime newDate)
	{
		JapaneseCalendar jc = new JapaneseCalendar();
		int era   = jc.GetEra(newDate);
		int year  = jc.GetYear(newDate);
		int month = jc.GetMonth(newDate);
		int day   = jc.GetDayOfMonth(newDate);
		DateTime jDate  = new DateTime(year, month, day);
		Console.WriteLine("和暦 : {0} {1:d}", sEra[era], jDate);
	}
	
	static void printJapaneseLunisolarCalendar(DateTime newDate)
	{
		JapaneseLunisolarCalendar jlc = new JapaneseLunisolarCalendar();
		int era   = jlc.GetEra(newDate);
		int year  = jlc.GetYear(newDate);
		int month = jlc.GetMonth(newDate);
		int day   = jlc.GetDayOfMonth(newDate);
		
		//閏月を取得
		string sLeap = "";
		if ( year > 0 ){
			int leapMonth = jlc.GetLeapMonth(year, era);
			if ( month == leapMonth ){
				sLeap = "（閏月）";
			}
			//閏月含む場合の月を補正
			if ( (leapMonth > 0) && (month >= leapMonth) ){
				month = month - 1;	//旧暦月の補正
			}
		}
		// 干支（天干、地支）
		int sy = jlc.GetSexagenaryYear(newDate);
		int tk = jlc.GetCelestialStem(sy);
		int ts = jlc.GetTerrestrialBranch(sy);
		
		// 六曜(大安・赤口・先勝・友引・先負・仏滅)
		// (月 + 日) % 6 の余り
		int rokuyo = (month + day) % 6;
		
		Console.WriteLine("旧暦 : {0} {1:d4}/{2:d2}/{3:d2} {4}",
			sEra[era], year, month, day, sLeap);
		Console.WriteLine("干支 : {0}{1}", sKanshi[tk], sChishi[ts]);
		Console.WriteLine("六曜 : {0}", sRokuyo[rokuyo]);
	}
}
```
`Window10`には、`C#コンパイラ(csc.exe)`が標準インストールされています。
どのバージョンがどのフォルダにインストールされているかは、それぞれの環境に依存します。
私の環境でのコンパイル、実行例を例示します。
旧暦には、`うるう月（閏月）`というものが存在します。`GetLeapMonth`を使って、その年の`うるう月`を算出し、月を補正します。
実行結果では、月を補正して正しく表示できています。※[旧暦カレンダー](http://www.ajnet.ne.jp/diary/)で検証
干支（天干、地支）は、その年の干支を取得しています。月や日にも干支が存在するそうです。

```text:実行結果
d:\Dev>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe JCalendar.cs
Microsoft (R) Visual C# Compiler version 4.8.3752.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240

d:\Dev>JCalendar
西暦 : 2020/09/26
和暦 : 令和 0002/09/26
旧暦 : 令和 0002/08/10
干支 : 庚子
六曜 : 大安

d:\Dev>JCalendar 2020 5 23
西暦 : 2020/05/23
和暦 : 令和 0002/05/23
旧暦 : 令和 0002/04/01 （閏月）
干支 : 庚子
六曜 : 仏滅
```

# 前準備２（C++/CLIでの実装）
次に、`C++/CLI`で旧暦や和暦を取得する処理を実装しました。
`C++/CLI`に関する日本語での情報は、それほど多くない印象なので、参考になればと思います。
前準備１の`C#`のソースコードを移植したイメージです。
実行時にパラメータなしだと今日の日付の旧暦や和暦、年月日パラメータを指定するとその日付の旧暦や和暦を表示します。

`C++/CLI`特有のものとして、`演算子 (^) `があります。

>ハンドル宣言子( ^ は "hat") は、オブジェクトがアクセス不可能であるとシステムが判断したときに、宣言されたオブジェクトが自動的に削除されることを意味する型指定子をに変更します。

[オブジェクト演算子 (^) へのハンドル (C++/CLI および C++/CX)](https://docs.microsoft.com/ja-jp/cpp/extensions/handle-to-object-operator-hat-cpp-component-extensions?view=vs-2019)より引用

また、`.NET API`のクラス生成では、`gcnew`を使います。

>マネージド型 (参照型または値型) のメモリは gcnew によって割り当てられ、ガベージ コレクションによって解放されます。

[ref new、gcnew (C++/CLI および C++/CX)](https://docs.microsoft.com/ja-jp/cpp/extensions/ref-new-gcnew-cpp-component-extensions?view=vs-2019)より引用

```c++:JCal.cpp
using namespace System;
using namespace System::Globalization;

#include <string>

String^ getEraStr(int era)
{	// 元号
	array<String^>^ sEra = gcnew array<String^>
		{ "", "明治", "大正", "昭和", "平成", "令和" };
	return ( sEra[era] );
}

String^ getRokuyoStr(int rokuyo)
{	// 六曜
	array<String^>^ sRokuyo =
		{ "大安", "赤口", "先勝", "友引", "先負", "仏滅" };
	return ( sRokuyo[rokuyo] );
}

String^ getKanshiStr(int kanshi)
{	// 天干
	array<String^>^ sKanshi =
		{ "", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" };
	return ( sKanshi[kanshi] );
}

String^ getChishiStr(int chishi)
{	// 地支
	array<String^>^sChishi =
		{ "", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" };
	return ( sChishi[chishi] );
}

void printJapaneseCalendar(DateTime newDate)
{
	JapaneseCalendar^ jc = gcnew JapaneseCalendar();
	int era   = jc->GetEra(newDate);
	int year  = jc->GetYear(newDate);
	int month = jc->GetMonth(newDate);
	int day   = jc->GetDayOfMonth(newDate);
	DateTime jDate  = DateTime::DateTime(year, month, day);
	Console::WriteLine("和暦 : {0} {1:d}", getEraStr(era), jDate);
}

void printJapaneseLunisolarCalendar(DateTime newDate)
{
	JapaneseLunisolarCalendar^ jlc = gcnew JapaneseLunisolarCalendar();
	int era   = jlc->GetEra(newDate);
	int year  = jlc->GetYear(newDate);
	int month = jlc->GetMonth(newDate);
	int day   = jlc->GetDayOfMonth(newDate);
	
	//閏月を取得
	String^ sLeap = "";
	if ( year > 0 ){
		int leapMonth = jlc->GetLeapMonth(year, era);
		if ( month == leapMonth ){
			sLeap = "（閏月）";
		}
		//閏月含む場合の月を補正
		if ( (leapMonth > 0) && (month >= leapMonth) ){
			month = month - 1;	//旧暦月の補正
		}
	}
	// 干支（天干、地支）
	int sy = jlc->GetSexagenaryYear(newDate);
	int tk = jlc->GetCelestialStem(sy);
	int ts = jlc->GetTerrestrialBranch(sy);
	
	// 六曜(大安・赤口・先勝・友引・先負・仏滅)
	// (月 + 日) % 6 の余り
	int rokuyo = (month + day) % 6;
	
	Console::WriteLine("旧暦 : {0} {1:d4}/{2:d2}/{3:d2} {4}",
		getEraStr(era), year, month, day, sLeap);
	Console::WriteLine("干支 : {0}{1}", getKanshiStr(tk), getChishiStr(ts));
	Console::WriteLine("六曜 : {0}", getRokuyoStr(rokuyo));
}

int main(int argc, char* argv[])
{
	DateTime newDate = DateTime::Now;
	if ( argc > 3 ){
		newDate = DateTime::DateTime(
			atoi(argv[1]), atoi(argv[2]), atoi(argv[3]));
	}
	Console::WriteLine("西暦 : {0:d}", newDate);
	if ( newDate >= DateTime::DateTime(1868, 9, 8) ){
		printJapaneseCalendar(newDate);
	}
	if ( newDate >= DateTime::DateTime(1960, 1, 28)
	  && newDate <= DateTime::DateTime(2050, 1, 22) ){
		printJapaneseLunisolarCalendar(newDate);
	}
}
```
私の環境には、[Visual C++ ビルドツール 2019](https://visualstudio.microsoft.com/ja/downloads/)がインストールされています。
※参考：[Visual C++ ビルドツール 2019 インストール手順](https://www.kkaneko.jp/tools/win/buildtool.html)

[VS2019用 x64 Native Tools コマンドプロンプト](https://docs.microsoft.com/ja-jp/cpp/build/building-on-the-command-line?view=vs-2019)を起動します。
[共通言語ランタイム (CLR)](https://docs.microsoft.com/ja-jp/dotnet/standard/clr) 機能を使用するために、コンパイルオプション`/clr`をつけてコンパイルします。

旧暦としては、令和は3月27日から始まるようです。※[旧暦カレンダー](http://www.ajnet.ne.jp/diary/)で検証

```text:実行結果
d:\Dev>cl /clr JCal.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 19.24.28314
Microsoft (R) .NET Framework の場合 バージョン 4.08.4220.0
Copyright (C) Microsoft Corporation.  All rights reserved.

JCal.cpp
Microsoft (R) Incremental Linker Version 14.24.28314.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:JCal.exe
JCal.obj

d:\Dev>JCal.exe 2019 4 30
西暦 : 2019/04/30
和暦 : 平成 0031/04/30
旧暦 : 平成 0031/03/26
干支 : 己亥
六曜 : 仏滅

d:\Dev>JCal.exe 2019 5 1
西暦 : 2019/05/01
和暦 : 令和 0001/05/01
旧暦 : 令和 0001/03/27
干支 : 己亥
六曜 : 大安
```

# D言語、C++/CLIラッピングのソースコード
前準備が長くなりましたが、ここからが本題です。
`.NET API`を呼び出すための`C++/CLI`ラッピング処理の実装例です。
旧暦、和暦の取得結果を`struct JCAL`にセットするシンプルな関数です。
**D言語**から呼び出せるように、`extern "C" __declspec(dllexport)`属性を付与しています。

```c++:JCalDll.cpp
using namespace System;
using namespace System::Globalization;

typedef struct {
	int era;
	int year;
	int month;
	int day;
	int leapMonth;
	int zodiac;
	int kanshi;
	int chishi;
	int rokuyo;
} JCAL;

#define VC_DLL_EXPORTS extern "C" __declspec(dllexport)

VC_DLL_EXPORTS void __cdecl getJapaneseCalendar(int year, int month, int day, JCAL &jcal)
{
	DateTime newDate = DateTime::DateTime(year, month, day);
	JapaneseCalendar^ jc = gcnew JapaneseCalendar();
	jcal.era   = jc->GetEra(newDate);
	jcal.year  = jc->GetYear(newDate);
	jcal.month = jc->GetMonth(newDate);
	jcal.day   = jc->GetDayOfMonth(newDate);
}

VC_DLL_EXPORTS void __cdecl getJapaneseLunisolarCalendar(int year, int month, int day, JCAL &jcal)
{
	DateTime newDate = DateTime::DateTime(year, month, day);
	JapaneseLunisolarCalendar^ jlc = gcnew JapaneseLunisolarCalendar();
	jcal.era   = jlc->GetEra(newDate);
	jcal.year  = jlc->GetYear(newDate);
	jcal.month = jlc->GetMonth(newDate);
	jcal.day   = jlc->GetDayOfMonth(newDate);
	jcal.leapMonth = 0;
	//閏月を取得
	if ( jcal.year > 0 ){
		int leapMonth = jlc->GetLeapMonth(jcal.year, jcal.era);
		if ( jcal.month == leapMonth ){
			jcal.leapMonth = 1;
		}
		//閏月含む場合の月を補正
		if ( (leapMonth > 0) && (jcal.month >= leapMonth) ){
			jcal.month = jcal.month - 1;	//旧暦月の補正
		}
	}
	// 干支（天干、地支）
	jcal.zodiac = jlc->GetSexagenaryYear(newDate);
	jcal.kanshi = jlc->GetCelestialStem(jcal.zodiac);
	jcal.chishi = jlc->GetTerrestrialBranch(jcal.zodiac);
	
	// 六曜(大安・赤口・先勝・友引・先負・仏滅)
	// (月 + 日) % 6 の余り
	jcal.rokuyo = (jcal.month + jcal.day) % 6;
}
```

次に**D言語**で`C++/CLI`ラッピングを呼び出し、旧暦、和暦を表示するプログラムの実装例です。
取得したい情報をstructで定義、メモリを確保し`C++/CLI`に渡しています。
`pragma(lib, "JCalDll")`と`extern (Windows)～`を書くだけで、`C++/CLI`側の関数を呼び出せるので、思っていたより簡単に実装できました。

```d:oldcal.d
import std.algorithm;
import std.conv;
import std.datetime;
import std.format;
import std.range;
import std.stdio;
import core.sys.windows.windows;

struct JCAL {
	int era;
	int year;
	int month;
	int day;
	int leapMonth;
	int zodiac;
	int kanshi;
	int chishi;
	int rokuyo;
}

pragma(lib, "JCalDll");

extern (Windows) nothrow @nogc {
	void getJapaneseCalendar(int, int, int, ref JCAL);
	void getJapaneseLunisolarCalendar(int, int, int, ref JCAL);
}

string[] sEra =
	[ "", "明治", "大正", "昭和", "平成", "令和" ];
string[] sKanshi =
	[ "", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" ];
string[] sChishi =
	[ "", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" ];
string[] sRokuyo =
	[ "大", "赤", "勝", "友", "負", "仏" ];
//	[ "大安", "赤口", "先勝", "友引", "先負", "仏滅" ];

void main(string[] args)
{
	Date dt = Clock.currTime().to!Date;
	if ( args.length > 2 ){
		dt = Date(args[1].to!int, args[2].to!int, 1);
	} else {
		dt = Date(dt.year, dt.month, 1);
	}
	writef("\n%4d年 %2d月", dt.year, dt.month);
	JCAL jcal;
	string line1 = "  　  |".cycle.take(dt.dayOfWeek * 6).to!string;
	string line2 = "      |".cycle.take(dt.dayOfWeek * 7).to!string;
	with ( jcal ){
		getJapaneseCalendar(dt.year, dt.month, 1, jcal);
		writef("(%s %2d年) ", sEra[era], year);
		getJapaneseLunisolarCalendar(dt.year, dt.month, 1, jcal);
		writefln("[%s%s]", sKanshi[kanshi], sChishi[chishi]);
		writefln("%-(  %s  |%)", ["日", "月", "火", "水", "木", "金", "土" ]);
		for ( int d = 1; d <= dt.daysInMonth; d++ ){
			getJapaneseLunisolarCalendar(dt.year, dt.month, d, jcal);
			line1 ~= format("%2d %s |", d, sRokuyo[rokuyo]);
			line2 ~= format("%2d/%2d%s|", month, day,(leapMonth == 1) ? "*" : " ");
		}
	}
	int num = (cast(int)line2.length / (7 * 7)) + 1;
	string line0 = "-".cycle.take(num * 7 * 7).to!string;
	roundRobin(line0.chunks(7 * 7), line1.chunks(6 * 7), line2.chunks(7 * 7))
		.each!(s => writefln("%s", s));
}
```
# D言語ソースコードの補足説明
実行時にパラメータなしだと今日の年月で処理します。`args`パラメータで年月を指定することも可能です。

`string line1`には、全角文字が各日付ごとに1文字含まれます。このため1日の表示幅が`6`文字となります。
新暦の日付で2文字、半角スペース1文字、六曜表示または全角スペースで1文字、半角スペース1文字、`|`の1文字の順で合計`6`文字です。

`string line2`はすべて半角で、1日の表示幅が`7`文字となります。
旧暦の年月で5文字、半角スペースまたはうるう月は`*`で1文字、`|`の1文字の順で合計`7`文字です。

`string line0`は、横線`-`です。

`line0 line1 line2`を7日ごとに区切って改行するために`chunks`、交互に表示するために`roundRobin`を使います。
[chunks](https://dlang.org/phobos/std_range.html#chunks)
[roundRobin](https://dlang.org/phobos/std_range.html#roundRobin)

# コンパイル
[VS2019用 x64 Native Tools コマンドプロンプト](https://docs.microsoft.com/ja-jp/cpp/build/building-on-the-command-line?view=vs-2019)を起動します。
`C++/CLI`コンパイル時に、オプション`/clr`と`/LD`を付けます。`JCalDll.lib`と`JCalDll.dll`が生成されます。
`JCalDll.lib`は、D言語ソースのコンパイル時に必要です。`JCalDll.dll`は、`oldcal.exe`実行時に必要です。

`dmd`でのコンパイル時は、64bitコード生成のためにオプション`-m64`をつけます。
`ldc2`をインストールしていれば、`ldc2 oldcal.d`でもOKです。

```text:コンパイル
d:\Dev>cl /clr /LD JCalDll.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 19.24.28314
Microsoft (R) .NET Framework の場合 バージョン 4.08.4220.0
Copyright (C) Microsoft Corporation.  All rights reserved.

JCalDll.cpp
Microsoft (R) Incremental Linker Version 14.24.28314.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:JCalDll.dll
/dll
/implib:JCalDll.lib
JCalDll.obj
   ライブラリ JCalDll.lib とオブジェクト JCalDll.exp を作成中

d:\Dev>dmd -m64 oldcal.d
```

# 実行結果
表示される日本語の文字コードは`UTF-8`なので、先に`chcp 65001`を実行してください。
`Shift-JIS`で日本語出力したい場合は、[文字コード変換処理](https://qiita.com/devmynote/items/9b6a4ebda4217df9e8a2)をソースコードに加える必要があります。

私の環境のコマンドプロンプトでは、[Cicaフォント](https://github.com/miiton/Cica)を使っています。
このため、実行結果では全角スペースが四角の枠で表示されています。

これで、自分の誕生月の旧暦カレンダーも見られそうです。
ただし、[JapaneseLunisolarCalendar](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.japaneselunisolarcalendar?view=netcore-3.1)クラスの`GetLeapMonth`の仕様で、西暦1960年1月27（旧暦の1959年）以前のうるう月を取得しようとすると`Exception`が発生するため、西暦1960年2月以降の旧暦カレンダーしか表示できません。上限は西暦2049年12月です。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/21c6aeab-2328-73df-1e24-5a653e084072.png)

# 今回使用した参考情報

[JapaneseCalendarクラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.japanesecalendar?view=netcore-3.1)
[JapaneseLunisolarCalendarクラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.japaneselunisolarcalendar?view=netcore-3.1)
[オブジェクト演算子 (^) へのハンドル (C++/CLI および C++/CX)](https://docs.microsoft.com/ja-jp/cpp/extensions/handle-to-object-operator-hat-cpp-component-extensions?view=vs-2019)
[ref new、gcnew (C++/CLI および C++/CX)](https://docs.microsoft.com/ja-jp/cpp/extensions/ref-new-gcnew-cpp-component-extensions?view=vs-2019)

[C#のメソッドをC++から呼ぶ方法](https://qiita.com/tetsurom/items/a0ad9bd24dbe513afdc4)
[C#をC++/CLIでラップしてC+＋アプリから呼ぶ](https://qiita.com/tera1707/items/69bdfe99733b84c97217)

[六曜・月齢・旧暦カレンダー](http://www.ajnet.ne.jp/diary/)
[年・月・日の干支](http://koyomi.vis.ne.jp/reki_doc/doc_0020.htm)
[六曜はどのように決まるのか？六曜の計算方法は？](https://nj-clucker.com/calculate-rokuyo/)
[c#で六曜の計算](https://qiita.com/yo-i/items/21650243f4e08314afd3)

# 今回使用していない情報だけど参考になりそうな情報

[C++/CLIラッピング入門](https://qiita.com/Convert314/items/0171811eabd69042e540)
[C++のモジュールからC#のDLLを呼び出してみる](https://knowledge.rinpress.com/index.php/%EF%BC%A3%EF%BC%8B%EF%BC%8B%E3%81%AE%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%8B%E3%82%89%EF%BC%A3%EF%BC%83%E3%81%AE%EF%BC%A4%EF%BC%AC%EF%BC%AC%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)
[C++からC# DLLを直接利用する方法](https://qiita.com/Midoliy/items/58d56e202f104ebf867a)
[Calling-NET-from-D](https://github.com/taylorh140/Calling-NET-from-D)