D言語で文字コード変換(SJIS, EUC, JIS, EBCDIC, IBM漢字)

# はじめに

**D言語**で、各種文字コード（SJIS, EUC, JIS, EBCDIC, IBM漢字）への変換処理の実例を示したいと思います。
※記事で取り扱っていない文字コードについても、変換処理を実装する際の参考になればと思います。

# 環境

* Windows 10（WindowsのAPIを使用しているため）
* [dmd](https://dlang.org/download.html) または [ldc](https://github.com/ldc-developers/ldc/releases)

# Shift_JIS(SJIS) <---> UTF8
`toMBSz`、`fromMBSz`使って、簡単に変換できます。[Shift_JIS 文字コード表](https://seiai.ed.jp/sys/text/java/shiftjis_table.html)

```d:codeconv.d（抜粋）
string toSJIS(string s)
{	// UTF8 → SJIS
	return ( s.toMBSz.to!string );
}

string fromSJIS(string s)
{	// SJIS → UTF8
	return ( s.toStringz.fromMBSz );
}
```

# EUC <---> UTF8
`toMBSz`、`fromMBSz`の引数にコードページを設定することで、変換できます。
コードページの対応表は、[Code Page Identifiers](https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers)にあります。`20932`は`EUC-JP`を意味しています。[日本語EUC 文字コード表](http://charset.7jp.net/euc.html)

```d:codeconv.d（抜粋）
string toEUC(string s)
{	// UTF8 → EUC(20932)
	return ( s.toMBSz(20932).to!string );
}

string fromEUC(string s)
{	// EUC(20932) → UTF8
	return ( s.toStringz.fromMBSz(20932) );
}
```

# JIS <---> UTF8
`JIS`から`UTF8`に変換する場合、`EUC`のように`fromMBSz`を使うだけでは変換できません。
なぜかというと、`fromMBSz`では文字コードが`0x80`以上の場合にのみ変換処理を行っています。
`JIS`の漢字コードは`0x80`未満で構成されるため、変換対象になりません。[JIS漢字コード表(JIS X 0208)](http://www.infonet.co.jp/ueyama/ip/binary/x0208txt.html)

```d:std.windows.charset.d（抜粋）
string fromMBSz(return scope immutable(char)* s, int codePage = 0)
{
    const(char)* c;

    for (c = s; *c != 0; c++)
    {
        if (*c >= 0x80)
        {
            wchar[] result;
            int readLen;

            result.length = MultiByteToWideChar(codePage, 0, s, -1, null, 0);

            if (result.length)
            {
                readLen = MultiByteToWideChar(codePage, 0, s, -1, result.ptr,
                        to!int(result.length));
            }

            if (!readLen || readLen != result.length)
            {
                throw new Exception("Couldn't convert string: " ~
                    sysErrorString(GetLastError()));
            }

            return result[0 .. result.length-1].to!string; // omit trailing null
        }
    }
    return s[0 .. c-s];         // string is ASCII, no conversion necessary
}

```
そのため、`fromCodePage`のような処理を実装する必要があります。
[MultiByteToWideChar](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-multibytetowidechar)は、Windowsが提供しているAPIです。**D言語は、Windows APIも簡単に呼び出せます。**

```d:codeconv.d（抜粋）
string toJIS(string s)
{	// UTF8 → JIS(50220)
	return ( s.toMBSz(50220).to!string );
}

string fromJIS(string s)
{	// JIS(50220) → UTF8
	return ( s.fromCodePage(50220) );
}

string fromCodePage(string s, int codePage)
{	// (codePage) → UTF8
	wstring ret;
	int len = MultiByteToWideChar(codePage, 0, s.ptr, cast(int)s.length, null, 0);
	if ( len > 0 ){
		ret.length = len;
		MultiByteToWideChar(codePage, 0, s.ptr, cast(int)s.length, cast(wchar*)ret.ptr, len);
	}
	return ( ret.to!string );
}
```

# EBCDIC <---> UTF8
`toMBSz`でも文字コードが`0x80`以上かの判定があります。
このため、`UTF8`から`EBCDIC`に変換する際は、`toCodePage`のような処理を実装する必要があります。
[WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte)もWindows APIです。
`EBCDIC`から`UTF8`に変換する際は、JISと同じく`fromCodePage`を使います。
`20290`は`IBM290`とも呼ばれ、`CCSID 5026`と同じ変換結果になりました。[EBCDIC(CCSID 5026)コード表](https://www.imagazine.co.jp/imagazine-7279/)

```d:codeconv.d（抜粋）
string toEBCDIC(string s)
{	// UTF8 → EBCDIC(20290)
	return ( s.toCodePage(20290) );
}

string fromEBCDIC(string s)
{	// EBCDIC(20290) → UTF8
	return ( s.fromCodePage(20290) );
}

string toCodePage(string s, int codePage)
{	// UTF8 → (codePage)
	char[] ret;
	wstring sw = s.to!wstring;
	int len = WideCharToMultiByte(codePage, 0, sw.ptr, cast(int)sw.length, null, 0, null, null);
	if ( len > 0 ){
		ret.length = len;
		WideCharToMultiByte(codePage, 0, sw.ptr, cast(int)sw.length, ret.ptr, len, null, null);
	}
	return ( ret.to!string );
}
```

# IBM漢字(CP930) <---> UTF8
IBM漢字(CP930)は、[MultiByteToWideChar](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-multibytetowidechar)や[WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte)が未対応のため、コード変換できませんでした。
このため、変換テーブルを作成してコード変換処理を行うよう実装しました。
変換テーブル（[cp930.txt](https://github.com/devmynote/PartsFactory/blob/main/charcodeconv/cp930.txt)）は、[こちら](https://qiita.com/madilloar/items/12b9e270388c92d0482d)の情報を元に作成させていただきました。

```d:codeconv.d（抜粋）
static class CP930TBL {
	static private wstring CP930toUTF8 = import("cp930.txt");
	static private uint[wchar] UTF8toCP930;
	
	static this() {
		UTF8toCP930 = initConvTbl();
	}
	
	static private uint[wchar] initConvTbl()
	{
		uint[wchar] cp930tbl;
		foreach ( hi, line; CP930toUTF8.split("\n") ){
			foreach ( lo, c; line ){
				cp930tbl[c] = cast(uint)((hi << 8) + lo + 0x4040);
			}
		}
		cp930tbl['　'] = 0x4040;
		return ( cp930tbl );
	}
	
	static string to(string s)
	{	// UTF8 → CP930
		char[] ret;
		foreach ( cw ; s.to!wstring ){
			auto c1 = UTF8toCP930[cw];
			ret ~= [cast(char)(c1 >> 8), cast(char)(c1 & 0xff)];
		}
		return ( ret.to!string );
	}
	
	static string from(string s)
	{	// CP930 → UTF8
		wchar[] ret;
		for ( int i = 0; i < s.length ; i += 2 ){
			int hi = s[i  ] - 0x40;
			int lo = s[i+1] - 0x40;
			ret ~= CP930toUTF8[hi * (0xff - 0x3F + 1) + lo];
		}
		return ( ret.to!string );
	}
}
alias toCP930   = CP930TBL.to;
alias fromCP930 = CP930TBL.from;
```

# ソースコード実装例
これまでの処理をまとめた実装例です。蛇足ですが、ソースコード自体の文字コードは`UTF-8`です。

```d:codeconv.d
import std.conv;
import std.stdio;
import std.string;
import std.windows.charset : fromMBSz, toMBSz;
import core.sys.windows.winnls : MultiByteToWideChar, WideCharToMultiByte;

string toSJIS(string s)
{	// UTF8 → SJIS
	return ( s.toMBSz.to!string );
}

string fromSJIS(string s)
{	// SJIS → UTF8
	return ( s.toStringz.fromMBSz );
}

string toCodePage(string s, int codePage)
{	// UTF8 → (codePage)
	char[] ret;
	wstring sw = s.to!wstring;
	int len = WideCharToMultiByte(codePage, 0, sw.ptr, cast(int)sw.length, null, 0, null, null);
	if ( len > 0 ){
		ret.length = len;
		WideCharToMultiByte(codePage, 0, sw.ptr, cast(int)sw.length, ret.ptr, len, null, null);
	}
	return ( ret.to!string );
}

string fromCodePage(string s, int codePage)
{	// (codePage) → UTF8
	wstring ret;
	int len = MultiByteToWideChar(codePage, 0, s.ptr, cast(int)s.length, null, 0);
	if ( len > 0 ){
		ret.length = len;
		MultiByteToWideChar(codePage, 0, s.ptr, cast(int)s.length, cast(wchar*)ret.ptr, len);
	}
	return ( ret.to!string );
}

string toJIS(string s)
{	// UTF8 → JIS(50220)
	return ( s.toMBSz(50220).to!string );
}

string fromJIS(string s)
{	// JIS(50220) → UTF8
	return ( s.fromCodePage(50220) );
}

string toEUC(string s)
{	// UTF8 → EUC(20932)
	return ( s.toMBSz(20932).to!string );
}

string fromEUC(string s)
{	// EUC(20932) → UTF8
	return ( s.toStringz.fromMBSz(20932) );
}

string toEBCDIC(string s)
{	// UTF8 → EBCDIC(20290)
	return ( s.toCodePage(20290) );
}

string fromEBCDIC(string s)
{	// EBCDIC(20290) → UTF8
	return ( s.fromCodePage(20290) );
}

static class CP930TBL {
	static private wstring CP930toUTF8 = import("cp930.txt");
	static private uint[wchar] UTF8toCP930;
	
	static this() {
		UTF8toCP930 = initConvTbl();
	}
	
	static private uint[wchar] initConvTbl()
	{
		uint[wchar] cp930tbl;
		foreach ( hi, line; CP930toUTF8.split("\n") ){
			foreach ( lo, c; line ){
				cp930tbl[c] = cast(uint)((hi << 8) + lo + 0x4040);
			}
		}
		cp930tbl['　'] = 0x4040;
		return ( cp930tbl );
	}
	
	static string to(string s)
	{	// UTF8 → CP930
		char[] ret;
		foreach ( cw ; s.to!wstring ){
			auto c1 = UTF8toCP930[cw];
			ret ~= [cast(char)(c1 >> 8), cast(char)(c1 & 0xff)];
		}
		return ( ret.to!string );
	}
	
	static string from(string s)
	{	// CP930 → UTF8
		wchar[] ret;
		for ( int i = 0; i < s.length ; i += 2 ){
			int hi = s[i  ] - 0x40;
			int lo = s[i+1] - 0x40;
			ret ~= CP930toUTF8[hi * (0xff - 0x3F + 1) + lo];
		}
		return ( ret.to!string );
	}
}
alias toCP930   = CP930TBL.to;
alias fromCP930 = CP930TBL.from;

void writeHex(string s)
{	// 文字列の16進数表示
	foreach ( c ; s ){
		writef("%x ", c);
	}
	writeln();
}

void main()
{
	string s = "あか青　AZaz ";
	writeHex(s);
	
	string s2 = s.toSJIS;
	writeHex(s2);
	writeHex(s2.fromSJIS);
	
	s2 = s.toJIS;
	writeHex(s2);
	writeHex(s2.fromJIS);
	
	s2 = s.toEUC;
	writeHex(s2);
	writeHex(s2.fromEUC);
	
	s2 = "AZaz19ｱﾝ<> ".toEBCDIC;
	writeHex(s2);
	writeHex(s2.fromEBCDIC);
	
	s2 = "一９鸙煕　".toCP930;
	writeHex(s2);
	writeHex(s2.fromCP930);
}
```

# コンパイル手順
`codeconv.d`と同じフォルダに`cp930.txt`を配置してコンパイルします。
`cp930.txt`は、[こちら](https://github.com/devmynote/PartsFactory/blob/main/charcodeconv/cp930.txt)に保存しています。

```text:コンパイル実行例
dmd -m64 -J=. codeconv.d
または
ldc2 -J=. codeconv.d
```

# exeファイル実行例
変換前後のコードを16進数で出力する例となります。

```text:exe実行例
D:\Dev> codeconv
e3 81 82 e3 81 8b e9 9d 92 e3 80 80 41 5a 61 7a 20
82 a0 82 a9 90 c2 81 40 41 5a 61 7a 20
e3 81 82 e3 81 8b e9 9d 92 e3 80 80 41 5a 61 7a 20
1b 24 42 24 22 24 2b 40 44 21 21 1b 28 42 41 5a 61 7a 20
e3 81 82 e3 81 8b e9 9d 92 e3 80 80 41 5a 61 7a 20
a4 a2 a4 ab c0 c4 a1 a1 41 5a 61 7a 20
e3 81 82 e3 81 8b e9 9d 92 e3 80 80 41 5a 61 7a 20
c1 e9 62 b9 f1 f9 81 bd 4c 6e 40
41 5a 61 7a 31 39 ef bd b1 ef be 9d 3c 3e 20
45 41 42 f9 67 fe 68 85 40 40
e4 b8 80 ef bc 99 e9 b8 99 e7 85 95 e3 80 80
```

# 応用例
## fromXXXとtoXXXを組み合わせる
例えば、`Shift_JIS`から`JIS`に変換したい場合は、`fromSJIS`と`toJIS`を組み合わせて実装できます。
ソースコード実装例の`codeconv.d`から、`main()`を差し替えてください。
[UFCS](https://qiita.com/nak2yoshi/items/1bbd91fb23d7cc67109e)で書くと、すっきりしますね。

```d:応用例
void main()
{
  string s1 = [0x82, 0xA0, 0x82, 0xA8, 0x82, 0xE2, 0x82, 0xDC]; // Shift_JISで"あおやま"
  string s2 = s1.fromSJIS.toJIS;
  s2.writeHex; // 1b 24 42 24 22 24 2a 24 64 24 5e 1b 28 42
}
```

## ファイル入出力の実装例
同じく、ソースコード実装例の`codeconv.d`から、`main()`を差し替えてください。

```d:応用例
void main()
{
  auto fi = File("sjis.txt", "rt");
  scope(exit) fi.close();
  auto fo = File("jis.txt", "wt");
  scope(exit) fo.close();
  string line;
  while ( (line = fi.readln()) !is null ){
    fo.write(line.fromSJIS.toJIS);
  }
}
```

`Shift_JIS`から`JIS`への変換例として、`Shift_JIS`で保存したインプットファイル（sjis.txt）を用意します。

```text:sjis.txt
あおやま
あかさか
```

実行結果をバイナリエディタで表示しました。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/834b03e0-70f7-f51a-d058-2aab45291556.png)

# 参考情報

[D言語でShift-JISを入出力する](https://qiita.com/___uhu/items/9fc09e6aea9939395d74)
[PowerShellで文字コード変換](https://qiita.com/pureneo/items/b96db9bb215656e5616c)
[IBM漢字コードからUTF8やSJISに変換する表を手に入れたい→手に入れた](https://qiita.com/madilloar/items/12b9e270388c92d0482d)
[Code Page Identifiers](https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers)
[D言語のUFCSが好きだ！](https://qiita.com/nak2yoshi/items/1bbd91fb23d7cc67109e)
[この記事のサンプルコード、変換テーブル](https://github.com/devmynote/PartsFactory/tree/main/charcodeconv)

## 各種文字コード表
[Shift_JIS 文字コード表](https://seiai.ed.jp/sys/text/java/shiftjis_table.html)
[日本語EUC 文字コード表](http://charset.7jp.net/euc.html)
[JIS漢字コード表(JIS X 0208)](http://www.infonet.co.jp/ueyama/ip/binary/x0208txt.html)
[EBCDIC(IBM290, CCSID 5026)コード表](https://www.imagazine.co.jp/imagazine-7279/)
[IBM漢字(CP930)コード表](https://github.com/madilloar/pub-pages/tree/master/docs/ibm-kanji)