異体字を安易に使用してはいけない

##はじめに
前回の投稿で「[異体字を簡単に入力する](https://qiita.com/Mt_SQ/items/87e944858476862450f1)」方法を紹介しました。
しかし、異体字や旧字体の安易な使用は
　<b>・環境に依存する
　・「異体字セレクタ」は発見や除去が難しい
　・文字列処理でトラブルの原因になり得る</b>
など、問題を起こす可能性も高いので使用の際は要注意です。

前回の投稿のカバーと注意喚起のため、自身が遭遇した実例を基に記事にまとめました。


##作業環境ほか
OS : Windows 10 Pro
Excel : Excel for Microsoft 365 MSO 32bit
PowerShell : Ver. 7.2.1

##Excelでの問題事例
###(1) LEN関数 および LENB関数での問題
Excelで異体字・旧字体を使用した場合に起きやすい「想定外（見た目と結果が異なる）」の事例です。
`LEN`関数や`LENB`関数は、それぞれ「文字数」や「バイト数(全角2バイト・半角1バイト)」を返すとの解説が一般的ですが･･･。
![異体字2-例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/cb04877b-8f71-00ac-dc1f-3564ecc17241.png)
<b>【図1】</b>

B,C列はA列に対する`LEN`関数および`LENB`関数の結果です。
「想定外」の部分には<font color=red>赤枠</font>をつけています。
なお、A列のフォントは「游明朝」、２文字目（田・藤）は「通常の文字」です。
もちろん「空白文字」の混入は無く、編集時もカーソルは２文字分しか移動しません。

Google Drive に[データ（異体字問題.xlsx）](https://docs.google.com/spreadsheets/d/1v-pwhA0K_CRRgq9O5ke8X7i4TKERQ8FK/edit?usp=sharing&ouid=117751781531543987674&rtpof=true&sd=true)を置いておきます。
Googleスプレッドシートが開きますが、ダウンロードは可能です。
（ちなみに、Googleスプレッドシートは`LENB`関数の結果が異なっています。）

さて、先頭文字が問題を起こしているので、`CODE`関数と`UNICODE`関数、念のため、VBAの`Len`関数と`LenB`関数の戻り値も確認してみます。（下図<b>【図2(a)】</b>）
セルA4,A5は、見た目が同じなのに文字コードは異なります。また、VBAの`LenB`関数はワークシート関数の`LENB`関数と異なる結果を返しますが、本題から逸れますのでコメントは控えます。
![異体字2-問題例1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/fa9d3092-39b1-bd4c-c27a-8e05d34b91ae.png)
<b>【図2(a)】</b> ※`CODE`関数は、JISコードが存在しない場合 0x3F('?') を返します。（一部例外あり）

数式は以下のとおりです。
![異体字2-数式.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/7775ee19-6ff6-c8a2-ed92-7c80332bc0ad.png)
<b>【図2(b)】</b> ※ F,G列は、次のユーザー定義関数です。

```vb:Excel&#x20;VBA
Function vba_len(c)
  vba_len = Len(c.Value)
End Function

Function vba_lenb(c)
  vba_lenb = LenB(c.Value)
End Function
```
先頭文字だけでは情報不足なので、セル内の全文字列をPowerShellで確認します。
セルA2:A14をデータをクリップボードにコピーして、次のワンライナーを実行します。

:::note warn
当記事に載せたPowerShellスクリプトの多くは、Windows PowerShell 5 で動作しません。
また、データの受け渡しにクリップボードを使用しますが、ExcelとPowerShellの相性問題（<a href="https://qiita.com/Mt_SQ/items/79383ee6fb31e78ce1bb#%E3%81%94%E6%B3%A8%E6%84%8F">詳細は過去の投稿をご参照ください</a>）から、PowerShell 7 の使用を前提とします。
:::

<b>【スクリプト1】（セル検査）</b>

```PowerShell:PowerShell&#x20;7
(gcb) -ne '' | %{($_.EnumerateRunes().Value | %{"U+{0:X4}" -f $_}) -join ' '} | scb
```
クリップボードの文字列をコードポイントで取得し、クリップボードに戻す単純なスクリプトです。`String.EnumerateRunes`メソッドは .NET Core 以降の機能です。

スクリプト実行後、セルF2でペーストしたものが下図<b>【図3】</b>です。
![異体字2-文字コード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/4022d1da-cc02-5698-ceca-a00770abe2c9.png)
<b>【図3】</b>

セルA3,A4,A7には異体字セレクタが使われていることがわかります。
結局、<b>異体字セレクタの有無にかかわらず、`LEN`関数および`LENB`関数（VBAも含め）は「想定外」となる可能性があり、あてにできません。</b>
特に人名に使われる異体字・旧字体は問題が発生する可能性が高くなります。

ちなみに、下図<b>【図4】</b>を見ると「UnicodeがU+FFFFに収まっているか？」、「JISコードが割り当てられているか？」の２条件から`LEN`関数および`LENB`関数の結果は<b>推測できそう</b>です。
![異体字2-LEN推察.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/6ff27182-a9e6-ad4a-8c91-43fe46b9679b.png)
<b>【図4】</b>

###(2) 印刷や表示が乱れる可能性がある。
下図<b>【図5(a)】</b>でB,C列はA列の値を参照し、配置やフォントを変更したものです。
一部のセルは影響を受けています。（<font color=red>赤枠部</font>）
![異体字2-問題例2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/69a2180c-0dd4-70d3-e313-13f565793e59.png)
<b>【図5(a)】</b>

このシートの印刷プレビュー画面<b>【図5(b)】</b>でも、異常が確認でき、実際の印刷結果にも不具合が生じます。特に、異体字セレクタとして「字形選択子」(U+FE0<font color=red><b>X</b></font>)を使った場合には、表示や印刷、pdf化などに問題が生じます。（<b>【図5(c)】【図5(d)】</b>）
このような問題は、原因の特定に苦労しそうです。
![異体字2-印刷.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/9a6138de-7b16-1eec-d854-ea2f26db3622.png)
<b>【図5(b)】 印刷プレビュー</b>
![異体字2-pdf印刷.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/6d902713-c359-aa5a-e627-cd8fbf8a9fd1.png)
<b>【図5(c)】 印刷でpdf化「Microsoft Print to PDF」</b>
![異体字2-pdf保存.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/9efcc091-b74a-8869-e23f-c036498b6a3b.png)
<b>【図5(d)】 保存でpdf化「名前をつけて保存 - ファイルの種類:PDF」</b>

##対策について
上記の事例をベースにPowerShellを使った対応手段をいくつか示します。

###(1) 異体字セレクタの除去
例示するスクリプトは、人名データを想定して、漢字・ひらがな・カタカナ、長音記号「ー」・同音の繰り返し記号「ゝ」「ゞ」「々」や空白文字で構成されるデータを処理します。

`Rune.Is～`メソッドを使った異体字セレクタ除去例です。
Excelでコピーしたセルから、「文字」「空白文字（改行文字含む）」以外を除去してクリップボードに戻します。

<b>【スクリプト2】（異体字セレクタの除去）</b>

```PowerShell:PowerShell&#x20;7
-join ((gcb -raw).EnumerateRunes() | ?{[Text.Rune]::IsLetter($_) -or [Text.Rune]::IsWhiteSpace($_)} | %{$_.ToString()}) | scb
```

<b>【図1】</b>のセルA2:A14のデータで処理してB列にペーストしました。
結果を確認するため、あらためてB列（B2:B14）をコピーし、先の<b>【スクリプト1】</b>でデータを検査します。（C列）
![異体字2-IVS除去.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/59aa3eb4-f4fe-e06f-79e0-b105f77b85bc.png)
<b>【図3】</b>と比較して、異体字セレクタが除去されています。

ちなみに、上記スクリプトは、㈱(U+3231)などの記号も除去します。
「㈱」を残したい場合は判断条件を追加します。
ついでに、「ゞ」は文字扱いであり、除去されないことも確認します。

```PowerShell:PowerShell&#x20;7
# ㈱が除去される
-join ("いすゞ自動車㈱".EnumerateRunes() | ?{[Text.Rune]::IsLetter($_) -or [Text.Rune]::IsWhiteSpace($_)} | %{$_.ToString()})
# いすゞ自動車

# ㈱が除去されないようにする
-join ("いすゞ自動車㈱".EnumerateRunes() | ?{[Text.Rune]::IsLetter($_) -or [Text.Rune]::IsWhiteSpace($_) -or [Text.Rune]::IsSymbol($_)} | %{$_.ToString()})
# いすゞ自動車㈱
```

###(2) LEN関数の代替手段
`LEN`関数について、先の推測が正しいとすれば、ワークシート関数やVBAで「正しい」文字数を得ることは可能かと思いますが、個人的には気乗りしません。
関数にこだわらなければ、PowerShellで見た目どおりの「正しい」文字数を取得できます。もちろん、異体字セレクタの有無に関係なく機能します。

<b>【スクリプト3】（見た目どおりの文字数を取得）</b>

```PowerShell:PowerShell&#x20;7
(gcb) -ne '' | %{[Globalization.StringInfo]::new($_).LengthInTextElements} | scb
```

<b>【図1】</b>のデータに手を加えて、`LEN`関数と上記スクリプトの結果（C列）を比較してみます。
![異体字2-LEN対策.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/45d5d953-8176-8757-8d86-ba6ab8fb2f32.png)
C列は見た目どおりの文字数を返しています。

###(3) Unicode正規化
異体字・旧字体の中でも「CJK互換漢字」「CJK互換漢字補助」の範囲に割り当てられた文字は、Unicode正規化が可能です。（「﨑」(U+FA11)などの例外あり）
異体字が原因で検索や並べ替えに不都合が生じるような場合、一考の余地はあります。
<b>【図1】</b>のセルA5の「梅」(U+FA44)、A9の「塚」(U+FA10)などが該当します。
「梅」(U+6885)を検索するとA2～A4は引っかかりますが、A5はスルーされます。

.NETでは`String.Normalize`メソッドを使ってUnicode正規化を実現できるので、PowerShellで確認してみます。

```PowerShell:PowerShell&#x20;7
([int][char]"`u{fa44}".Normalize()).ToString("X")
# 6885
([int][char]"`u{fa10}".Normalize()).ToString("X")
# 585A
```

梅(U+FA44) は 梅(U+6885) に、塚(U+FA10) は 塚(U+585A) に正規化されています。
必要に応じて`.Normalize()`を追加するだけなので手間はかかりません。