package core.util;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * 文字関連ユーティリティ
 *
 * @author Tadashi Nakayama
 */
public final class MojiUtil {

	/** エンコード */
	public static final Charset CHARSET_JIS = Charset.forName("iso-2022-jp");
	/** エンコード */
	public static final Charset CHARSET_X_JIS = Charset.forName("x-windows-iso2022jp");
	/** エンコード */
	public static final Charset CHARSET_W31J = Charset.forName("Windows-31J");
	/** エンコード */
	public static final Charset CHARSET_SJIS = Charset.forName("SJIS");
	/** エンコード */
	public static final Charset CHARSET_EUC = Charset.forName("euc-jp");
	/** エンコード */
	public static final Charset CHARSET_X_SJIS = Charset.forName("x-SJIS_0213");

	/** 改行コード */
	public static final String RET_CODE_CR = "\r";
	/** 改行コード */
	public static final String RET_CODE_LF = "\n";
	/** 改行コード */
	public static final String RET_CODE_CRLF = "\r\n";

	/** エンコード */
	private static final Charset CHARSET_SHIFT_JIS = Charset.forName("Shift_JIS");

	/** UTF-8用 BOM */
	private static final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};

	/** 半角文字をキーにしたHashMap */
	private static final Map<Integer, String> H2F_TABLE;
	/** 全角文字をキーにしたHashMap */
	private static final Map<Integer, String> F2H_TABLE;

	/**
	 * コンストラクタ
	 */
	private MojiUtil() {
		throw new AssertionError();
	}

	/**
	 * 数字変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setNumber(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('１'), "1");
		f2h.put(Integer.valueOf('２'), "2");
		f2h.put(Integer.valueOf('３'), "3");
		f2h.put(Integer.valueOf('４'), "4");
		f2h.put(Integer.valueOf('５'), "5");
		f2h.put(Integer.valueOf('６'), "6");
		f2h.put(Integer.valueOf('７'), "7");
		f2h.put(Integer.valueOf('８'), "8");
		f2h.put(Integer.valueOf('９'), "9");
		f2h.put(Integer.valueOf('０'), "0");
	}

	/**
	 * アルファベット変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setBigAlphabet(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('Ａ'), "A");
		f2h.put(Integer.valueOf('Ｂ'), "B");
		f2h.put(Integer.valueOf('Ｃ'), "C");
		f2h.put(Integer.valueOf('Ｄ'), "D");
		f2h.put(Integer.valueOf('Ｅ'), "E");
		f2h.put(Integer.valueOf('Ｆ'), "F");
		f2h.put(Integer.valueOf('Ｇ'), "G");
		f2h.put(Integer.valueOf('Ｈ'), "H");
		f2h.put(Integer.valueOf('Ｉ'), "I");
		f2h.put(Integer.valueOf('Ｊ'), "J");
		f2h.put(Integer.valueOf('Ｋ'), "K");
		f2h.put(Integer.valueOf('Ｌ'), "L");
		f2h.put(Integer.valueOf('Ｍ'), "M");
		f2h.put(Integer.valueOf('Ｎ'), "N");
		f2h.put(Integer.valueOf('Ｏ'), "O");
		f2h.put(Integer.valueOf('Ｐ'), "P");
		f2h.put(Integer.valueOf('Ｑ'), "Q");
		f2h.put(Integer.valueOf('Ｒ'), "R");
		f2h.put(Integer.valueOf('Ｓ'), "S");
		f2h.put(Integer.valueOf('Ｔ'), "T");
		f2h.put(Integer.valueOf('Ｕ'), "U");
		f2h.put(Integer.valueOf('Ｖ'), "V");
		f2h.put(Integer.valueOf('Ｗ'), "W");
		f2h.put(Integer.valueOf('Ｘ'), "X");
		f2h.put(Integer.valueOf('Ｙ'), "Y");
		f2h.put(Integer.valueOf('Ｚ'), "Z");
	}

	/**
	 * アルファベット変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setSmallAlphabet(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('ａ'), "a");
		f2h.put(Integer.valueOf('ｂ'), "b");
		f2h.put(Integer.valueOf('ｃ'), "c");
		f2h.put(Integer.valueOf('ｄ'), "d");
		f2h.put(Integer.valueOf('ｅ'), "e");
		f2h.put(Integer.valueOf('ｆ'), "f");
		f2h.put(Integer.valueOf('ｇ'), "g");
		f2h.put(Integer.valueOf('ｈ'), "h");
		f2h.put(Integer.valueOf('ｉ'), "i");
		f2h.put(Integer.valueOf('ｊ'), "j");
		f2h.put(Integer.valueOf('ｋ'), "k");
		f2h.put(Integer.valueOf('ｌ'), "l");
		f2h.put(Integer.valueOf('ｍ'), "m");
		f2h.put(Integer.valueOf('ｎ'), "n");
		f2h.put(Integer.valueOf('ｏ'), "o");
		f2h.put(Integer.valueOf('ｐ'), "p");
		f2h.put(Integer.valueOf('ｑ'), "q");
		f2h.put(Integer.valueOf('ｒ'), "r");
		f2h.put(Integer.valueOf('ｓ'), "s");
		f2h.put(Integer.valueOf('ｔ'), "t");
		f2h.put(Integer.valueOf('ｕ'), "u");
		f2h.put(Integer.valueOf('ｖ'), "v");
		f2h.put(Integer.valueOf('ｗ'), "w");
		f2h.put(Integer.valueOf('ｘ'), "x");
		f2h.put(Integer.valueOf('ｙ'), "y");
		f2h.put(Integer.valueOf('ｚ'), "z");
	}

	/**
	 * カタカナ変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setBigKatakana1(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('ア'), "ｱ");
		f2h.put(Integer.valueOf('イ'), "ｲ");
		f2h.put(Integer.valueOf('ウ'), "ｳ");
		f2h.put(Integer.valueOf('エ'), "ｴ");
		f2h.put(Integer.valueOf('オ'), "ｵ");
		f2h.put(Integer.valueOf('カ'), "ｶ");
		f2h.put(Integer.valueOf('キ'), "ｷ");
		f2h.put(Integer.valueOf('ク'), "ｸ");
		f2h.put(Integer.valueOf('ケ'), "ｹ");
		f2h.put(Integer.valueOf('コ'), "ｺ");
		f2h.put(Integer.valueOf('サ'), "ｻ");
		f2h.put(Integer.valueOf('シ'), "ｼ");
		f2h.put(Integer.valueOf('ス'), "ｽ");
		f2h.put(Integer.valueOf('セ'), "ｾ");
		f2h.put(Integer.valueOf('ソ'), "ｿ");
		f2h.put(Integer.valueOf('タ'), "ﾀ");
		f2h.put(Integer.valueOf('チ'), "ﾁ");
		f2h.put(Integer.valueOf('ツ'), "ﾂ");
		f2h.put(Integer.valueOf('テ'), "ﾃ");
		f2h.put(Integer.valueOf('ト'), "ﾄ");
		f2h.put(Integer.valueOf('ナ'), "ﾅ");
		f2h.put(Integer.valueOf('ニ'), "ﾆ");
		f2h.put(Integer.valueOf('ヌ'), "ﾇ");
		f2h.put(Integer.valueOf('ネ'), "ﾈ");
		f2h.put(Integer.valueOf('ノ'), "ﾉ");
	}

	/**
	 * カタカナ変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setBigKatakana2(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('ハ'), "ﾊ");
		f2h.put(Integer.valueOf('ヒ'), "ﾋ");
		f2h.put(Integer.valueOf('フ'), "ﾌ");
		f2h.put(Integer.valueOf('ヘ'), "ﾍ");
		f2h.put(Integer.valueOf('ホ'), "ﾎ");
		f2h.put(Integer.valueOf('マ'), "ﾏ");
		f2h.put(Integer.valueOf('ミ'), "ﾐ");
		f2h.put(Integer.valueOf('ム'), "ﾑ");
		f2h.put(Integer.valueOf('メ'), "ﾒ");
		f2h.put(Integer.valueOf('モ'), "ﾓ");
		f2h.put(Integer.valueOf('ヤ'), "ﾔ");
		f2h.put(Integer.valueOf('ユ'), "ﾕ");
		f2h.put(Integer.valueOf('ヨ'), "ﾖ");
		f2h.put(Integer.valueOf('ラ'), "ﾗ");
		f2h.put(Integer.valueOf('リ'), "ﾘ");
		f2h.put(Integer.valueOf('ル'), "ﾙ");
		f2h.put(Integer.valueOf('レ'), "ﾚ");
		f2h.put(Integer.valueOf('ロ'), "ﾛ");
		f2h.put(Integer.valueOf('ワ'), "ﾜ");
		f2h.put(Integer.valueOf('ヲ'), "ｦ");
		f2h.put(Integer.valueOf('ン'), "ﾝ");
	}

	/**
	 * カタカナ変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setSmallKatakana1(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('ァ'), "ｧ");
		f2h.put(Integer.valueOf('ィ'), "ｨ");
		f2h.put(Integer.valueOf('ゥ'), "ｩ");
		f2h.put(Integer.valueOf('ェ'), "ｪ");
		f2h.put(Integer.valueOf('ォ'), "ｫ");
		f2h.put(Integer.valueOf('ッ'), "ｯ");
		f2h.put(Integer.valueOf('ャ'), "ｬ");
		f2h.put(Integer.valueOf('ュ'), "ｭ");
		f2h.put(Integer.valueOf('ョ'), "ｮ");
	}

	/**
	 * 記号変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setSymbol1(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('゛'), "ﾞ");
		f2h.put(Integer.valueOf('゜'), "ﾟ");
		f2h.put(Integer.valueOf('、'), "､");
		f2h.put(Integer.valueOf('。'), "｡");
		f2h.put(Integer.valueOf('・'), "･");
		f2h.put(Integer.valueOf('ー'), "ｰ");
		f2h.put(Integer.valueOf('「'), "｢");
		f2h.put(Integer.valueOf('」'), "｣");
		f2h.put(Integer.valueOf('！'), "!");
		f2h.put(Integer.valueOf('”'), "\"");
		f2h.put(Integer.valueOf('＃'), "#");
		f2h.put(Integer.valueOf('＄'), "$");
		f2h.put(Integer.valueOf('％'), "%");
		f2h.put(Integer.valueOf('＆'), "&");
		f2h.put(Integer.valueOf('’'), "'");
		f2h.put(Integer.valueOf('（'), "(");
		f2h.put(Integer.valueOf('）'), ")");
		f2h.put(Integer.valueOf('＊'), "*");
		f2h.put(Integer.valueOf('＋'), "+");
		f2h.put(Integer.valueOf('，'), ",");
	}

	/**
	 * 記号変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setSymbol2(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('．'), ".");
		f2h.put(Integer.valueOf('／'), "/");
		f2h.put(Integer.valueOf('；'), ";");
		f2h.put(Integer.valueOf('：'), ":");
		f2h.put(Integer.valueOf('＜'), "<");
		f2h.put(Integer.valueOf('＝'), "=");
		f2h.put(Integer.valueOf('＞'), ">");
		f2h.put(Integer.valueOf('？'), "?");
		f2h.put(Integer.valueOf('＠'), "@");
		f2h.put(Integer.valueOf('［'), "[");
		f2h.put(Integer.valueOf('￥'), "\\");
		f2h.put(Integer.valueOf('］'), "]");
		f2h.put(Integer.valueOf('＾'), "^");
		f2h.put(Integer.valueOf('＿'), "_");
		f2h.put(Integer.valueOf('｀'), "`");
		f2h.put(Integer.valueOf('｛'), "{");
		f2h.put(Integer.valueOf('｜'), "|");
		f2h.put(Integer.valueOf('｝'), "}");
		f2h.put(Integer.valueOf('　'), " ");
	}

	/**
	 * カタカナ変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setBigKatakana3(final Map<Integer, String> f2h) {
		// 全角→半角 用テーブル
		f2h.put(Integer.valueOf('ヴ'), "ｳﾞ");
		f2h.put(Integer.valueOf('ガ'), "ｶﾞ");
		f2h.put(Integer.valueOf('ギ'), "ｷﾞ");
		f2h.put(Integer.valueOf('グ'), "ｸﾞ");
		f2h.put(Integer.valueOf('ゲ'), "ｹﾞ");
		f2h.put(Integer.valueOf('ゴ'), "ｺﾞ");
		f2h.put(Integer.valueOf('ザ'), "ｻﾞ");
		f2h.put(Integer.valueOf('ジ'), "ｼﾞ");
		f2h.put(Integer.valueOf('ズ'), "ｽﾞ");
		f2h.put(Integer.valueOf('ゼ'), "ｾﾞ");
		f2h.put(Integer.valueOf('ゾ'), "ｿﾞ");
		f2h.put(Integer.valueOf('ダ'), "ﾀﾞ");
		f2h.put(Integer.valueOf('ヂ'), "ﾁﾞ");
		f2h.put(Integer.valueOf('ヅ'), "ﾂﾞ");
		f2h.put(Integer.valueOf('デ'), "ﾃﾞ");
		f2h.put(Integer.valueOf('ド'), "ﾄﾞ");
		f2h.put(Integer.valueOf('バ'), "ﾊﾞ");
		f2h.put(Integer.valueOf('ブ'), "ﾌﾞ");
		f2h.put(Integer.valueOf('ビ'), "ﾋﾞ");
		f2h.put(Integer.valueOf('ベ'), "ﾍﾞ");
		f2h.put(Integer.valueOf('ボ'), "ﾎﾞ");
		f2h.put(Integer.valueOf('パ'), "ﾊﾟ");
		f2h.put(Integer.valueOf('ピ'), "ﾋﾟ");
		f2h.put(Integer.valueOf('プ'), "ﾌﾟ");
		f2h.put(Integer.valueOf('ペ'), "ﾍﾟ");
		f2h.put(Integer.valueOf('ポ'), "ﾎﾟ");
	}

	/**
	 * カタカナ変換テーブル設定
	 *
	 * @param f2h テーブルマップ
	 */
	private static void setSmallKatakana2(final Map<Integer, String> f2h) {
		f2h.put(Integer.valueOf('ヵ'), "ｶ");
		f2h.put(Integer.valueOf('ヶ'), "ｹ");
		f2h.put(Integer.valueOf('ヮ'), "ﾜ");
		f2h.put(Integer.valueOf('ヰ'), "ｲ");
		f2h.put(Integer.valueOf('ヱ'), "ｴ");
		f2h.put(Integer.valueOf('ゎ'), "ﾜ");
		f2h.put(Integer.valueOf('ゐ'), "ｲ");
		f2h.put(Integer.valueOf('ゑ'), "ｴ");
		f2h.put(Integer.valueOf('\u2212'), "-");
		f2h.put(Integer.valueOf('\uff0d'), "-");
	}

	static {
		// 全角→半角 用テーブル
		final var f2h = new HashMap<Integer, String>();
		setNumber(f2h);
		setBigAlphabet(f2h);
		setSmallAlphabet(f2h);
		setBigKatakana1(f2h);
		setBigKatakana2(f2h);
		setSmallKatakana1(f2h);
		setSymbol1(f2h);
		setSymbol2(f2h);

		// 半角→全角 用テーブル
		final var h2f = new HashMap<Integer, String>();
		for (final var me : f2h.entrySet()) {
			final var key = String.valueOf(Character.toChars(me.getKey()));
			h2f.put(Integer.valueOf(me.getValue().codePointAt(0)), key);
		}
		H2F_TABLE = Collections.unmodifiableMap(h2f);

		setBigKatakana3(f2h);
		setSmallKatakana2(f2h);

		F2H_TABLE = Collections.unmodifiableMap(f2h);
	}

	/**
	 * UTF-8BOM値取得
	 *
	 * @return UTF-8BOM値
	 */
	public static byte[] utf8Bom() {
		return UTF8_BOM.clone();
	}

	/**
	 * 対象の文字列内の変換できる文字だけ半角に変換する。
	 *
	 * @param item 対象文字列
	 * @param space true:スペースも変換する。false:スペースは変換しない。
	 * @param kana true:カナも変換する。false:カナは変換しない。
	 * @return 半角に変換した文字列
	 */
	public static String toHalfCase(final String item,
			final boolean space, final boolean kana) {
		final Function<Integer, String> half = c -> {
			String str = null;
			if (c != '　' || space) {
				str = F2H_TABLE.get(c);
				if (str != null && !kana) {
					final var cp = str.codePointAt(0);
					if (0xFF61 <= cp && cp <= 0xFF9F) {
						str = null;
					}
				}
			}
			return str;
		};

		StringBuilder newStr = null;
		for (var i = 0; item != null && i < item.length(); i = item.offsetByCodePoints(i, 1)) {
			final var c = item.codePointAt(i);
			newStr = addBuilder(newStr, item, i, half.apply(c), c);
		}

		return Objects.toString(newStr, Objects.toString(item, ""));
	}

	/**
	 * 対象の文字列内の変換できる文字だけを全角に変換する。
	 *
	 * @param item 対象文字列
	 * @param space true:スペースも変換する。false:スペースは変換しない。
	 * @return 全角に変換した文字列
	 */
	public static String toFullCase(final String item, final boolean space) {
		return toFullCase(item, space, CHARSET_W31J);
	}

	/**
	 * 対象の文字列内の変換できる文字だけを全角に変換する。
	 *
	 * @param item 対象文字列
	 * @param space true:スペースも変換する。false:スペースは変換しない。
	 * @param charset 変換後出力エンコード
	 * @return 全角に変換した文字列
	 */
	public static String toFullCase(final String item, final boolean space, final Charset charset) {
		final String hyphen = String.valueOf(Character.toChars(correctGarbled('\uff0d', charset)));

		final Function<int[], String> toNormal = cp -> {
			if (cp[1] == 'ﾞ' || cp[1] == 'ﾟ') {
				final var ret = Normalizer.normalize(Normalizer.normalize(
								new String(cp, 0, cp.length), Form.NFKD), Form.NFC);
				if (ret.length() == 1) {
					return ret;
				}
			}
			return null;
		};

		StringBuilder newStr = null;
		var i = 0;
		while (item != null && i < item.length()) {
			final var c = item.codePointAt(i);

			var str = H2F_TABLE.get(c);
			var offset = i;

			if (c == '-') {
				str = hyphen;
			} else if (c != ' ' || space) {
				final var n = item.offsetByCodePoints(i, 1);
				if (n < item.length()) {
					final var s = toNormal.apply(new int[]{c, item.codePointAt(n)});
					if (s != null) {
						str = s;
						offset = n;
					}
				}
			}

			newStr = addBuilder(newStr, item, i, str, c);

			i = item.offsetByCodePoints(offset, 1);
		}

		return Objects.toString(newStr, Objects.toString(item, ""));
	}

	/**
	 * 文字列付加
	 *
	 * @param newStr 文字列バッファ
	 * @param item 元文字列
	 * @param i 位置
	 * @param str 付加文字列
	 * @param c コードポイント
	 * @return 文字列バッファ
	 */
	public static StringBuilder addBuilder(final StringBuilder newStr, final String item,
			final int i, final String str, final int c) {
		if (str != null) {
			if (newStr == null) {
				final var sb = new StringBuilder(item.substring(0, i));
				sb.append(str);
				return sb;
			}
			newStr.append(str);
		} else {
			if (newStr != null) {
				newStr.appendCodePoint(c);
			}
		}
		return newStr;
	}

	/**
	 * 文字化け対応(SJIS <-> Windows-31J)
	 *
	 * @param str 文字列
	 * @param charset 変換後出力エンコード
	 * @return 変換後文字列
	 */
	public static String correctGarbled(final String str, final Charset charset) {
		StringBuilder sb = null;
		if (charset != null && !isUtf(charset)) {
			for (var i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
				final var cp = str.codePointAt(i);
				final var ch = correctGarbled(cp, charset);
				if (cp != ch) {
					sb = addBuilder(sb, str, i, String.valueOf(Character.toChars(ch)), cp);
				}
			}
		}
		return Objects.toString(sb, str);
	}

	/**
	 * 文字化け対応(SJIS <-> Windows-31J)
	 *
	 * @param ch キャラクタ
	 * @param charset 変換後出力エンコード
	 * @return 変換後キャラクタ
	 */
	public static int correctGarbled(final int ch, final Charset charset) {

		// Windows-31J ～ ∥ － ￠ ￡ ￢ ―
		if (ch == '\uff5e' || ch == '\u2225' || ch == '\uff0d'
				|| ch == '\uffe0' || ch == '\uffe1' || ch == '\uffe2' || ch == '\u2015') {
			if (CHARSET_EUC.equals(charset)
					|| CHARSET_JIS.equals(charset)
					|| CHARSET_SJIS.equals(charset)
					|| CHARSET_SHIFT_JIS.equals(charset)) {
				final byte[] b = String.valueOf(Character.toChars(ch)).getBytes(CHARSET_W31J);
				return new String(b, CHARSET_SJIS).codePointAt(0);
			}

		// Windows-31J以外
		} else if (ch == '\u301c' || ch == '\u2016' || ch == '\u2212'
				|| ch == '\u00a2' || ch == '\u00a3' || ch == '\u00ac' || ch == '\u2014') {
			if (CHARSET_W31J.equals(charset)) {
				final byte[] b = String.valueOf(Character.toChars(ch)).getBytes(CHARSET_SJIS);
				return new String(b, CHARSET_W31J).codePointAt(0);
			}

		}

		return ch;
	}

	/**
	 * UTF確認
	 *
	 * @param charset エンコード
	 * @return UTFの場合 true を返す。
	 */
	public static boolean isUtf(final Charset charset) {
		return StandardCharsets.UTF_8.equals(charset)
			|| StandardCharsets.UTF_16BE.equals(charset)
			|| StandardCharsets.UTF_16LE.equals(charset);
	}

	/**
	 * 文字列がShift_JIS　第1水準・第2水準かをチェックする。
	 *
	 * @param str 判定対象文字列
	 * @param kana 半角カナを含む場合 true
	 * @param ext ユーザ定義領域を含む場合 true
	 * @return	対応文字列の場合はtrue 対象文字列以外の場合はfalse
	 */
	public static boolean isShiftJIS(final String str, final boolean kana, final boolean ext) {
		return str.codePoints().allMatch(ch -> isShiftJIS(ch, kana, ext));
	}

	/**
	 * Shift_JIS　第1水準・第2水準かをチェックする。
	 *
	 * @param ch 判定対象文字
	 * @param kana 半角カナを含む場合 true
	 * @param ext 機種依存、拡張領域を含む場合 true
	 * @return	対応文字の場合は true を返す。タブ、改行以外の制御文字は、false を返す。
	 */
	public static boolean isShiftJIS(final int ch, final boolean kana, final boolean ext) {

		final var str = String.valueOf(Character.toChars(correctGarbled(ch, CHARSET_W31J)));

		// 半角文字
		final Predicate<byte[]> isLatin = b -> {
			final byte x20b = Integer.valueOf(0x20).byteValue();
			final byte x7Eb = Integer.valueOf(0x7E).byteValue();
			final byte x3Fb = Integer.valueOf(0x3F).byteValue();
			final byte[] u16BE = str.getBytes(StandardCharsets.UTF_16BE);
			return b.length == 1 && x20b <= b[0] && b[0] <= x7Eb
				&& (b[0] != x3Fb || (u16BE.length == 2 && u16BE[0] == 0x00 && u16BE[1] == x3Fb));
		};

		// カナ
		final Predicate<byte[]> isKana = b -> {
			final byte xA1b = Integer.valueOf(0xA1).byteValue();
			final byte xDFb = Integer.valueOf(0xDF).byteValue();
			return b.length == 1 && kana && xA1b <= b[0] && b[0] <= xDFb;
		};

		// 制御文字
		final Predicate<byte[]> isControl = b -> {
			final byte x09b = Integer.valueOf(0x09).byteValue();
			final byte x0Ab = Integer.valueOf(0x0A).byteValue();
			final byte x0Db = Integer.valueOf(0x0D).byteValue();
			return b.length == 1 && (x09b == b[0] || x0Ab == b[0] || x0Db == b[0]);
		};

		// 全角文字
		final Predicate<byte[]> isWide = b -> {
			final byte x87b = Integer.valueOf(0x87).byteValue();
			final byte xEDb = Integer.valueOf(0xED).byteValue();
			return b.length == 2 && (ext || (x87b != b[0] && b[0] < xEDb));
		};

		final var b = str.getBytes(CHARSET_W31J);
		return isLatin.test(b) || isKana.test(b) || isControl.test(b) || isWide.test(b);
	}
}
