オリジナル文字コード（UTF-16, JIS8混在）からユニコード（UTF-16）へ変換する文字コード変換プログラムを作ってみました

# はじめに
　同じような悩みを持つ方がどれだけいるかは全く謎ですが，オリジナル文字コードを変換して，別の環境に移植する必要があったため，その方法を記載します。取り上げる内容は以下のとおりです。

* 対象は文字コードですが，何らしかのコードを変換するという仕組みとして共通に使用できます
* 文字コード初心者ならではの躓きを取り上げます
* サロゲートペアという unicode(ucs2) の範囲を超えてしまった文字を扱います
* C#（初心者）でスマートに実装しようとしたのですが，途中ライブラリの使用をあきらめています
* 今後の課題を取り上げます

# 環境
* Windows 10
* VS Code
* .Net Core SDK 3.1

# 工数
* 約１人日（以下，内訳）

1. 実装・テスト：1/4人日
1. qiitaへの投稿記載:1/4人日
1. サロゲートペア変換API（UNICODE符号位置からサロゲートペアへの変換）の捜索:2/4人日

##### 3については結局既存のライブラリを使用して実装することを断念して，計算することにしました。

# 背景
　以下の理由でシステムの若返りを図りたいと考えています。

* 技術者が足りないため，保守ができない
* 開発環境の選択が少ない
* データの利用が難しい（必ず難解な文字コード変換を挟むことになる）。

# 今回作成したもの
　下の赤枠の文字コード変換するためのツール
![文字コード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423321/b138bbeb-0277-4c08-e001-588c86679490.png)


# 変換ツールの材料

* １バイトデータ（JIS8）を２バイトデータ（BMP）に置換する変換テーブル

~~~txt:.\1ByteT\1ByteToBMP.csv
　：
23,0023
24,0024
25,0025
　：
~~~

* ２バイトデータを２バイトデータ（BMP）又は面02（U+2XXXX）に置換する変換テーブル

【特徴】変換元の環境では，BMP面の一部をサロゲートペア領域として使用しています（固定長データ処理系のため）。

~~~csv:.\2ByteT\2ByteToBMPandSIP.csv
　　：
2127,2127
212B,212B
　　：
A0C3,2373F
A0C4,23763
　　：
~~~
* 項目リスト（固定長データの区切り単位：レコードサイズ254バイト）

~~~txt:引数３
1B  ,1,10
1B  ,11,2
1B  ,13,2
1B  ,15,2
2B  ,17,100
2B  ,117,60
2B  ,177,40
1B  ,217,30
1B  ,247,7
1B  ,254,1
~~~

# 変換プログラム
１　変換プログラムのインターフェースは以下のとおりです。

~~~cmd:コマンド入力
module名 【引数１：入力ファイル名（固定長）】 【引数２：出力ファイル名（csv）】 【引数３：項目リスト】
~~~

２　変換プログラムは以下のとおりです。

~~~c#:Program.cs
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Diagnostics;
 
class Program
{
    static void Main(string[] args)
    {
        // CSVファイルの読み込み
        string filePath  = @".\JUFx_KMJ\JUF1_KMJ.csv";
        string filePath2 = @".\JUFx_KMJ\JUF2_KMJ.csv";

        string filePath3 = args[2];
        string filePath4 = args[0];
        string filePath5 = args[1];
 
        var myTable  = new Dictionary<string, string>();
        var myTable2 = new Dictionary<string, string>();

        using (StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("UTF-8")))
        {
            while (reader.Peek() >= 0)
            {
                // 読み込んだ文字列をカンマ区切りで配列に格納
                string[] cols = reader.ReadLine().Split(',');
                // コメント行を飛ばす
                if(Regex.IsMatch(cols[0], "^#")){continue;}
                for (int n = 0; n < cols.Length; n++)
                {
                    //For Debug
                    //Console.Write(cols[n] + ",");
                }
                // Dictionary生成
                myTable.Add(cols[0], cols[1]);
            }
        }

        using (StreamReader reader2 = new StreamReader(filePath2, Encoding.GetEncoding("UTF-8")))
        {
            while (reader2.Peek() >= 0)
            {
                // 読み込んだ文字列をカンマ区切りで配列に格納
                string[] cols = reader2.ReadLine().Split(',');
                // コメント行を飛ばす
                if(Regex.IsMatch(cols[0], "^#")){continue;}
                for (int n = 0; n < cols.Length; n++)
                {
                    //For Debug
                    //Console.Write(cols[n] + ",");
                }
                // Dictionary生成
                myTable2.Add(cols[0], cols[1]);
            }
        }

        // 最後まで読み込む
        List<string[]> readCsvList = new List<string[]>();
        using (StreamReader readCsvObject = new StreamReader(filePath3, Encoding.GetEncoding("utf-8")))
        {
            while (!readCsvObject.EndOfStream)
            {
                var readCsvLine = readCsvObject.ReadLine();
                readCsvList.Add(readCsvLine.Split(','));
            }
        }

        var recSize = Convert.ToInt32(readCsvList[readCsvList.Count - 1][1])
                        + Convert.ToInt32(readCsvList[readCsvList.Count - 1][2]) - 1;
        //Console.Write("recSize: " + recSize + "\n");
        //Console.Write(readCsvList.Count + "\n");
        using (FileStream fs = new FileStream(filePath4, FileMode.Open, FileAccess.Read)) {
        using (FileStream fsout = new FileStream(filePath5, FileMode.Create, FileAccess.Write)) {
        using (var writer = new BinaryWriter(fsout)) {
            // バイナリファイル読み出し
            int fileSize = (int)fs.Length; // ファイルのサイズ
            byte[] buf = new byte[fileSize]; // データ格納用配列

            int readSize; // Readメソッドで読み込んだバイト数
            int remain = fileSize; // 読み込むべき残りのバイト数
            int bufPos = 0; // データ格納用配列内の追加位置

            //マルチレイアウトは許可しない
            Debug.Assert(fileSize%recSize == 0);

            while (remain > 0) {
                readSize = fs.Read(buf, bufPos, Math.Min(recSize, remain));
                bufPos += readSize;
                remain -= readSize;
            }

            for(int i = 0; i < fileSize; i++) {
            for(int j = 0; j < readCsvList.Count; j++) {
                if((i != 0) && ((i%recSize) == 0)) 
                {
                    writer.Write(Convert.ToUInt16("000A", 16));    
                }
                if((j != 0) && ((i%recSize) == (Convert.ToInt32(readCsvList[j][1])-1)))
                {
                    writer.Write(Convert.ToUInt16("002C", 16));
                }

                // １バイトテーブル
                if(Regex.IsMatch(readCsvList[j][0], "EBC") && 
                ((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) && 
                ((i%recSize) <  (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1)))
                {
                    //Console.Write(myTable[buf[i].ToString("X2")] + "\n");
                    writer.Write(Convert.ToUInt16(myTable[buf[i].ToString("X2")], 16));
                    break;
                }
                // ２バイトテーブル
                if(Regex.IsMatch(readCsvList[j][0], "EKJ") && 
                ((i%recSize) >= (Convert.ToInt32(readCsvList[j][1])-1)) && 
                ((i%recSize) <  (Convert.ToInt32(readCsvList[j][1]) + Convert.ToInt32(readCsvList[j][2])-1)))
                {
                    Encoding utf8 = Encoding.GetEncoding("UTF-8");
                    int num = utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]);
                    if(utf8.GetByteCount(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]) <= 4)
                    {
                        //Console.Write(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")] + "\n");
                        writer.Write(Convert.ToUInt16(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")], 16));
                    }       
                    else
                    {
                        //サロゲートペア計算（ライブラリが見つからなかった）
                        var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16);
                        //Console.Write("codePoint: " + codePoint + "\n");
                        var codePoint2 = (codePoint - 0x10000) >> 10;
                        //Console.Write("plane: " + plane + "\n");
                        var lead = (codePoint2 & 0x3F) | 0xD800;
                        //Console.Write("lead: " + lead + "\n");
                        var trail = (codePoint & 0x3FF) | 0xDC00;
                        //Console.Write("trail: " + trail + "\n");
                        writer.Write(Convert.ToUInt16(lead));
                        writer.Write(Convert.ToUInt16(trail));
                    }
                    i++;
                    break;
                }
            }
            }
        }
        }
        }
    }
}
~~~

# 実装のポイント（妥協を含む）
### 1　テーブルのキー読み出しでエンディアン変換

　以下のコードでbuf（元データ：ビッグエンディアン）のデータを読み出し，そのままビッグエンディアン順でキーの指定を行っています（リトルエンディアンであれば，buf[i]とbuf[i+1]の指定が逆になる。）。
![BELE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423321/017c98ae-9cea-b438-67bd-41b417667e73.png)


~~~c#:ビッグエンディアンデータの読みだしとキー指定
myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")]
~~~

### 2　サロゲートペアの計算ロジックを作成した
　２バイトデータが変換テーブルを使用して，面02(U+2XXXX)に変換された場合，UTF-16ではサロゲートペアに更に変換が必要となる。その際，C#で変換ライブラリを見つけることができなかったため，以下の変換を実装しています。

~~~c#:サロゲートペア計算とバイナリ書き込み
var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16);
//Console.Write("codePoint: " + codePoint + "\n");
var codePoint2 = (codePoint - 0x10000) >> 10;
//Console.Write("plane: " + plane + "\n");
var lead = (codePoint2 & 0x3F) | 0xD800;
//Console.Write("lead: " + lead + "\n");
var trail = (codePoint & 0x3FF) | 0xDC00;
//Console.Write("trail: " + trail + "\n");
writer.Write(Convert.ToUInt16(lead));
writer.Write(Convert.ToUInt16(trail));
~~~

　ネット上にはいくつか実装方法があったが，乗算・除算器を使用するものだったため，加減算及びシフト演算のみのコードに書き直しました。また，サロゲートペアは32bitデータになりますが，UTF-16なので16ビットごとにリトルエンディアンによる書き込みが必要です。

# 今後
　計算時間が結構掛かっている割に，以下のようにプロセスエクスプローラーで確認するとCPU，DISKIOは全力を出し切っていません。実際の移行時には，HyperThread OFF にして，ファイルを４分割し，Threadを４本立てて同時実行する実装を現在検討しています。
![performance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423321/fa6f194c-8769-d45c-b296-1ce1fc24a4a4.png)

# 追記１

@jzkeyさんのコメントより，サロゲートペアの計算を以下のライブラリで計算できることがわかった。

~~~c#:Char.ConvertFromUtf32での実装
var codePoint = Convert.ToInt32(myTable2[buf[i].ToString("X2") + buf[i+1].ToString("X2")],16);
string sg = Char.ConvertFromUtf32(codePoint);
writer.Write(Convert.ToUInt16(sg[0]));
writer.Write(Convert.ToUInt16(sg[1]));
~~~