C#定石 - ファイル一覧 - 照合規則

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

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

- [自然順 - Wikipedia](https://ja.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E9%A0%86)
- [日本語文字列照合順番 - Wikipedia](https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E%E6%96%87%E5%AD%97%E5%88%97%E7%85%A7%E5%90%88%E9%A0%86%E7%95%AA)
- [JIS X 0213漢字一覧 - Wikipedia](https://ja.wikipedia.org/wiki/JIS_X_0213%E6%BC%A2%E5%AD%97%E4%B8%80%E8%A6%A7)
- [漢字辞典オンライン](https://kanji.jitenon.jp/)


### テスト環境

ここに記載した情報/ソースコードは、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 機能範囲で記述しています。
:::

## ファイル一覧

さまざまな局面で、ファイル一覧を生成して、利用することがありました。
ファイル一覧は、用途によって、以下の３パターンのいずれかの実装としていました。

- Windows 日本語カルチャ照合規則でソート
- 自然順（エクスプローラ互換）でソート
- ソート未実施

::: note
文字列データに対する、一致判定、および、ソート並び順を既定するルールを照合規則（照合順序）と呼びます。
JIS 規格としては、「JIS X 4061 日本語文字列照合順番」が存在しています。
:::

照合規則についての情報は、本記事、後半に記載します。
まずは、前述３パターンのファイル一覧について、サンプルコードを記載します。

### Windows 日本語カルチャ照合規則でソート

Windows 日本語カルチャ（ja-jp）既定の照合規則を利用したソートです。
LINQ で、フルパスではなく、ファイル名のみとしてからソートしています。

```CSharp
// フォルダ下のファイル一覧処理
private int FilesUnderFolder(string target)
{
  int fileCount = 0;   // フォルダ下のファイル数
  var dirInfo = new DirectoryInfo(target);

  // サブフォルダ下も処理する場合はコメントを外す
  //// フォルダ名称のみを Windows 日本語カルチャ照合規則でソート
  // var folders = dirInfo.GetDirectories()
  //                 .Select(fileinfo => fileinfo.Name).OrderBy(x => x);
  // if (folders != null)
  // {
  //   foreach (var folder in folders)
  //   {
  //     string fullPath = target + '\\' + folder;
  //     fileCount += FilesUnderFolder(fullPath);
  //   }
  // }

  // ファイル名称のみを Windows 日本語カルチャ照合規則でソート
  var files = dirInfo.GetFiles()
                .Select(fileinfo => fileinfo.Name).OrderBy(x => x);
  if (files != null)
  {
    foreach (var file in files)
    {
      string fullPath = target + '\\' + file;
      // TODO

      fileCount++;
    }
  }
  return fileCount;
}
```

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

自然順を手軽に実現するのは、WIN32API - StrCmpLogicalW を利用することです。
Array、List、LINQ で利用可能な `IComparer<string>` を実装したクラスを作成します。

https://learn.microsoft.com/ja-jp/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw

```CSharp
// 自然順 比較クラス
public class NaturalComparer : IComparer<string>
{
  // WIN32API
  private static class NativeMethods
  {
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
    public static extern int StrCmpLogicalW(string psz1, string psz2);
  }
  private NaturalComparer()
  {
  }
  public int Compare(string x, string y)
  {
    if (ReferenceEquals(x, y)) return 0;
    if (x is null) return -1;
    if (y is null) return 1;
    // 文字列比較：自然順 - ここを変更することで任意の照合規則とすることが可能   
    return NativeMethods.StrCmpLogicalW(x, y);
  }
  public static NaturalComparer Natural
  {
    get
    {
      if (_Natural == null)
        _Natural = new NaturalComparer();
      return _Natural;
    }
  }
  static NaturalComparer _Natural = null;
}
```

::: note
.NET 8 の場合、Null許容参照型の明示が必要です。

```CSharp
public int Compare(string? x, string? y)
static NaturalComparer? _Natural = null;
```
:::

LINQ - OrderBy に NaturalComparer を指定すると、「自然順（エクスプローラ互換）でソート」となります。

```CSharp
// ファイル名称のみを自然順（エクスプローラ互換）でソート
var files = dirInfo.GetFiles()
              .Select(fileinfo => fileinfo.Name)
              .OrderBy(x => x, NaturalComparer.Natural);
if (files != null)
{
  foreach (var file in files)
  {
    string fullPath = target + '\\' + file;
    // TODO

    fileCount++;
  }
}
```

::: note
NaturalComparer クラスは、Array、List でも利用可能です。
```CSharp
var arrayItems = new string[] { "hoge5.txt", "hoge10.txt", "hoge1.txt" };
var lstItems = new List<string>(arrayItems);

// Array
Array.Sort(arrayItems, NaturalComparer.Natural);
// List
lstItems.Sort(NaturalComparer.Natural);
```
:::

### ソート未実施

「ソート未実施」の場合は、LINQ でフルパスを取得します。

```CSharp
// ファイル一覧取得 - フルパス
var files = dirInfo.GetFiles()
              .Select(fileinfo => fileinfo.FullName); 
if (files != null)
{
  foreach (var file in files)
  {
    // TODO - file はフルパス
    
    fileCount++;
  }
}
```

## 照合規則

照合規則の構成要素として、以下のようなルールがあげられます。

- 同一視（区別をしない）
  - 同一視する文字は、同一視したグループ内でウェイト（優先順位）を付与する
  - 同一視状態で完全一致した場合は、後方文字からウェイトに基づいて順序付けする
  - 同一視対象としては、下記などがあります
    - 英大文字 / 英小文字
    - 全角 / 半角
    - ひらがな / カタカナ
    - 小書き文字 / 清音文字 / 濁点文字 / 半濁音文字
    - アラビア数字（算用数字）/ 漢数字 / ローマ数字
- 比較方法
  - 文字の数値部分
    - 文字として比較 / 数値として比較
  - 非数値の文字
    - 文字クラスで分類して、分類同士の序列を設定（下記に一例を記載） 
      - 記号 ＜ アラビア数字 ＜ アルファベット ＜ ひらがな・カタカナ ＜ 漢字
    - 分類内の比較 
      - 文字コード順（Unicode / JIS X 0213 面区点）/ 辞書順（よみがな順）など

::: note
文字コードにおいて、漢字は「部首/画数順」「音読み順」などの順列で配置されているので、文字コード順では、この配置順にソートされます。
:::

::: note
照合規則について詳しく知りたい方は、下記情報などを参照してください。
- [日本語文字列照合順番 - Wikipedia](https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E%E6%96%87%E5%AD%97%E5%88%97%E7%85%A7%E5%90%88%E9%A0%86%E7%95%AA)
- [JIS X 4061 - 日本産業規格の簡易閲覧](https://kikakurui.com/x4/X4061-1996-01.html)
- [照合順序 - 文字の比較と並び順（その 1）| Microsoft Learn](https://learn.microsoft.com/ja-jp/archive/blogs/jpsql/1-3)
:::

以降では、Windows に標準搭載されている機能をベースに、いくつかのパターンを確認してみます。

### Windows 日本語カルチャ照合規則

「JIS X 4061 日本語文字列照合順番」をベースとした実装であると認識していますが、正直、あまり詳しいことまでは理解していません。
そのため、どのような動作となるか、具体的な例を用いて確認することとします。

まずは、同一視対象について確認してみます。

```CSharp
// 英大文字/英小文字 + 全角/半角
var arrayItems1 = new string[] {"a", "b", "A", "B", "Ａ", "Ｂ", "ａ", "ｂ"};
Array.Sort(arrayItems1);
// → "a", "ａ", "A", "Ａ", "b", "ｂ", "B", "Ｂ"

// ひらがな/カタカナ + 全角/半角 + 小書き文字/清音文字/濁点文字/半濁音文字
var arrayItems2 = new string[] { "あ", "い", "ア", "イ", "ｱ", "ｲ", 
                                 "は", "ぱ", "ば", "つ", "っ", "づ" };
Array.Sort(arrayItems2);
// → "ｱ", "ア", "あ", "ｲ", "イ", "い", "っ", "つ", "づ", "は", "ば", "ぱ"

var arrayItems3 = new string[] { "1", "2", "１", "２", "一", "二", "Ⅰ", "Ⅱ" };
Array.Sort(arrayItems3);
// → "1", "１", "2", "２", "Ⅰ", "Ⅱ", "一", "二"
```

アルファベット、ひらがな/カタカナは、同一視対象とされています。
数値を文字として比較した場合、漢数字とローマ数字は、アラビア数字と同一視対象になりません。

漢字については、母数が少ないですが、下記を用いて確認してみます。

| 文字 | 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 面区点順をベースとしているようですが、「啞」がイレギュラーですね、、、
他にもイレギュラーがありそうなので、別記事で詳細確認することにします。

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

文字データの数値部分を数値として比較する照合規則です。
ファイル一覧サンプルコードとして掲載した NaturalComparer を用いて確認してみます。

```CSharp
var arrayItems = new string[] {"hoge5.txt", "hoge10.txt", "hoge1.txt"};

// 文字コード順（同一視対象存在）
Array.Sort(arrayItems);
// → "hoge1.txt", "hoge10.txt", "hoge5.txt"

// 自然順（エクスプローラ互換）
Array.Sort(arrayItems, NaturalComparer.Natural);
// → "hoge1.txt", "hoge5.txt", "hoge10.txt"
```

::: note
日本語の語彙として序列を表す「上 / 中 / 下」「前 / 後」、および、漢数字などについても、語彙順にソートされることが「自然」と感じますが、Windows の自然順ではサポート対象外です。
これらの文字を「自然」な並び順とするには、語彙解釈が必要となるためだと思います。
- 序列を表すために利用時（上期、中期、下期）は、序列に従う
- 上記以外は、非数値の文字データに基づく比較（文字コード順、辞書順、特殊）
:::

Windows 日本語カルチャ照合規則との差異について確認してみます。
まずは、同一視対象です。

```CSharp
// 英大文字/英小文字 + 全角/半角
var arrayItems1 = new string[] {"a", "b", "A", "B", "Ａ", "Ｂ", "ａ", "ｂ"};
Array.Sort(arrayItems1, NaturalComparer.Natural);
// → "a", "A", "Ａ", "ａ", "b", "B", "Ｂ", "ｂ"

// ひらがな/カタカナ + 全角/半角 + 小書き文字/清音文字/濁点文字/半濁音文字
var arrayItems2 = new string[] { "あ", "い", "ア", "イ", "ｱ", "ｲ", 
                                 "は", "ぱ", "ば", "つ", "っ", "づ" };
Array.Sort(arrayItems2, NaturalComparer.Natural);
// → "ｱ", "ア", "あ", "ｲ", "イ", "い", "っ", "つ", "づ", "は", "ば", "ぱ"

var arrayItems3 = new string[] { "1", "2", "１", "２", "一", "二", "Ⅰ", "Ⅱ" };
Array.Sort(arrayItem3, NaturalComparer.Natural);
// → "1", "１", "2", "２", "Ⅰ", "Ⅱ", "一", "二"
```

同一視対象は同等ですが、アルファベットのウェイトに相違がありました。
この差異には気づいていませんでした。注意が必要ですね。

| 照合規則                      | アルファベット ウェイト |
| ---------------------------- | ---------------------- |
| Windows 日本語カルチャ照合規則 | 半角小文字 ＜ 全角小文字 ＜ 半角大文字 ＜ 全角大文字 |
| 自然順 - StrCmpLogicalW       | 半角小文字 ＜ 半角大文字 ＜ 全角大文字 ＜ 全角小文字 |


次に、漢字につい確認します。

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

これは同一結果です。


### 文字コード順

StringComparer で用意されている Ordinal を利用すると、文字コード順（Unicode順）でソートできます。

https://learn.microsoft.com/ja-jp/dotnet/api/system.stringcomparer

```CSharp
var arrayItems4 = new string[] { "一", "三", "二", "亜", "亞", "唖", 
                                 "啞", "啡", "排", "柛", "神", "神" };
Array.Sort(arrayItems4); // Windows 日本語カルチャ照合規則順にする
var lstItems4 = new List<string>(arrayItems4);

// Array
Array.Sort(arrayItems4, StringComparer.Ordinal);
// → "一", "三", "二", "亜", "亞", "唖", "啞", "啡", "排", "柛", "神", "神", 

// List
stItems4.Sort(StringComparer.Ordinal);

// LINQ
var sortd = arrayItems4.OrderBy(x => x, StringComparer.Ordinal);
```

結果を確認すると文字コード順となりました。