概要
日本の業務システムでは、半角カナと全角カナの混在が日常的に発生します。銀行の全銀フォーマットは半角カナを要求し、画面入力やデータベースには全角カナが格納されていることが多いため、相互変換の処理は避けて通れません。単純な1文字ずつの置換で済めばよいのですが、半角カナの濁音(ガ)は基本文字 + 濁点の2文字で表現されるのに対し、全角カナの濁音(ガ)は1文字です。この2文字結合の処理を正しく実装しないと、変換結果がおかしくなります。この記事では、変換テーブルの設計、濁音・半濁音の結合ロジック、往復変換の一致検証まで、Pure Java で完結する実装を示します。
使いどころ
全銀フォーマットのデータ作成時に、顧客名や口座名義をデータベースの全角カナから半角カナに変換する
OCR で読み取った半角カナ混じりのテキストを全角カナに正規化し、名寄せや検索の精度を上げる
CSV 取込時に全角・半角カナが混在するデータを統一形式に変換し、データベース格納前に正規化する
コード例
import java.util.Map;
public class HalfKanaConverter {
private static final String HALF_KANA =
"ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン";
private static final String FULL_KANA =
"ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン";
private static final Map<Character, Character> DAKUTEN_MAP = Map.ofEntries(
Map.entry('カ', 'ガ'), Map.entry('キ', 'ギ'), Map.entry('ク', 'グ'),
Map.entry('ケ', 'ゲ'), Map.entry('コ', 'ゴ'), Map.entry('サ', 'ザ'),
Map.entry('シ', 'ジ'), Map.entry('ス', 'ズ'), Map.entry('セ', 'ゼ'),
Map.entry('ソ', 'ゾ'), Map.entry('タ', 'ダ'), Map.entry('チ', 'ヂ'),
Map.entry('ツ', 'ヅ'), Map.entry('テ', 'デ'), Map.entry('ト', 'ド'),
Map.entry('ハ', 'バ'), Map.entry('ヒ', 'ビ'), Map.entry('フ', 'ブ'),
Map.entry('ヘ', 'ベ'), Map.entry('ホ', 'ボ'), Map.entry('ウ', 'ヴ')
);
private static final Map<Character, Character> HANDAKUTEN_MAP = Map.of(
'ハ', 'パ', 'ヒ', 'ピ', 'フ', 'プ', 'ヘ', 'ペ', 'ホ', 'ポ'
);
/** 半角カナ → 全角カナ(濁音・半濁音の結合処理付き) */
public static String toFullWidth(String input) {
if (input == null) return null;
var sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
char next = (i + 1 < input.length()) ? input.charAt(i + 1) : 0;
if (next == '゙' && DAKUTEN_MAP.containsKey(c)) {
sb.append(DAKUTEN_MAP.get(c));
i++;
continue;
}
if (next == '゚' && HANDAKUTEN_MAP.containsKey(c)) {
sb.append(HANDAKUTEN_MAP.get(c));
i++;
continue;
}
int idx = HALF_KANA.indexOf(c);
sb.append(idx >= 0 ? FULL_KANA.charAt(idx) : c);
}
return sb.toString();
}
/** 全角カナ → 半角カナ(濁音・半濁音は2文字に展開) */
public static String toHalfWidth(String input) {
if (input == null) return null;
var sb = new StringBuilder();
for (char c : input.toCharArray()) {
var dakuten = DAKUTEN_MAP.entrySet().stream()
.filter(e -> e.getValue() == c).findFirst();
if (dakuten.isPresent()) {
sb.append(dakuten.get().getKey()).append('゙');
continue;
}
var handakuten = HANDAKUTEN_MAP.entrySet().stream()
.filter(e -> e.getValue() == c).findFirst();
if (handakuten.isPresent()) {
sb.append(handakuten.get().getKey()).append('゚');
continue;
}
int idx = FULL_KANA.indexOf(c);
sb.append(idx >= 0 ? HALF_KANA.charAt(idx) : c);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(toFullWidth("ガギグゲゴ")); // ガギグゲゴ
System.out.println(toFullWidth("パピプペポ")); // パピプペポ
System.out.println(toFullWidth("ABC アイウ 123")); // ABC アイウ 123
System.out.println(toHalfWidth("ガギグゲゴ")); // ガギグゲゴ
System.out.println(toHalfWidth("パピプペポ")); // パピプペポ
// 往復変換テスト
String original = "ガギグゲゴ";
String full = toFullWidth(original);
String back = toHalfWidth(full);
System.out.println("往復一致: " + original.equals(back)); // true
}
}Version Coverage
Map.ofEntries + Map.entry で不変マップを宣言的に初期化できる。var による型推論でループ内の変数宣言も簡潔になる。
// Java 17: Map.ofEntries で宣言的に初期化
private static final Map<Character, Character> DAKUTEN_MAP =
Map.ofEntries(
Map.entry('カ', 'ガ'), Map.entry('キ', 'ギ'),
Map.entry('ク', 'グ'), Map.entry('ケ', 'ゲ'),
Map.entry('コ', 'ゴ')
// ... 以下同様
);Library Comparison
注意点
半角カナの濁音は「基本文字 + 濁点(゙)」の2文字で構成される。1文字ずつ変換すると濁音が正しく処理されないため、必ず次の文字を先読みするロジックが必要
半濁音の変換テーブルと濁音の変換テーブルは分離する。ハ行は濁音(バ)と半濁音(パ)の両方を持つため、先に半濁点(゚)をチェックしてから濁点(゙)を処理する順序にするとバグを防げる
全角カナから半角カナへの逆変換では、濁音1文字を半角2文字に展開する必要があるため、文字列の長さが変わる点に注意
変換テーブルに「ヴ」(ヴ → ヴ)を含めるかどうかは業務要件による。全銀フォーマットでは「ヴ」は非対応のケースがある
FAQ
一般的には「ヲ」に変換します。ただし全銀フォーマットの口座名義で「ヲ」が使われることは稀で、業務要件で除外するケースもあります。
変換テーブルが正しく対称に定義されていれば戻ります。テストで往復一致を確認するのが確実です。濁音・半濁音の結合と展開が正しく対応しているかが鍵になります。
Unicode のコードポイント差分(0x60)を加算することで変換できます。ただし「ゐ」「ゑ」など対応するカタカナがないケースもあるため、業務で使う文字範囲を確認してください。