Unicode - 全角半角

## はじめに

[Unicode - 恩恵と厄介事](https://qiita.com/chai0917/items/16fa57fc3078c5314d8d) 記事作成時、うまく整理できず省いてしまった、全角半角について記載しようと思います。

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

- [全角と半角 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%85%A8%E8%A7%92%E3%81%A8%E5%8D%8A%E8%A7%92)
- [文章中の英数字の表記は、半角が正しい？](https://moha.linica.jp/blog/archives/7110)
- [東アジアの文字幅 - Wikipedia](https://ja.wikipedia.org/wiki/%E6%9D%B1%E3%82%A2%E3%82%B8%E3%82%A2%E3%81%AE%E6%96%87%E5%AD%97%E5%B9%85)

## 全角と半角について
まずは、日本語処理として、全角と半角にまつわるトピックスを記載します。

### 等幅フォントと文字幅

仕事としてソフトウェア開発に携わり始めた頃、フォントはビットマップで等幅フォント（固定幅フォント）が当たり前でした。
等幅フォントでは、漢字（JIS X0208）フォントサイズは正方形で、JIS X0201 - ASCII/カタカナは漢字に対して横幅が半分にデザインされています。
このように横幅が「２：１」となっているので、漢字を全角、JIS X0201 - ASCII/カタカナを半角と呼んでいます。

この横幅に基づく指標を文字幅と呼びます。

```
1234
→ 文字数４、文字幅４
第12版
→ 文字数４、文字幅６
```

::: note
等幅フォントにおいて、全角と半角の比率は「２：１」 固定と思われがちですが、フォントファイル依存で「Source Han Code JP」等幅フォントなどでは、全角２文字に対して半角３文字「３：２」となっています。
:::


### 印刷フォーム

印刷フォームを設計する上で、等幅フォントを用いた文字幅から、印字領域をキッチリ設定することがあります。
このようなケースでは、領域に対する許容量は、文字数ではなく、半角全角に基づいた文字幅で決められます。
A4、B4 と限られた印字領域を効率利用するために、現在でも、文字幅に基づいた印刷フォームは存在します。

ソフトウェアでの表示領域も、以前は、文字幅ベースで設計することがありましたが、プロポーショナルフォント利用が一般的になったこともあり、現在は、文字数で領域設定することが多いと思います。
ソフトウェアでの表示は、スクロール、タブなどさまざまな手法で、表示領域を広げることができるので、印刷フォームのように文字幅でキッチリ配置する必要性が低いからですかね。

### 半角カタカナ

Windows 98、Windows 2000 の頃までは、少ない文字幅で多くの文字を表示可能ということもあり、ソフトウェアでの項目表示として半角カタカナを利用しているケースが多々見られました。

![win98_startmenu1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/0021a12b-d0c2-566a-bdcf-3ee4b127a9fa.png)


現在は、フリガナ入力で入力可能文字を制限する局面で半角カタカナ入力を行うケースがありますが、基本的には、半角カタカナは利用しないことが一般的だと思います。

出版 / 印刷業界では、半角カタカナは利用しないのが一般的とのことです。


### 半角英数字

ソフトウェアでの表示項目では、英数字は半角を利用することが多いと思います。

出版 / 印刷業界では、２つの流派があるみたいです。

- 英数字は全て半角とする
  - データとしての統一を考えると、このパターン 
- １文字の場合は全角、２文字以上は半角とする
  - 縦書きなどを考えると、１文字の場合、全角とするとレイアウトが整う 

### 半角記号

JIS X0201 - ASCII 記号として存在する文字は、全角半角どちらが望ましいかというのは、さまざまなケースがあるので、一律に決めることは難しそうです。
ひとつのコンテンツで、全角、もしくは、半角、どちらかに統一はすべきです。


## Unicode

### East Asian Width

Unicode の基本概念「表意文字」に基づくと、全角英字「Ａ」と半角英字「A」は同一視対象です。
しかしながら、Unicode 以外の日本語文字コードでは、これらはあきらかに区別され、文字幅という属性値は大きな意味を持っています。

Unicode と Unicode 以外の日本語文字コードとのデータ交換を考える上でも、可逆性を維持するためには、全角英字「Ａ」と半角英字「A」を別コードにする必要があります。

このような理由で、Unicode でも、全角英字「Ａ」と半角英字「A」は別コードとなりました。


文字幅については、East Asian Width という附属書で定められています。

https://www.unicode.org/reports/tr11/

- F（Fullwidh［全角］）
  - 全角英数（全角「Ａ」）
- H（Halfwidth［半角］）
  - 半角カタカナ（半角「ｱ」）
- W（Wide［広］）
  - 全角漢字/仮名文字など（全角「ア」）
- Na（Narrow［狭］）
  - 上記以外で、対応するいわゆる全角の文字が存在したもの（半角英数「A」）
- A（Ambiguous［曖昧］）
  - 文脈によって文字幅が異なる文字（ギリシャ文字/キリル文字など）
- N（Neutral［中立］）
  - 上記のいずれにも属さない文字（アラビア文字など）

Unicode のテキストを東アジアとして扱う場合は、下記の対処となります。
- 全角：F / W / A 
- 半角：H / Na / N


### 厄介事

C# 標準クラスでは、East Asian Width に関する API は用意されていません。

::: note
Android では、[UCharacter.EastAsianWidth](https://developer.android.com/reference/android/icu/lang/UCharacter.EastAsianWidth) という API が存在します。
.NET for Android API でも、[UCharacter.EastAsianWidth Class](https://learn.microsoft.com/ja-jp/dotnet/api/android.icu.lang.ucharacter.eastasianwidth) が用意されています。
:::

NuGet で East Asian Width 関係を探してみたところ下記がありました。

- [NuGet Gallery | EastAsianWidthDotNet](https://www.nuget.org/packages/EastAsianWidthDotNet)
  - 対応フレームワーク：.NET Framework / .NET  
- [NuGet Gallery | EastAsianWidth ](https://www.nuget.org/packages/EastAsianWidth)
  - 対応フレームワーク：.NET 

.NET Framework / .NET で利用できる EastAsianWidthDotNet の振舞いを確認してみます。

```
PM> NuGet\Install-Package EastAsianWidthDotNet
```

```
using EastAsianWidthDotNet;
```

```CSharp
string hoge1 = "第10版";
string hoge2 = "\u30DB\u309A";        // 合成文字：ホ + 半濁音結合文字
string hoge3 = "\uD842\uDF9F";        // サロゲートペア：叱
string hoge4 = "葛\U000E0100";        // 異体字セレクタ：葛󠄀

var w1 = hoge1.GetWidth();            // → 6
var w2 = hoge2.GetWitdh();            // → 4
var w3 = hoge3.GetWitdh();            // → 2
var w4 = hoge4.GetWitdh();            // → 4
```

「サロゲートペア」は正しく判断できているようですが、「合成文字」「異体字セレクタ」は意図した文字幅が取得できませんでした。

日本語処理に限定すると、全角半角に基づく文字幅は、East Asian Width を網羅する必要はないので、下記のようなアプローチで、処理しても良いかなと思います。


```
日本語処理を行う文字列の全角半角文字幅
→（文字数）✕ ２ ―（JIS X0201 - ASCII/カタカナ文字数）
```

ソースコードとすると、下記のような感じですかね。

```SCharp
private int JapaneseZenHanWidth(string target)
{
  int leng = target?.Length ?? 0;
  if (leng > 0)
  {
    var si = new System.Globalization.StringInfo(target);
    int count = target.ToCharArray().Where(
                  c => (c < '\u007E' || c == '\u203E'
                  || ('\uFF61' <= c && c <= '\uFF9F'))).Count();  
    return ((si.LengthInTextElements * 2) - count);
  }
  return (0);
}
```

```CSharp
string hoge1 = "第10版";
string hoge2 = "\u30DB\u309A";        // 合成文字：ホ + 半濁音結合文字
string hoge3 = "\uD842\uDF9F";        // サロゲートペア：叱
string hoge4 = "葛\U000E0100";        // 異体字セレクタ：葛󠄀

int w1 = JapaneseZenHanWidth(hoge1);  // → 6
int w2 = JapaneseZenHanWidth(hoge2);  // → 2
int w3 = JapaneseZenHanWidth(hoge3);  // → 2
int w4 = JapaneseZenHanWidth(hoge4);  // → 2
```