Javaで英数記号・ひらがな・カタカナ・JIS第一／第二水準漢字以外をエラーにするチェック処理を実装してみた

# 要件
- 半角英数記号・全角英数記号・全角カタカナ・全角ひらがな・漢字（第一水準・第二水準）を許容して、それ以外はエラー。
- 文字コードはShift-JIS(MS932)。
- 実行環境はJava SE 8u152（たぶんJava8以下でも動くはず。）

色々と試行錯誤したが、とりあえず期待した動作をするようになった。

# ソース
簡単に説明すると、
1. チェック対象文字列をchar型配列に変換
2. いずれかのif~else ifに該当した場合、continueで次文字チェックへ
3. 該当しなかった場合、true(エラーあり)を返す。
4. 最後の文字までチェックして問題なければ、false(エラーなし)を返す。

```java
    /**
     * 第2水準の文字種チェック<br />
     * 半角英数記号・全角英数記号・全角カタカナ・全角ひらがな・漢字（第一水準・第二水準）は許容する。
     *
     * @param str チェック対象文字列
     * @return true:エラーあり/false:エラーなし
     */
    public static boolean checkForbiddenCharacter(String str) {
        char[] charArray = str.toCharArray();
        for (char c : charArray) {
            // JISコードの配列で取得
            int targetChar = getSJISByte(c);

            // 半角文字OK(『?』以外)
            if ((0x20 <= targetChar && targetChar <= 0x3D) || (0x40 <= targetChar && targetChar <= 0x7E)) {
                continue;
            }
            // 半角文字『?』について
            else if (targetChar == 0x3F) {
                // 入力文字が『?』であればOK
                if ("?".equals(String.valueOf(c))) {
                    continue;
                }
                // NG
                else {
                   return true;
                }
            }
            // 半角カナOK
            else if (0xA1 <= targetChar && targetChar <= 0xDF) {
                continue;
            }
            // 全角非漢字OK
            else if (0x8140 <= targetChar && targetChar <= 0x84BE) {
                continue;
            }
            // 第一水準OK
            else if (0x889F <= targetChar && targetChar <= 0x9872) {
                continue;
            }
            // 第二水準OK
            else if ((0x989F <= targetChar && targetChar <= 0x9FFC) || (0xE040 <= targetChar && targetChar <= 0xEAA4)) {
                continue;
            }
            // NG
            else {
                return true;
            }
        }
        return false;
    }
```

```java
    /**
     * Shift-JISでバイト変換
     *
     * @param c 変換対象文字
     * @return 変換後文字列
     */
    private static int getSJISByte(char c) {
        int targetChar = 0;
        try {
            // MS932でバイト変換
            byte[] byteArray = String.valueOf(c).getBytes("MS932");
            if (byteArray.length <= 1) {
                // 半角文字
                targetChar = (byteArray[0] & 0xFF);
            } else {
                // 全角文字
                targetChar = (((byteArray[0] & 0xFF) * 0x100) + (byteArray[1] & 0xFF));
            }
        } catch (Exception e) {
            ;
        }
        return targetChar;
    }

```

## 個人的に良いと思っている点
他のプロジェクトとかに横展開しやすい。
全角カナはエラーにしたいなどと思ったときに、該当するelse~ifを削除するだけで済む。

## 改善したい点
行数が長い。
もっと行数を短くできるはず。else ifが多すぎ。
すごく長い文字列をチェックすると、1文字ずつ文字チェックしているためレスポンスが悪い。

# 最後に
こうすればもっと綺麗なコードになる。ここにバグがある。などありましたら誰か教えてください。

# 2018年5月6日追記
コメントにて修正案を教えていただきました。
注意点ですが、Stream APIを使用しているので、Java 8以降でしか動作しないです。

```java
    /**
     * strが全てcharTypesの文字で構成されているか
     *
     * @param str チェック対象文字列
     * @param charTypes 許容する文字
     * @return true:エラーあり/false:エラーなし
     */
    public static boolean isConsistingWithCharSet(String str, CharType... charTypes) {
        for (char c : str.toCharArray()) {
            // JISコードの配列で取得
            int targetChar = getSJISByte(c);

            // 対象の文字が与えられた文字種でなければfalse
            if(Arrays.stream(charTypes).noneMatch(p -> p.contains(targetChar))) {
                return false;
            }
        }
        return true;
    }

    enum CharType {
        // 半角英数記号
        HALFWIDTH_ALPHANUMERICSYMBOL (p -> ((0x20 <= p && p <= 0x3D) || (0x40 <= p && p <= 0x7E)) || (p == 0x3F && "?".equals(String.valueOf((char)p)))),

        // 半角カタカナ
        HALFWIDTH_KATAKANA (p -> 0xA1 <= p && p <= 0xDF),

        // 全角第一水準漢字
        FULLWIDTH_FIRSTLEVEL_KANJI (p -> 0x889F <= p && p <= 0x9872),

        // 全角第二水準漢字
        FULLWIDTH_SECONDLEVEL_KANJI (p -> (0x989F <= p && p <= 0x9FFC) || (0xE040 <= p && p <= 0xEAA4)),

        // 全角その他
        FULLWIDTH_OTHERS (p -> 0x8140 <= p && p <= 0x84BE);

        private final IntPredicate predicate;

        private CharType(IntPredicate predicate) {
            this.predicate = predicate;
        }

        public boolean contains(int num) {
            return predicate.test(num);
        }
    }
}
```

enumを使用しているため、引数で指定すれば、半角カタカナのみで構成されているかどうかなどの確認が簡単。使い方はこちら。

```java
    public static void main(String args[]) {
        // true
        System.out.println(isConsistingWithCharSet("あいうえお", CharType.values()));
        // true
        System.out.println(isConsistingWithCharSet("一二三四五", CharType.FULLWIDTH_FIRSTLEVEL_KANJI));
        // false
        System.out.println(isConsistingWithCharSet("abcde", CharType.FULLWIDTH_OTHERS));
    }
```

また、Unicodeコードポイントのマッピングが一部異なるので、MS932ではなくてShift-JISでコード変換した方が良いとのことでした。

```java
    /**
     * Shift-JISでバイト変換
     *
     * @param c 変換対象文字
     * @return 変換後文字列
     */
    private static int getSJISByte(char c) {
        int targetChar = 0;
        try {
            // MS932でバイト変換
            byte[] byteArray = String.valueOf(c).getBytes("Shift-JIS");
            if (byteArray.length <= 1) {
                // 半角文字
                targetChar = (byteArray[0] & 0xFF);
            } else {
                // 全角文字
                targetChar = (((byteArray[0] & 0xFF) * 0x100) + (byteArray[1] & 0xFF));
            }
        } catch (Exception e) {
            ;
        }
        return targetChar;
    }
```
かなり行数が短くなりました。コメントくださった方々ありがとうございました。


# 参考文献
コードに関して、下記のURLを参考にさせていただきました。
http://www.shuiren.org/chuden/teach/code/main4.htm
http://www.asahi-net.or.jp/~ax2s-kmtn/ref/jisx0208.html