Java で全角・半角・どちらでもないを判定する方法 

Windows-31J でエンコードしたときのバイト数を基準として全角・半角を判定する方法を紹介します。エンコードできない場合は半角でも全角でもないものとします。

## 実装

以下のメソッドは、入力が半角文字だと1、全角文字だと2を返し、どちらでもない場合には例外 CharacterCodingException を投げます。

```java
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;

public class Windows31JWidthChecker {
    private static final Charset WINDOWS31J = Charset.forName("windows-31j");

    /**
     * Windows-31J での文字幅を返す。
     * @return 半角文字は 1、全角文字は 2。
     * @throws CharacterCodingException Windows-31J で表せない文字だった場合。
     */
    public static int widthInWindows31J(char c) throws CharacterCodingException {
        CharBuffer charBuffer = CharBuffer.allocate(1).put(c);
        charBuffer.flip();
        ByteBuffer byteBuffer = WINDOWS31J.newEncoder().encode(charBuffer);
        return byteBuffer.limit();
    }
}
```

## 動作確認

以下のコードで動作確認してみます。

```java
import java.nio.charset.CharacterCodingException;

class Windows31JWidthCheckerTest {
    public static void main(String[] args) {
        char[] chars = {
                'a', 'ａ', 'β', '○', 'ｶ', 'が', 'か', '゙',
                '茶', '茶', '丳', '㍍', '㌀', 'क', ''
        };
        for (char c : chars) {
            String result;
            try {
                int width = Windows31JWidthChecker.widthInWindows31J(c);
                if (width == 1) {
                    result = "半角";
                } else if (width == 2) {
                    result = "全角";
                } else {
                    throw new IllegalStateException("logic error!");
                }
            } catch (CharacterCodingException e) {
                result = "どちらでもない";
            }
            System.out.printf("「%s」(U+%04X) は%s%n", c, (int) c, result);
        }
    }
}
```

実行結果は次のようになります。

```
「a」(U+0061) は半角
「ａ」(U+FF41) は全角
「β」(U+03B2) は全角
「○」(U+25CB) は全角
「ｶ」(U+FF76) は半角
「が」(U+304C) は全角
「か」(U+304B) は全角
「゙」(U+3099) はどちらでもない
「茶」(U+8336) は全角
「茶」(U+F9FE) はどちらでもない
「丳」(U+4E33) はどちらでもない
「㍍」(U+334D) は全角
「㌀」(U+3300) はどちらでもない
「क」(U+0915) はどちらでもない
「」(U+E000) は全角
```

うまく動作していますね。

U+E000 が全角と判定されるのは気持ち悪い感じもしますが、Unicode の私用領域のうち、U+E000 などはシフト JIS のユーザー外字領域にマッピングするものなので、これが期待通りの結果です。

https://ja.wikipedia.org/wiki/%E5%A4%96%E5%AD%97#JIS_X_0221_(Unicode)%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%A4%96%E5%AD%97


## String.getBytes("windows-31j") でバイト数を数えたほうが簡単じゃない？

これは良くない方法だと思います。変換できない文字が "?" に置き換えられてしまうため、誤って半角と判定してしまいます。例えば「㌀」('\u3300') の幅を1 (半角) と誤判定してしまいます。

```
jshell> "\u3300".getBytes("windows-31j").length
$1 ==> 1
```

CharsetEncoder を用いると、デフォルトでこうした場合にエラーを報告してくれます。

https://docs.oracle.com/javase/jp/18/docs/api/java.base/java/nio/charset/CharsetEncoder.html


## サロゲート ペアに対応しなくて平気？

CP932 は Windows-31J の別名ですが、下記変換表を見ると char で表現できる U+FFFF 以下の文字との対応関係として定義されているので、特別な対応は必要ありません。サロゲート ペアになる場合には「どちらでもない」という結果になります。

https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT

念のため以下のように、サロゲート レンジの char で必ず「どちらでもない」を表す例外が出ることも確認しました。

```java
import org.junit.jupiter.api.Test;

import java.nio.charset.CharacterCodingException;

import static org.junit.jupiter.api.Assertions.assertThrows;

class SecondWindows31JWidthCheckerTest {
    @Test
    void shouldThrowExceptionForSurrogate() throws CharacterCodingException {
        for (char c = '\ud800'; c <= '\udfff'; c++) {
            char finalC = c;
            assertThrows(CharacterCodingException.class, () -> Windows31JWidthChecker.widthInWindows31J(finalC));
        }
    }
}
```