/*
 * Copyright (C) 2005-2006 Kouji Sugisawa. All rights reserved.
 */

package jp.sourceforge.livez.lang;

import java.io.UnsupportedEncodingException;

import org.apache.commons.lang.StringUtils;

/**
 * 日本語圏での文字列操作によく使用する機能を提供するユーティリティクラスです。
 * 
 * @author 杉澤 浩二
 */
public final class JaStringUtils {

	/**
	 * 全角スペースを表す。
	 */
	public static final char WIDE_SPACE = '\u3000';


	/**
	 * インスタンス化できない事を強制します。<p>
	 */
	private JaStringUtils() {}

	////////////////////////////////////////////////////////////
	// チェック関連

	/**
	 * 指定された文字列がひらがなであるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 文字列がひらがなだけの場合は true それ以外の場合は false
	 * @see <a href="http://www.unicode.org/charts/PDF/U3040.pdf">Unicode Character Code Charts Hiragana</a>
	 */
	public static boolean isHiragana(final String s) {
		return validate(s, '\u3040', '\u309F');
	}

	/**
	 * 指定された文字列がカタカナであるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 文字列がカタカナだけの場合は true それ以外の場合は false
	 * @see <a href="http://www.unicode.org/charts/PDF/U30A0.pdf">Unicode Character Code Charts Katakana</a>
	 */
	public static boolean isKatakana(final String s) {
		return validate(s, '\u30A0', '\u30FF');
	}

	/**
	 * 指定された文字列がカタカナ表音拡張(小文字のクシストヌハヒフヘホムラリルレロ)であるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 文字列がカタカナ表音拡張だけの場合は true それ以外の場合は false
	 * @see <a href="http://www.unicode.org/charts/PDF/U31F0.pdf">Unicode Character Code Charts Katakana phonetic extensions</a>
	 */
	public static boolean isKatakanaPhoneticExtensions(final String s) {
		return validate(s, '\u31F0', '\u31FF');
	}

	/**
	 * 指定された文字列が半角漢字記号(｡｢｣､)であるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 文字列が半角漢字記号だけの場合は true それ以外の場合は false
	 * @see <a href="http://www.unicode.org/charts/PDF/UFF00.pdf">Unicode Character Code Charts Halfwidth Katakana</a>
	 */
	public static boolean isHalfwidthCJKPunctuation(final String s) {
		return validate(s, '\uFF61', '\uFF64');
	}

	/**
	 * 指定された文字列が半角カタカナであるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 文字列が半角カタカナだけの場合は true それ以外の場合は false
	 * @see <a href="http://www.unicode.org/charts/PDF/UFF00.pdf">Unicode Character Code Charts Halfwidth Katakana</a>
	 */
	public static boolean isHalfwidthKatakana(final String s) {
		return validate(s, '\uFF65', '\uFF9F');
	}

	/**
	 * 指定された文字列が漢字(中国語及び韓国語含む)であるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 文字列が漢字だけの場合は true それ以外の場合は false
	 * @see <a href="http://www.unicode.org/charts/PDF/U4E00.pdf">Unicode Character Code Charts Unified CJK Ideographs</a>
	 */
	public static boolean isCJKUnifiedIdeographs(final String s) {
		return validate(s, '\u4E00', '\u9FFF');
	}

	/**
	 * 指定された文字列の全ての文字が範囲内にあるかどうかを返します。<p>
	 * 文字列に <code>null</code> や空文字列が指定された場合、<code>false</code> を返します。
	 * 
	 * @param s 文字列
	 * @param start 開始
	 * @param end 終了
	 * @return 範囲内にある場合は true。それ以外の場合、false
	 */
	private static boolean validate(final String s, final char start, final char end) {
		if (StringUtils.isEmpty(s))
			return false;

		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);
			if (c < start || c > end)
				return false;
		}

		return true;
	}

	////////////////////////////////////////////////////////////
	// MS 仕様関連

	/**
	 * Microsoft仕様のUnicode文字列をJava仕様のUnicode文字列へ変換して返します。<p>
	 * 〜や−などのコードポイントを修正します。<br>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換された文字列
	 */
	public static String microsoftUnicodeToUnicode(final String s) {
		if (s == null)
			return null;

		StringBuilder result = new StringBuilder(s.length());
		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);

			// −
			if (c == '\uFF0D')
				result.append('\u2212');
			// 〜
			else if (c == '\uFF5E')
				result.append('\u301C');
			// ¢(cent)
			else if (c == '\uFFE0')
				result.append('\u00A2');
			// ‖(doubleverticalline)
			else if (c == '\u2225')
				result.append('\u2016');
			// £(pound)
			else if (c == '\uFFE1')
				result.append('\u00A3');
			// ¬(NOTSIGN)
			else if (c == '\uFFE2')
				result.append('\u00AC');
			// その他
			else
				result.append(c);
		}

		return result.toString();
	}

	/**
	 * Java仕様のUnicode文字列をMicrosoft仕様のUnicode文字列へ変換して返します。<p>
	 * 〜や−などのコードポイントを修正してWindows環境や携帯端末環境で表示可能にします。<br>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換された文字列
	 */
	public static String unicodeToMicrosoftUnicode(final String s) {
		if (s == null)
			return null;

		StringBuilder result = new StringBuilder(s.length());
		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);

			// −
			if (c == '\u2212')
				result.append('\uFF0D');
			// 〜
			else if (c == '\u301C')
				result.append('\uFF5E');
			// ¢(cent)
			else if (c == '\u00A2')
				result.append('\uFFE0');
			// ‖(doubleverticalline)
			else if (c == '\u2016')
				result.append('\u2225');
			// £(pound)
			else if (c == '\u00A3')
				result.append('\uFFE1');
			// ¬(NOTSIGN)
			else if (c == '\u00AC')
				result.append('\uFFE2');
			// その他
			else
				result.append(c);
		}

		return result.toString();
	}

	////////////////////////////////////////////////////////////
	// 長さ/桁数関連

	/**
	 * 指定された文字列の(シフトJISでの)バイト数を返します。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>0</code> を返します。
	 * 
	 * <pre>
	 * JaStringUtils.getShiftJisLength(null)  = 0
	 * JaStringUtils.getShiftJisLength("")    = 0
	 * JaStringUtils.getShiftJisLength("a")   = 1
	 * JaStringUtils.getShiftJisLength("あ")  = 2
	 * JaStringUtils.getShiftJisLength("あa") = 3
	 * </pre>
	 * 
	 * @param s 文字列
	 * @return バイト数
	 */
	public static int getShiftJISByteLength(final String s) {
		if (s == null)
			return 0;

		try {
			return s.getBytes(CharEncoding.SHIFT_JIS).length;
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * 指定された文字の(シフトJISでの)バイト数を返します。<p>
	 * 
	 * <pre>
	 * JaStringUtils.getShiftJisLength(' ')  = 1
	 * JaStringUtils.getShiftJisLength('a')  = 1
	 * JaStringUtils.getShiftJisLength('　') = 2
	 * JaStringUtils.getShiftJisLength('あ') = 2
	 * </pre>
	 * 
	 * @param c 文字
	 * @return バイト数
	 */
	public static int getShiftJISByteLength(final char c) {
		return getShiftJISByteLength(String.valueOf(c));
	}

	////////////////////////////////////////////////////////////
	// 切取り関連

	/**
	 * 指定された文字が trim 操作において除去対象の文字であるかどうかを返します。<p>
	 * 
	 * @param c 文字
	 * @return ASCII制御文字、半角スペース、全角スペースのいづれかである場合は true。それ以外の場合は false
	 */
	private static boolean isTrimCharacter(final char c) {
		return Ascii.isControlCharacter(c) || c == Ascii.SP || c == WIDE_SPACE;
	}

	/**
	 * 指定された文字列の最後からASCII制御文字、半角スペース、全角スペースを取り除いて返します。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * <pre>
	 * JaStringUtils.leftTrim(null)         = null
	 * JaStringUtils.leftTrim("")           = ""
	 * JaStringUtils.leftTrim(" ")          = ""
	 * JaStringUtils.leftTrim("　")         = ""
	 * JaStringUtils.leftTrim("あいう")     = "あいう"
	 * JaStringUtils.leftTrim("  あいう  ") = "あいう  "
	 * </pre>
	 * 
	 * @param s 文字列
	 * @return 処理された文字列
	 */
	public static String leftTrim(final String s) {
		if (s == null)
			return null;

		for (int i = 0; i < s.length(); i++) {
			if (!isTrimCharacter(s.charAt(i)))
				return s.substring(i);
		}

		return StringUtils.EMPTY;
	}

	/**
	 * 指定された文字列の先頭からASCII制御文字、半角スペース、全角スペースを取り除いて返します。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * <pre>
	 * JaStringUtils.rightTrim(null)         = null
	 * JaStringUtils.rightTrim("")           = ""
	 * JaStringUtils.rightTrim(" ")          = ""
	 * JaStringUtils.rightTrim("　")         = ""
	 * JaStringUtils.rightTrim("あいう")     = "あいう"
	 * JaStringUtils.rightTrim("  あいう  ") = "  あいう"
	 * </pre>
	 * 
	 * @param s 文字列
	 * @return 処理された文字列
	 */
	public static String rightTrim(final String s) {
		if (s == null)
			return null;

		for (int i = s.length() - 1; i >= 0; i--) {
			if (!isTrimCharacter(s.charAt(i)))
				return s.substring(0, i + 1);
		}

		return StringUtils.EMPTY;
	}

	/**
	 * 指定された文字列の先頭と最後からASCII制御文字、半角スペース、全角スペースを取り除いて返します。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * <pre>
	 * JaStringUtils.trim(null)         = null
	 * JaStringUtils.trim("")           = ""
	 * JaStringUtils.trim(" ")          = ""
	 * JaStringUtils.trim("　")         = ""
	 * JaStringUtils.trim("あいう")     = "あいう"
	 * JaStringUtils.trim("  あいう  ") = "あいう"
	 * </pre>
	 * 
	 * @param s 文字列
	 * @return 処理された文字列
	 */
	public static String trim(final String s) {
		return rightTrim(leftTrim(s));
	}

	/**
	 * 日本語文字(漢字や濁点文字など)やCRLFなどを途中で切取る事なく left 処理を行います。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @param len バイト数
	 * @return 処理された文字列
	 */
	public static String left(final String s, final int len) {
		if (s == null)
			return null;

		final StringBuilder results = new StringBuilder(len);
		int count = 0;

		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);
			final int n = count + getShiftJISByteLength(c);

			if (n > len)
				break;

			final int nextIndex = i + 1;
			if (n == len && nextIndex < s.length()) {
				final char next = s.charAt(nextIndex);
				if (c == Ascii.CR && next == Ascii.LF)
					break;
				if (next == 'ﾞ' || next == 'ﾟ')
					break;
			}

			results.append(c);
			count = n;
		}

		return results.toString();
	}

	/**
	 * 日本語文字(漢字や濁点文字など)やCRLFなどを途中で切取る事なく right 処理を行います。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @param len バイト数
	 * @return 処理された文字列
	 */
	public static String right(final String s, final int len) {
		if (s == null)
			return null;

		final StringBuilder results = new StringBuilder(len);
		int count = 0;

		for (int i = s.length() - 1; i >= 0; i--) {
			final char c = s.charAt(i);
			final int n = count + getShiftJISByteLength(c);

			if (n > len)
				break;

			final int j = i - 1;
			if (n == len && j < s.length()) {
				final char next = s.charAt(j);
				if (c == Ascii.LF && next == Ascii.CR)
					break;
				if (c == 'ﾞ' || c == 'ﾟ')
					break;
			}

			results.append(c);
			count = n;
		}

		return results.toString();
	}

/*
	public static String mid(final String s, final int pos, final int len) {
		if (s == null)
			return null;

		return null;
	}

	public static String mid(final String s, final int pos) {
		return mid(s, pos, -1);
	}
*/

	////////////////////////////////////////////////////////////
	// 変換関連

	/**
	 * 半角の数字を表す。<p>
	 */
	private static final String NARROW_DIGIT = 
		// 0123456789
		"\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039";

	/**
	 * 全角の数字を表す。<p>
	 */
	private static final String WIDE_DIGIT =
		// ０１２３４５６７８９
		"\uFF10\uFF11\uFF12\uFF13\uFF14\uFF15\uFF16\uFF17\uFF18\uFF19";

	/**
	 * 指定した文字列内の半角数字を全角数字へ変換した文字列を返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toWideDigit(final String s) {
		return convert(s, NARROW_DIGIT, WIDE_DIGIT);
	}

	/**
	 * 指定した文字列内の全角数字を半角数字へ変換した文字列を返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toNarrowDigit(final String s) {
		return convert(s, WIDE_DIGIT, NARROW_DIGIT);
	}

	/**
	 * 半角の英字を表す。<p>
	 */
	private static final String NARROW_ALPHABET =
		// ABCDEFGHIJKLMNOPQRSTUVWXYZ
		"\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A" +
		// abcdefghijklmnopqrstuvwxyz
		"\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A";

	/**
	 * 全角の英字を表す。<p>
	 */
	private static final String WIDE_ALPHABET =
		// ＡＢＣＤＥＦＧＨＩＪＫＬＭＮＯＰＱＲＳＴＵＶＷＸＹＺ
		"\uFF21\uFF22\uFF23\uFF24\uFF25\uFF26\uFF27\uFF28\uFF29\uFF2A\uFF2B\uFF2C\uFF2D\uFF2E\uFF2F\uFF30\uFF31\uFF32\uFF33\uFF34\uFF35\uFF36\uFF37\uFF38\uFF39\uFF3A" +
		// ａｂｃｄｅｆｇｈｉｊｋｌｍｎｏｐｑｒｓｔｕｖｗｘｙｚ
		"\uFF41\uFF42\uFF43\uFF44\uFF45\uFF46\uFF47\uFF48\uFF49\uFF4A\uFF4B\uFF4C\uFF4D\uFF4E\uFF4F\uFF50\uFF51\uFF52\uFF53\uFF54\uFF55\uFF56\uFF57\uFF58\uFF59\uFF5A";

	/**
	 * 指定した文字列内の半角英字を全角英字へ変換した文字列を返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toWideAlphabet(final String s) {
		return convert(s, NARROW_ALPHABET, WIDE_ALPHABET);
	}

	/**
	 * 指定した文字列内の全角英字を半角英字へ変換した文字列を返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toNarrowAlphabet(final String s) {
		return convert(s, WIDE_ALPHABET, NARROW_ALPHABET);
	}

	/**
	 * 半角のASCII文字(ASCII制御文字及びチルダ"~"を除く)を表す。<p>
	 */
	private static final String NARROW_ASCII = NARROW_DIGIT + NARROW_ALPHABET +
		" !\"#$%&'(*+,-./:;<=>?@[\\]^_`{|}";

	/**
	 * 全角のASCII文字を表す。<p>
	 */
	private static final String WIDE_ASCII = WIDE_DIGIT + WIDE_ALPHABET +
		"　！”＃＄％＆’（＊＋，−．／：；＜＝＞？＠［￥］＾＿｀｛｜｝";

	/**
	 * 指定した文字列内の半角ASCII文字を全角ASCII文字へ変換した文字列を返します。<p>
	 * ASCII制御文字及びチルダ"~"は変換されません。<br>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toWideAscii(final String s) {
		return convert(s, NARROW_ASCII, WIDE_ASCII);
	}

	/**
	 * 指定した文字列内の全角ASCII文字を半角ASCII文字へ変換した文字列を返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toNarrowAscii(final String s) {
		return convert(s, WIDE_ASCII, NARROW_ASCII);
	}

	/**
	 * 指定された文字列について from 及び to で指定された変換表を使用して変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @param from 変換元の表
	 * @param to 変換先の表
	 * @return 変換した文字列
	 */
	private static String convert(final String s, final String from, final String to) {
		if (s == null)
			return null;

		StringBuilder results = new StringBuilder(s.length());

		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);
			final int n = from.indexOf(c);
			if (n != -1)
				results.append(to.charAt(n));
			else
				results.append(c);
		}

		return results.toString();
	}

	////////////////////////////////////////////////////////////
	// ひらがな/カタカナ

	/**
	 * 双方向変換用のひらがな一覧表を表します。<p>
	 */
	private static final String[] HIRAGANA = {
		// 濁音
		"\u304C",	"\u304E",	"\u3050",	"\u3052",	"\u3054",	// GA行(がぎぐげご)
		"\u3056",	"\u3058",	"\u305A",	"\u305C",	"\u305E",	// ZA行(ざじずぜぞ)
		"\u3060",	"\u3062",	"\u3065",	"\u3067",	"\u3069",	// DA行(だぢづでど)
		"\u3070",	"\u3073",	"\u3076",	"\u3079",	"\u307C",	// BA行(ばびぶべぼ)
		"\u3071",	"\u3074",	"\u3077",	"\u307A",	"\u307D",	// PA行(ぱぴぷぺぽ)
//		null,		null,		"\u3094",	null,		null,		// VA行(    うﾞ   )
		null,		null,		null,		null,		null,		// VA行(    うﾞ   )

		"\u3042",	"\u3044",	"\u3046",	"\u3048",	"\u304A",	// あ行(あいうえお)
		"\u304B",	"\u304D",	"\u304F",	"\u3051",	"\u3053",	// か行(かきくけこ)
		"\u3055",	"\u3057",	"\u3059",	"\u305B",	"\u305D",	// さ行(さしすせそ)
		"\u305F",	"\u3061",	"\u3064",	"\u3066",	"\u3068",	// た行(たちつてと)
		"\u306A",	"\u306B",	"\u306C",	"\u306D",	"\u306E",	// な行(なにぬねの)
		"\u306F",	"\u3072",	"\u3075",	"\u3078",	"\u307B",	// は行(はひふへほ)
		"\u307E",	"\u307F",	"\u3080",	"\u3081",	"\u3082",	// ま行(まみむめも)
		"\u3084",				"\u3086",				"\u3088",	// や行(や  ゆ  よ)
		"\u3089",	"\u308A",	"\u308B",	"\u308C",	"\u308D",	// ら行(らりるれろ)
		"\u308F",	"\u3090",	"\u3091",	"\u3092",	"\u3093",	// わ行(わゐゑをん)

		// 小文字
		"\u3041",	"\u3043",	"\u3045",	"\u3047",	"\u3049",	// あ行(ぁぃぅぇぉ)
		"\u3095",				null,		"\u3096",				// か行(か  くけ  )
					null,		null,								// さ行(  しす    )
								"\u3063",				null,		// た行(    っ  と)
								null,								// な行(    ぬ    )
		null,		null,		null,		null,		null,		// は行(はひふへほ)
								null,								// ま行(    む    )
		"\u3083",				"\u3085",				"\u3087",	// や行(ゃ  ゅ  ょ)
		null,		null,		null,		null,		null,		// ら行(らりるれろ)
		"\u308E",													// わ行(ゎ        )

		// 音声記号
		"\u309B",	"\u309C",										// 発音記号(゛゜)
		"\u30FB",	"\u30FC",										// 長音記号(・ー)
		"\u309D",	"\u309E",										// 繰返記号(ゝゞ)

		// 句読点
		"。",		"「",		"」",		"、"
	};

	/**
	 * 双方向変換用の全角カタカナ一覧表を表します。<p>
	 */
	private static final String[] WIDE_KATAKANA = {
		// 濁音
		"\u30AC",	"\u30AE",	"\u30B0",	"\u30B2",	"\u30B4",	// ガ行(ガギグゲゴ)
		"\u30B6",	"\u30B8",	"\u30BA",	"\u30BC",	"\u30BE",	// ザ行(ザジズゼゾ)
		"\u30C0",	"\u30C2",	"\u30C5",	"\u30C7",	"\u30C9",	// ダ行(ダヂヅデド)
		"\u30D0",	"\u30D3",	"\u30D6",	"\u30D9",	"\u30DC",	// バ行(バビブベボ)
		"\u30D1",	"\u30D4",	"\u30D7",	"\u30DA",	"\u30DD",	// パ行(パピプペポ)
		"\u30F7",	"\u30F8",	"\u30F4",	"\u30F9",	"\u30FA",	// VA行

		"\u30A2",	"\u30A4",	"\u30A6",	"\u30A8",	"\u30AA",	// ア行(アイウエオ)
		"\u30AB",	"\u30AD",	"\u30AF",	"\u30B1",	"\u30B3",	// カ行(カキクケコ)
		"\u30B5",	"\u30B7",	"\u30B9",	"\u30BB",	"\u30BD",	// サ行(サシスセソ)
		"\u30BF",	"\u30C1",	"\u30C4",	"\u30C6",	"\u30C8",	// タ行(タチツテト)
		"\u30CA",	"\u30CB",	"\u30CC",	"\u30CD",	"\u30CE",	// ナ行(ナニヌネノ)
		"\u30CF",	"\u30D2",	"\u30D5",	"\u30D8",	"\u30DB",	// ハ行(ハヒフヘホ)
		"\u30DE",	"\u30DF",	"\u30E0",	"\u30E1",	"\u30E2",	// マ行(マミムメモ)
		"\u30E4",				"\u30E6",				"\u30E8",	// ヤ行(ヤ  ユ  ヨ)
		"\u30E9",	"\u30EA",	"\u30EB",	"\u30EC",	"\u30ED",	// ラ行(ラリルレロ)
		"\u30EF",	"\u30F0",	"\u30F1",	"\u30F2",	"\u30F3",	// ワ行(ワヰヱヲン)

		// 小文字
		"\u30A1",	"\u30A3",	"\u30A5",	"\u30A7",	"\u30A9",	// ア行(ァィゥェォ)
		"\u30F5",				"\u31F0",	"\u30F6",				// カ行(ヵ  クヶ  )
					"\u31F1",	"\u31F2",							// サ行(  シス    )
								"\u30C3",				"\u31F3",	// タ行(    ッ  ト)
								"\u31F4",							// ナ行(    ヌ    )
		"\u31F5",	"\u31F6",	"\u31F7",	"\u31F8",	"\u31F9",	// ハ行(ハヒフヘホ)
								"\u31FA",							// マ行(    ム    )
		"\u30E3",				"\u30E5",				"\u30E7",	// ヤ行(ャ  ュ  ョ)
		"\u31FB",	"\u31FC",	"\u31FD",	"\u31FE",	"\u31FF",	// ラ行(ラリルレロ)
		"\u30EE",													// ワ行(ヮ        )

		// 音声記号
		"\u309B",	"\u309C",										// 発音記号(゛゜)
		"\u30FB",	"\u30FC",										// 長音記号(・ー)
		"\u30FD",	"\u30FE",										// 繰返記号(ヽヾ)

		// 句読点
		"。",		"「",		"」",		"、"
	};

	/**
	 * 双方向変換用の半角カタカナ一覧表を表します。<p>
	 */
	private static final String[] NARROW_KATAKANA = {
		// 濁音
		"ｶﾞ",	"ｷﾞ",	"ｸﾞ",	"ｹﾞ",	"ｺﾞ",	// ガ行(ガギグゲゴ)
		"ｻﾞ",	"ｼﾞ",	"ｽﾞ",	"ｾﾞ",	"ｿﾞ",	// ザ行(ザジズゼゾ)
		"ﾀﾞ",	"ﾁﾞ",	"ﾂﾞ",	"ﾃﾞ",	"ﾄﾞ",	// ダ行(ダヂヅデド)
		"ﾊﾞ",	"ﾋﾞ",	"ﾌﾞ",	"ﾍﾞ",	"ﾎﾞ",	// バ行(バビブベボ)
		"ﾊﾟ",	"ﾋﾟ",	"ﾌﾟ",	"ﾍﾟ",	"ﾎﾟ",	// パ行(パピプペポ)
		"ﾜﾞ",	null,	"ｳﾞ",	null,	"ｦﾞ",	// VA行

		"ｱ",	"ｲ",	"ｳ",	"ｴ",	"ｵ",	// ア行(アイウエオ)
		"ｶ",	"ｷ",	"ｸ",	"ｹ",	"ｺ",	// カ行(カキクケコ)
		"ｻ",	"ｼ",	"ｽ",	"ｾ",	"ｿ",	// サ行(サシスセソ)
		"ﾀ",	"ﾁ",	"ﾂ",	"ﾃ",	"ﾄ",	// タ行(タチツテト)
		"ﾅ",	"ﾆ",	"ﾇ",	"ﾈ",	"ﾉ",	// ナ行(ナニヌネノ)
		"ﾊ",	"ﾋ",	"ﾌ",	"ﾍ",	"ﾎ",	// ハ行(ハヒフヘホ)
		"ﾏ",	"ﾐ",	"ﾑ",	"ﾒ",	"ﾓ",	// マ行(マミムメモ)
		"ﾔ",			"ﾕ",			"ﾖ",	// ヤ行(ヤ  ユ  ヨ)
		"ﾗ",	"ﾘ",	"ﾙ",	"ﾚ",	"ﾛ",	// ラ行(ラリルレロ)
		"ﾜ", 	null,	null,	"ｦ",	"ﾝ",	// ワ行(ワヰヱヲン)

		// 小文字
		"ｧ",	"ｨ",	"ｩ",	"ｪ", 	"ｫ",	// ア行(ァィゥェォ)
		null,			null,	null,			// カ行(ヵ  クヶ  )
				null,	null,					// サ行(  シス    )
						"ｯ",			null,	// タ行(    ッ  ト)
						null,					// ナ行(    ヌ    )
		null,	null,	null,	null,	null,	// ハ行(ハヒフヘホ)
						null,					// マ行(    ム    )
		"ｬ",			"ｭ",			"ｮ",	// ヤ行(ャ  ュ  ョ)
		null,	null,	null,	null,	null,	// ラ行(ラリルレロ)
		null,									// ワ行(ヮ        )

		// 音声記号
		"ﾞ",	"ﾟ",							// 発音記号(゛゜)
		"･",	"ｰ",							// 長音記号(・ー)
		null,	null,							// 繰返記号

		// 句読点
		"｡",	"｢",	"｣",	"､"
	};

	/**
	 * 指定した文字列内のひらがなを半角カタカナへ変換した文字列を返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String hiraganaToNarrowKana(final String s) {
		return convert(s, HIRAGANA, NARROW_KATAKANA);
	}

	/**
	 * 指定された文字列を平仮名から全角カタカナへ変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String hiraganaToWideKana(final String s) {
		return convert(s, HIRAGANA, WIDE_KATAKANA);
	}

	/**
	 * 指定された文字列を半角カタカナから全角カタカナへ変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String narrowKanaToWideKana(final String s) {
		return convert(s, NARROW_KATAKANA, WIDE_KATAKANA);
	}

	/**
	 * 指定された文字列を半角カタカナからひらがなへ変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String narrowKanaToHiragana(final String s) {
		return convert(s, NARROW_KATAKANA, HIRAGANA);
	}

	/**
	 * 指定された文字列を全角カタカナから半角カタカナへ変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String wideKanaToNarrowKana(final String s) {
		return convert(s, WIDE_KATAKANA, NARROW_KATAKANA);
	}

	/**
	 * 指定された文字列を全角カタカナからひらがなへ変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String wideKanaToHiragana(final String s) {
		return convert(s, WIDE_KATAKANA, HIRAGANA);
	}

	/**
	 * 指定された文字列内の全角カタカナと半角カタカナをひらがなへ変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。<br>
	 * このメソッドは利便性の為に提供されています。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String katakanaToHiragana(final String s) {
		String result = s;
		result = narrowKanaToHiragana(result);
		result = wideKanaToHiragana(result);
		return result;
	}

	/**
	 * 指定された文字列について from 及び to で指定された変換表を使用して変換して返します。<p>
	 * 文字列に <code>null</code> を指定した場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @param from 変換元の表
	 * @param to 変換先の表
	 * @return 変換した文字列
	 */
	private static String convert(final String s, final String[] from, final String[] to) {
		if (s == null)
			return null;

		String result = s;

		for (int i = 0; i < from.length; i++) {
			if (from[i] != null && to[i] != null)
				result = StringUtils.replace(result, from[i], to[i]);
		}

		return result;
	}

	////////////////////////////////////////////////////////////
	// 総合

	/**
	 * 指定された文字列内の半角文字を全角文字へ変換して返します。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toWide(final String s) {
		return narrowKanaToWideKana(toWideAscii(s));
	}

	/**
	 * 指定された文字列内の全角文字を半角文字へ変換して返します。<p>
	 * 文字列に <code>null</code> が指定された場合、<code>null</code> を返します。
	 * 
	 * @param s 文字列
	 * @return 変換した文字列
	 */
	public static String toNarrow(final String s) {
		return wideKanaToNarrowKana(toNarrowAscii(s));
	}

}
