🤔

カタカナを半角 / 全角変換する

2022/02/21に公開

覆水盆に返らず (Mr.Fukusui did not go home at summer vacation.)

可逆的な相互変換ができるものではないため、そもそも本来であればそんな変換が必要な運用が存在すること自体異常ではある。このご時世に於いて、半角カタカナの存在意義など微塵も無く。それは、外来文字である英数字の全角も同様に。

しかしながら、なかなかそうも言えないのが現実なもので。現存するレガシー仕様に、依然則らなければいけないことも少なくない。未だにブラウザのUSERAGENT偽装で殆どがMozillaを偽っているわけだし。

ともあれ、Javaにてカタカナを 半角 全角 変換をする必要があり、やってみた。

そんな ~0 番煎じの内容

コード

https://paiza.io/projects/E_ux4CXTJyYFj1iw_GIjdA

Let's go 実装 ライダー

実装① シンプルに変換表で変換 👎

変換表

管理と実装のし易さから、文字種別に2次元の変換表を設け、それを3次元のジャグ配列での管理を考える。

①通常のカタカナ

通常.java
private static final String[][] KANA_NORMAL_MAP = new String[][] {
	// [0] 半角
	{
		"ア", "イ", "ウ", "エ", "オ ", "カ", "キ", "ク", "ケ", "コ ",
		"サ", "シ", "ス", "セ", "ソ ", "タ", "チ", "ツ", "テ", "ト ",
		"ナ", "ニ", "ヌ", "ネ", "ノ ", "ハ", "ヒ", "フ", "ヘ", "ホ ",
		"マ", "ミ", "ム", "メ", "モ ", "ヤ", "ユ", "ヨ",
		"ラ", "リ", "ル", "レ", "ロ ", "ワ", "ヲ", "ン"
	},
	// [1] 全角
	{
		"ア", "イ", "ウ", "エ", "オ ", "カ", "キ", "ク", "ケ", "コ ",
		"サ", "シ", "ス", "セ", "ソ ", "タ", "チ", "ツ", "テ", "ト ",
		"ナ", "ニ", "ヌ", "ネ", "ノ ", "ハ", "ヒ", "フ", "ヘ", "ホ ",
		"マ", "ミ", "ム", "メ", "モ ", "ヤ", "ユ", "ヨ",
		"ラ", "リ", "ル", "レ", "ロ ", "ワ", "ヲ", "ン"
	}
};

②濁音/半濁音

濁音/半濁音.java
private static final String[][] KANA_COMBINED_MAP = new String[][] {
	// [0] 半角
	{
		// 濁点付き
		"ヴ", 
		"ガ", "ギ", "グ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ",
		"ダ", "ヂ", "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
		// 半濁点付き
		"パ", "ピ", "プ", "ペ", "ポ"
	},
	// [1] 全角
	{
		// 濁音
		"ヴ", 
		"ガ", "ギ", "グ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ",
		"ダ", "ヂ", "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ",
		// 半濁音
		"パ", "ピ", "プ", "ペ", "ポ"
	}
};

③小文字

小文字.java
private static final String[][] KANA_SMAL_MAP = new String[][] {
	// [0] 半角
	{
		"ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ"
	},
	// [1] 全角
	{
		"ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ"
	}
};

④記号

長音句読点 などを 記号 と称するのが適切かはわからないけど。それらの文字の変換を定義。

記号.java
private static final String[][] KANA_SYMBOL_MAP = new String[][] {
	// [0] 半角
	{
		// 長音、句読点
		"ー", "。", "、",
		// 濁点
		"゙",
		// 半濁点
		"゚"
	},
	// [1] 全角
	{
		// 長音、句読点
		"ー", "。", "、",
		// 濁点 (U+309B KATAKANA-HIRAGANA VOICED SOUND MARK)
		"゛",
		// 半濁点 (U+309C KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK)
		"゜"
	}
};

⑨ 変換表セット

ジャグ配列でぶっこみ の拓

変換表セット.java
private static final String[][][] SIMPLE_MAPSET = new String[][][] {
	/* ①通常. */
	KANA_NORMAL_MAP,
	/* ②濁音/半濁音. */
	KANA_COMBINED_MAP,
	/* ③小文字. */
	KANA_SMAL_MAP,
	/* ④記号. */
	KANA_SYMBOL_MAP
};

変換処理

変換処理.java
/* [0] 半角. */
private static final int MAP_INDEX_HALF = 0;
/* [1] 全角. */
private static final int MAP_INDEX_FULL = 1;

private static String converFromMapset(final String str, final String[][][] mapset, final boolean isConvToHalf) {
	final int iSrc, iTgt;
	String ret = str;

	if(isConvToHalf) {
		// [1] 全角 => [0] 半角
		iSrc = MAP_INDEX_FULL;
		iTgt = MAP_INDEX_HALF;
	} else {
		// [0] 半角 => [1] 全角
		iSrc = MAP_INDEX_HALF;
		iTgt = MAP_INDEX_FULL;
	}
	
	for(String map[][]: mapset) {
		final int max = Math.min(map[0].length, map[1].length);
		for(int i = 0; i < max; i++) {
			final String src = map[iSrc][i];
			final String tgt = map[iTgt][i];
			ret = ret.replace(src, tgt);
		}
	}

	return ret;
}

public static void main(String[] args) throws Exception {
	System.out.println(converFromMapset("ガパピーョ。", SIMPLE_MAPSET, true));
	System.out.println(converFromMapset("ガパピーョ。", SIMPLE_MAPSET, false));
}

変換結果

input
// 全角
ガパピーョ。 (U+30AC U+30D1 U+30D4 U+30FC U+30E7 U+3002)

// 半角
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)
output
// (全角) ⇒ 半角
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)

// (半角) ⇒ 全角
カ゛ハ゜ヒ゜ーョ。 (U+30AB U+309B U+30CF U+309C U+30D2 U+309C U+30FC U+30E7 U+3002)

全角 から 半角 への変換には特に問題ないのだが、全角 から 半角 への変換では、半角同様に、濁音、半濁音が2文字になってしまっている。当然だが、全角カタカナでこんな表記を求めるわけもなく。濁音、半濁音が1文字になる様に工夫が必要である。

実装② 変換表で変換 (濁音 / 半濁音を先に) 👍

実装① を元に、変換表セットの順番を変更し、複数文字構成である、 濁音 / 半濁音 を先に持ってくる。先に変換してしまえば、あとは単独のみになるため、一応問題なく変換が行える感じ。

※ この手の手法は、改行コードを LF に統一する際にも行われる手法かと思われる。 (CR+LF を先に置換して、 CRを後で置換したりとか)。

変換表

⑨ 変換表セット

濁音、半濁音を先に持ってくる。
このための三次元ジャグ配列。

変換表セット.java
private static final String[][][] SIMPLE_MAPSET = new String[][][] {
	/* ②濁音/半濁音. */
	KANA_COMBINED_MAP,
	/* ①通常. */
	KANA_NORMAL_MAP,
	/* ③小文字. */
	KANA_SMAL_MAP,
	/* ④記号. */
	KANA_SYMBOL_MAP
};

変換結果

input
// 全角
ガパピーョ。 (U+30AC U+30D1 U+30D4 U+30FC U+30E7 U+3002)

// 半角
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)
output
// (全角) ⇒ 半角
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)

// (半角) ⇒ 全角
ガパピーョ。 (U+30AC U+30D1 U+30D4 U+30FC U+30E7 U+3002)

半角 から変換した 全角 の濁音/半濁音が1文字に鳴っている。非常にGood!

実装③ NFCで正規化 👍

もう1つの方法。

UNICODEには、 正規化 (Normalization) と言う考えのものがある。合成文字について、 合成表現を行う NFC (正規化形式C) と、 分離表現を行う NFD (正規化形式D) があり、日本語だと、カタカナの濁音・半濁音などが対象になる。
( NFKC NFKD については割愛。)

UNICODEとしては、NFC から普及していることもあり。 NFD 自体に問題があるわけでもないのだが、しかし、なかなかに NFD の存在はトラブルの元。と言うか毒になる。
( UTF-8 Mac問題 は、ただ単にNFDによる問題)

なので、 実装① を元に、 単純に NFD の形で変換させ、その後にNFC正規化 する。

変換表

④記号

記号の変換テーブルの濁点・半濁点を、合成可能な ゙ (U+3099)゚ (U+309A) に変更する。ただし、 ゙ (U+3099)゚ (U+309A) を単独で記載するのは、文字シーケンス的には異常なため、ソース的にも良くないので、コード ポイントで記載する。

記号.java
private static final String[][] KANA_SYMBOL_MAP = new String[][] {
	// [0] 半角
	{
		// 長音、句読点
		"ー", "。", "、", "゙", "゚",
		// 濁点
		"゙",
		// 半濁点
		"゚"
	},
	// [1] 全角
	{
		// 長音、句読点
		"ー", "。", "、",
		// 濁点 (U+3099 COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK)
		String.valueOf((char)0x3099),
		// 半濁点 (U+309A COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK)
		String.valueOf((char)0x309A)
	}
};

昔、ゲームの欧州向けローカライズで、S-JISのC言語のソースに欧州文字を埋め込むときにもこんなやりかたしたっけ。

変換処理

正規化は Normalizer#normalize() にて行う。

変換処理.java
public static void main(String[] args) throws Exception {
	System.out.println(converFromMapset("ガパピーョ。", SIMPLE_MAPSET, true));
	System.out.println(Normalizer.normalize(converFromMapset("ガパピーョ。", SIMPLE_MAPSET, true), Normalizer.Form.NFC));

	System.out.println(converFromMapset("ガパピーョ。", SIMPLE_MAPSET, false));
	System.out.println(Normalizer.normalize(converFromMapset("ガパピーョ。", SIMPLE_MAPSET, false), Normalizer.Form.NFC));
}

変換結果

input
// 全角
ガパピーョ。 (U+30AC U+30D1 U+30D4 U+30FC U+30E7 U+3002)

// 半角
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)
output
// (全角) ⇒ 半角 (NFDの状態)
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)
// (全角) ⇒ 半角 (NFC化後)
ガパピーョ。 (U+FF76 U+FF9E U+FF8A U+FF9F U+FF8B U+FF9F U+FF70 U+FF6E U+FF61)

// (半角) ⇒ 全角 (NFDの状態)
ガパピーョ。 (U+30AB U+3099 U+30CF U+309A U+30D2 U+309A U+30FC U+30E7 U+3002)
// (半角) ⇒ 全角 (NFC化後)
ガパピーョ。 (U+30AC U+30D1 U+30D4 U+30FC U+30E7 U+3002)

Q&A

Q. final多くない?

final教徒ですんで。

ひとりごと

当たり前だけど、句読点や長音にはカタカナ用、ひらがな用なんてものは無い。しかし、一方でひらがなには 半角などと言う考え方が無い。そのため、ひらがなとカタカナが混在が混在する文章ではどうしたって機会変換など可笑しくなってしまう。

input
がぱぴーょ。 (U+304C U+3071 U+3074 U+30FC U+3087 U+3002)
output
// ⇒ 半角 (NFDの状態)
がぱぴーょ。 (U+304C U+3071 U+3074 U+FF70 U+3087 U+FF61)

句点が 全角の 。 (U+3002) から 。 (U+FF61) に変わってしまっている。また長音も ー (U+30FC) から ー (U+FF70) に変わってしまっている

もっとも、そもそも歴史的な観点で考えれば。コンピューターで最初に取り込まれたの日本語文字がカタカナで、その仕様が通称 半角カタカナ として、その後にも維持されただけに過ぎない。

コンピューターの性能が上がり、1 character7bit から 8bit となり、歐米では欧州文字が、日本ではカタカナが定義され。その後日本は文字数が多かったため、更に多byteによる文字の表現方法を策定し、ひらがな、漢字、記号が取り込まれた。

カタカナが再定義されたのは、恐らく表示幅が原因だろう。8bit時代まではASCII文字幅で、カタカナが表現可能だったものの。いざ漢字を表現しようにも、ASCII文字幅では幅が足りなかった。そして、漢字だけ幅を広げると、ひらがなとカタカナがASCII文字幅のままでは非常に見栄えが悪い。そのため、 それまでの ASCII文字幅の半角 と、新たに 日本語文字幅としての全角 の考えが生まれ、先んじて生まれていたカタカナは互換性のため保持され、 半角カタカナ の通称として現存しているのだと思われる。

また、表示幅を広く取れるなら、濁音 / 半濁音も1文字で表現できなかった従来のカタカナには、やはり不都合があったのだと思われる。そもそも考えてみれば、濁音 / 半濁音 は1文字として学校で習ってきていたハズのに。銀行などの計算機 (コンピューター) が絡むと途端に2文字扱いになるのは非常に不自然で。これもやはり8bit時代の表示幅が起因すると考えると合点がいく。コンピューターは計算機なので、金融系の業務のほうが当然導入が早かったであろうと思われるし。

しかしながら、そんな背景であっても。現在の 半角カタカナ の存在意義としては、レガシー運用の互換性維持 と、アスキー アートや顔文字 以外には無いだろう。よもや 非推奨文字 と言っても過言ではない。では 全て全角化するべきなのだろうか? と言えば、それは違う。

もちろん日本語文字は全角であるべきなのだろうが。しかし、 英数字については半角であるべき である。理由は3点あり。

  1. (文化面) アラビア数字、ベーシック アルファベット (英字) は、そもそも日本の文字ではない。日本がどうこう言う立場ではない。
  2. (情報面) ASCII文字が持つ互換性の性質面に於いて、全角英数字には互換性が全く無く、英数字としては使い物にならない。ASCII領域にある文字が正しく、1つの文字に複数の割割り当ては要らない。
  3. (表示面) 全角英数字は根本的に見栄えが悪い。日本語文章上に於いても、英数字が全角幅を持つことは醜悪。幅の微調整はプロポーショナル フォントの役目。

しかしながら、これまでの 全角化・半角化を謳う機能の殆が、英数カナ記号を対象に変換を行ってしまう。それが、8bit時代の運用に寄せる だったり 8bit領域から外す などの目的なのであればしかたないが。普通の通販サイトで「住所は全角で入力してください」とか、もう目も当てられない。ネットでニュース記事などを見ていても、「1月23日」「Windows10」「3Dゲーム」みたいな全角/半角が情緒不安定な記述を見かけると、最早内容が頭に入ってこない。最も数字が全角なのは、テンキーの無いノートPC ユーザーに多く、Microsoft IMEが数字は全角から入力してしまうことが主な原因かと思われるが。

本当に必要なのは 全角化半角化 の様な変換ではなく。 NFKC / NFKD の様な、文字種に応じた正規化 が必要なのだと思われる。そして、我々イエロー モンキーは 半角カタカナ全角英数字 の普及率を低下させる働きをしていかなければならない。あと、 U+005C円記号 (¥) ではなく バック スラッシュ (\) であり、円記号 (¥)U+00A5 で認知を広めることも。。。
(Windows 11が依然日本語フォントでフォント騙しをしていて、辟易とした。UIフォントでは未だにディレクトリ セパレーターが ¥ に見えるんだもんなぁ。。。)

参考 (謝辞)

Discussion