はじめに
C#定石 - ファイル一覧 - 照合規則 で、任意にピックアップした漢字を、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 | 示部 | シン・ジン | 
var arrayItems4 = new string[] { "一", "三", "二", "亜", "亞", "唖", 
                                 "啞", "啡", "排", "柛", "神", "神" };
Array.Sort(arrayItems4);
// → "亜", "唖", "一", "三", "神", "二", "排", "亞", "神", "啞", "啡", "柛"
JIS X 0213 面区点順をベースとしているようですが、「啞」がイレギュラーという結果になったので、漢字ソート順を確認してみることにします。
参考情報
下記情報を参考にさせて頂きました。
テスト環境
ここに記載した情報/ソースコードは、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
 
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
漢字ソート順
テストデータ作成とソート実行
まず、 JIS X 0213漢字一覧 - Wikipedia から、「JIS X 0213漢字一覧の1面」「JIS X 0213漢字一覧の2面」を、Unicode タブ区切りテキストファイルに落とします。
対象ファイルの項目は「文字」「面区点」「Shift_JIS-2004」「Unicode」「水準」「備考」となっていますが、「文字」「面区点」「水準」「Unicode」「UTF16」に変更します。
// 原本 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> を作成してソートします。
// 対象データ
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; }          // 第1水準, 第2水準, ...
  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 にアップロードしておきました。
- Windows 日本語カルチャ照合規則 確認
- JISX0213-1.txt(JIS X0213 漢字1面 区点順 テストデータ)
 - JISX0213-2.txt(JIS X0213 漢字2面 区点順 テストデータ)
 - JISX0213-sorted.txt(ソート結果)
 
 
結果確認
ソート結果を確認すると、末尾にサロゲートペアが固まっています。
このことから、文字クラスとして、「非サロゲートペア」<「サロゲートペア」 で分類がされているようです。
「非サロゲートペア」は、第1水準漢字と第2水準漢字については、いくつかのイレギュラー部分はありますが、JIS X 0213 面区点順をベースとしているようです。
「サロゲートペア」については、Unicode順となっています。
ソート結果全体で、JIS X 0213 面区点順ではない部分の抽出として「ひとつ前の文字の JIS X 0213 面区点よりも、該当文字の JIS X 0213 面区点が小さい」部分をピックアップすると 851ヶ所もあります。
このようなイレギュラーで、規則性を見いだせたのは、第1水準漢字の直後に、第3水準 異体字という並び順 5ヶ所のみでした。
| 文字 | 面区点 | 水準 | 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 | 
「非サロゲートペア」部分、第2水準漢字の末尾 JIS X 0213 1-84-06 以降は、第3水準漢字、第4水準漢字となります。
1-84-06 前後(前方1文字、後方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)で漢字をソートすると下記順序となります。
- 非サロゲートペア
- 第1水準漢字と第2水準漢字( + 第3水準漢字 5文字)
- 基本的に JIS X 0213 面区点順
 - 第1水準漢字の直後に、第3水準 異体字が配置されるケース有り
- 殺(1-27-06、第1水準)殺(1-86-41、第3水準)
 - 欄(1-45-83、第1水準)欄(1-86-27、第3水準)
 - 虜(1-46-26、第1水準)虜(1-91-47、第3水準)
 - 類(1-46-64、第1水準)類(1-94-04、第3水準)
 - 廊(1-47-13、第1水準)廊(1-84-14、第3水準)
 
 
 - 第3水準漢字と第4水準漢字
- 規則性を見出すことはできませんでした
 
 
 - 第1水準漢字と第2水準漢字( + 第3水準漢字 5文字)
 - サロゲートペア
- Unicode順