Windows 日本語カルチャ照合規則 - 漢字ソート順

## はじめに

[C#定石 - ファイル一覧 - 照合規則](https://qiita.com/chai0917/items/fbf33eb3db3cb1c82f27) で、任意にピックアップした漢字を、Windows 日本語カルチャ照合規則でソートした結果を確認しました。

| 文字 | Unicode | JIS X0213 | 水準 | 画数 | 部首 | 音読み |
| :--: | ---- | ------- | ---- | --: | ---- | ------ |
| 一 | U+4E00 | 1-16-76 | 第1水準 |   1 | 一部 | イチ・イツ |
| 三 | U+4E09 | 1-27-16 | 第1水準 |   3 | 一部 | サン      |
| 二 | U+4E8C | 1-38-83 | 第1水準 |   2 | 二部 | ニ        |
| 亜 | U+4E9C | 1-16-1  | 第1水準 |   7 | 二部 | ア        |
| 亞 | U+4E9E | 1-48-19 | 第2水準 |   8 | 二部 | ア        |
| 唖 | U+5516 | 1-16-2  | 第1水準 |  10 | 口部 | ア・アク  |
| 啞 | U+555E | 1-15-8  | 第3水準 |  11 | 口部 | ア・アク  |
| 啡 | U+5561 | 2-4-8   | 第4水準 |  11 | 口部 | ハイ・ヒ  |
| 排 | U+6392 | 1-39-51 | 第1水準 |  11 | 手部 | ハイ      |
| 柛 | U+67DB | 2-14-50 | 第4水準 |   9 | 木部 | シン      |
| 神 | U+795E | 1-31-32 | 第1水準 |   9 | 示部 | シン・ジン |
| 神 | U+FA19 | 1-89-28 | 第3水準 |  10 | 示部 | シン・ジン |

```CSharp
var arrayItems4 = new string[] { "一", "三", "二", "亜", "亞", "唖", 
                                 "啞", "啡", "排", "柛", "神", "神" };
Array.Sort(arrayItems4);
// → "亜", "唖", "一", "三", "神", "二", "排", "亞", "神", "啞", "啡", "柛"
```

JIS X 0213 面区点順をベースとしているようですが、「啞」がイレギュラーという結果になったので、漢字ソート順を確認してみることにします。

### 参考情報
下記情報を参考にさせて頂きました。

- [JIS X 0213漢字一覧 - Wikipedia](https://ja.wikipedia.org/wiki/JIS_X_0213%E6%BC%A2%E5%AD%97%E4%B8%80%E8%A6%A7)
- [JIS X 4061 - 日本産業規格の簡易閲覧](https://kikakurui.com/x4/X4061-1996-01.html)

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

## 漢字ソート順

### テストデータ作成とソート実行

まず、 [JIS X 0213漢字一覧 - Wikipedia](https://ja.wikipedia.org/wiki/JIS_X_0213%E6%BC%A2%E5%AD%97%E4%B8%80%E8%A6%A7) から、「JIS X 0213漢字一覧の1面」「JIS X 0213漢字一覧の2面」を、Unicode タブ区切りテキストファイルに落とします。

対象ファイルの項目は「文字」「面区点」「Shift_JIS-2004」「Unicode」「水準」「備考」となっていますが、「文字」「面区点」「水準」「Unicode」「UTF16」に変更します。

```CSharp
// 原本 TSV → 今回利用 TSV
private void Original2Custom(string original, string custom)
{
  using (var sr = new StreamReader(original, Encoding.UTF8))
  using (var sw = new StreamWriter(custom, false, Encoding.UTF8))
  {
    // ヘッダー
    // original「文字」「面区点」「Shift_JIS-2004」「Unicode」「水準」「備考」
    // → custom「文字」「面区点」「水準」「Unicode」「UTF16」
    var line = sr.ReadLine();
    sw.WriteLine("文字\t面区点\t水準\tUnicode\tUTF16");

    // データ
    while (sr.Peek() > -1)
    {
      // 1行取得してタブ区切りで分解
      line = sr.ReadLine().Trim();
      var items = line.Split('\t');
      if (items?.Length >= 5)
      {
        sw.WriteLine(string.Format("{0}\t{1}\t{2}\t{3}\t{4}",
                              parts[0].Trim(), parts[1].Trim(),
                              parts[4].Trim(), parts[3].Trim(),
                              HexaDumpUTF16(parts[0].Trim())));
      }
    }
  }
}
// UTF16 ヘキサダンプ
private string HexaDumpUTF16(string target)
{
  var chars = target.ToCharArray();
  var sb = new StringBuilder();
  foreach (char c in chars)
  {
    sb.Append(string.Format("0x{0:X4} ", (ushort)c));
  }
  return sb.ToString().TrimEnd();
}
```

データ管理クラスとして CharX0213 を用意、`List<CharX0213>` を作成してソートします。

```CSharp
// 対象データ
private List<CharX0213> lstTarget = new List<CharX0213>();

// 対象ファイルロード → ソート → 結果ファイル出力
public void DoSort(string x0213_1, string x0213_2, string output)
{
  // 対象ファイルロード
  lstTarget.Clear();
  File2List(x0213_1);
  File2List(x0213_2);

  // ソート
  var sorted = lstTarget.OrderBy(x => x.Target);

  // 結果出力
  using (var sw = new StreamWriter(output, false, Encoding.UTF8))
  {
    // ヘッダ
    sw.WriteLine("文字\t面区点\t水準\tUnicode\tUTF16");
    // データ
    foreach(var item in sorted)
    {
      sw.WriteLine(string.Format("{0}\t{1}\t{2}\t{3}\t{4}",
                            item.Target, item.X0213, 
                            item.Level, item.Unicode, 
                            item.HexaDumpUTF16));
    }
  }
}
// ファイル ロード
private void File2List(string path)
{
  using (var sr = new StreamReader(path, Encoding.UTF8))
  {
    // ヘッダ
    var line = sr.ReadLine();
    // データ
    while (sr.Peek() > -1)
    {
      line = sr.ReadLine().Trim();
      var items = line.Split('\t');
      if (items?.Length >= 5)
      {
        lstTarget.Add(new CharX0213(items));
      }
    }
  }
}

public class CharX0213
{
  public string Target { get; set; }         // 文字
  public string X0213 { get; set; }          // JIS X 0213 面区点
  public string Level { get; set; }          // 第１水準, 第２水準, ...
  public string Unicode { get; set; }        // Unicode
  public string HexaDumpUTF16 { get; set; }  // UTF16 16進数

  public CharX0213(int sirial, string [] items)
  {
    if (items.Length >= 5)
    {
      Target = items[0].Trim();
      X0213 = items[1].Trim();
      Level = items[2].Trim();
      Unicode = items[3].Trim();
      HexaDumpUTF16 = items[4].Trim();
    }
  }
}
```

掲載ソースで作成したテストデータと、Windows 日本語カルチャ照合規則でソートした結果を GitHub にアップロードしておきました。

https://github.com/chaichai0917/SampleCode

- Windows 日本語カルチャ照合規則 確認
  - JISX0213-1.txt（JIS X0213 漢字１面 区点順 テストデータ）
  - JISX0213-2.txt（JIS X0213 漢字２面 区点順 テストデータ）
  - JISX0213-sorted.txt（ソート結果）


### 結果確認

ソート結果を確認すると、末尾にサロゲートペアが固まっています。
このことから、文字クラスとして、`「非サロゲートペア」＜「サロゲートペア」` で分類がされているようです。

「非サロゲートペア」は、第１水準漢字と第２水準漢字については、いくつかのイレギュラー部分はありますが、JIS X 0213 面区点順をベースとしているようです。
「サロゲートペア」については、Unicode順となっています。

ソート結果全体で、JIS X 0213 面区点順ではない部分の抽出として「ひとつ前の文字の JIS X 0213 面区点よりも、該当文字の JIS X 0213 面区点が小さい」部分をピックアップすると 851ヶ所もあります。
このようなイレギュラーで、規則性を見いだせたのは、第１水準漢字の直後に、第３水準 異体字という並び順 ５ヶ所のみでした。



| 文字 | 面区点 | 水準 | Unicode | UTF16 |
| ---- | ----- | ---- | ------- | ----- |
| 殺 | 1-27-06 | 第1水準 | U+6BBA | 0x6BBA |
| 殺 | 1-86-41 | 第3水準 | U+F970 | 0xF970 |
| 薩 | 1-27-07 | 第1水準 | U+85A9 | 0x85A9 |

| 文字 | 面区点 | 水準 | Unicode | UTF16 |
| ---- | ----- | ---- | ------- | ----- |
| 欄 | 1-45-83 | 第1水準 | U+6B04 | 0x6B04 |
| 欄 | 1-86-27 | 第3水準 | U+F91D | 0xF91D |
| 濫 | 1-45-84 | 第1水準 | U+6FEB | 0x6FEB |

| 文字 | 面区点 | 水準 | Unicode | UTF16 |
| ---- | ----- | ---- | ------- | ----- |
| 虜 | 1-46-26 | 第1水準 | U+865C | 0x865C |
| 虜 | 1-91-47 | 第3水準 | U+F936 | 0xF936 |
| 了 | 1-46-27 | 第1水準 | U+4E86 | 0x4E86 |

| 文字 | 面区点 | 水準 | Unicode | UTF16 |
| ---- | ----- | ---- | ------- | ----- |
| 類 | 1-46-64 | 第1水準 | U+985E | 0x985E |
| 類 | 1-94-04 | 第3水準 | U+F9D0 | 0xF9D0 |
| 令 | 1-46-65 | 第1水準 | U+4EE4 | 0x4EE4 |

| 文字 | 面区点 | 水準 | Unicode | UTF16 |
| ---- | ----- | ---- | ------- | ----- |
| 廊 | 1-47-13 | 第1水準 | U+5ECA | 0x5ECA |
| 廊 | 1-84-14 | 第3水準 | U+F928 | 0xF928 |
| 弄 | 1-47-14 | 第1水準 | U+5F04 | 0x5F04 |

「非サロゲートペア」部分、第２水準漢字の末尾 JIS X 0213 1-84-06 以降は、第３水準漢字、第４水準漢字となります。
1-84-06 前後（前方１文字、後方15文字）のソート順を抜粋します。

| 文字 | 面区点 | 水準 | Unicode | UTF16 |
| ---- | ----- | ---- | ------- | ----- |
| 凜 | 1-84-05 | 第2水準 | U+51DC | 0x51DC |
| 熙 | 1-84-06 | 第2水準 | U+7199 | 0x7199 |
| 纊 | 1-90-23 | 第3水準 | U+7E8A | 0x7E8A |
| 褜 | 1-91-78 | 第3水準 | U+891C | 0x891C |
| 鍈 | 1-93-25 | 第3水準 | U+9348 | 0x9348 |
| 銈 | 1-93-14 | 第3水準 | U+9288 | 0x9288 |
| 蓜 | 2-86-53 | 第4水準 | U+84DC | 0x84DC |
| 俉 | 1-14-25 | 第3水準 | U+4FC9 | 0x4FC9 |
| 炻 | 2-79-64 | 第4水準 | U+70BB | 0x70BB |
| 昱 | 1-85-21 | 第3水準 | U+6631 | 0x6631 |
| 棈 | 1-85-73 | 第3水準 | U+68C8 | 0x68C8 |
| 鋹 | 2-91-03 | 第4水準 | U+92F9 | 0x92F9 |
| 曻 | 1-85-23 | 第3水準 | U+66FB | 0x66FB |
| 彅 | 1-84-26 | 第3水準 | U+5F45 | 0x5F45 |
| 丨 | 1-14-04 | 第3水準 | U+4E28 | 0x4E28 |
| 仡 | 1-14-08 | 第3水準 | U+4EE1 | 0x4EE1 |
| 伀 | 2-01-31 | 第4水準 | U+4F00 | 0x4F00 |

画数、部首、音読みは関係なさそうですよね、、、
知識不足のため、この並び順から、規則性を見出すことはできませんでした。


### 自然順（エクスプローラ互換）

WIN32API - StrCmpLogicalW でソートした場合についても、Windows 日本語カルチャ照合規則と同様の結果となりました。

## まとめ

Windows 日本語カルチャ照合規則、および、自然順（WIN32API - StrCmpLogicalW）で漢字をソートすると下記順序となります。

- 非サロゲートペア
  - 第１水準漢字と第２水準漢字（ + 第３水準漢字 ５文字）
    - 基本的に JIS X 0213 面区点順
    - 第１水準漢字の直後に、第３水準 異体字が配置されるケース有り
      -  殺（1-27-06、第１水準）殺（1-86-41、第３水準）
      -  欄（1-45-83、第１水準）欄（1-86-27、第３水準）
      -  虜（1-46-26、第１水準）虜（1-91-47、第３水準）
      -  類（1-46-64、第１水準）類（1-94-04、第３水準）
      -  廊（1-47-13、第１水準）廊（1-84-14、第３水準）
  - 第３水準漢字と第４水準漢字
    - 規則性を見出すことはできませんでした 
- サロゲートペア
  - Unicode順