Skip to content

Commit f05dd2f

Browse files
committed
ローマ字→ひらがな変換ロジックをリファクタリング
Converter.romajiToHiragana の変換ロジックを RomajiToHiragana クラスに 抽出し、可読性を改善: - String[] の代わりに Entry record を使用 - マジックナンバー(4)を変換テーブルから自動計算 - 巻き戻り値を事前にintへパース(実行時パース廃止) - Minecraft/Forge に依存しない純粋なJavaクラスとして分離 - 227件のユニットテストを追加(JUnit 5) https://claude.ai/code/session_01D2dE5uXJ2exvdTGNvXjKJA
1 parent 60b373c commit f05dd2f

4 files changed

Lines changed: 572 additions & 79 deletions

File tree

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ dependencies {
171171
runtimeOnly fg.deobf("curse.maven:modern-ui-352491:6199942")
172172

173173
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
174+
175+
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
176+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
174177
}
175178

176179
mixin {
@@ -240,6 +243,10 @@ tasks.named('jar', Jar).configure {
240243
// }
241244
//}
242245

246+
test {
247+
useJUnitPlatform()
248+
}
249+
243250
tasks.withType(JavaCompile).configureEach {
244251
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
245252
}

src/main/java/io/github/meatwo310/tsukichat/util/Converter.java

Lines changed: 5 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,97 +13,23 @@
1313
import java.net.URLEncoder;
1414
import java.nio.charset.StandardCharsets;
1515
import java.util.Arrays;
16-
import java.util.LinkedHashMap;
17-
import java.util.Objects;
1816
import java.util.regex.Pattern;
1917
import java.util.stream.Stream;
2018

2119
public class Converter {
22-
/**
23-
* ローマ字からひらがなへの変換テーブル。
24-
* キーにローマ字、値にひらがなと巻き戻り数を持つ
25-
*/
26-
private static final LinkedHashMap<String, String[]> hiraganaMap = new LinkedHashMap<>();
2720
private static final Logger LOGGER = TsukiChat.LOGGER;
28-
29-
/**
30-
* ひらがな変換テーブルを初期化する
31-
* @param resourceName リソース名
32-
*/
33-
private static void initMap(String resourceName) {
34-
URL pathToTable = Converter.class.getResource(resourceName);
35-
36-
if (Objects.isNull(pathToTable)) {
37-
System.out.println("Could not find resource: " + resourceName);
38-
return;
39-
}
40-
41-
// テーブルを読み込む
42-
try (java.io.InputStream stream = pathToTable.openStream()) {
43-
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
44-
String line;
45-
while ((line = reader.readLine()) != null) {
46-
if (line.isBlank()) continue;
47-
String[] parts = line.split(":"); // 0: ローマ字, 1: ひらがな, 2: 巻き戻り数
48-
String romaji = parts[0];
49-
String hiragana = parts[1];
50-
String back;
51-
52-
if (parts.length == 2) { // ローマ字+ひらがな
53-
back = "0";
54-
} else if (parts.length == 3) { // ローマ字+ひらがな+巻き戻り数
55-
// Int型に変換可能か確認
56-
try {
57-
Integer.parseInt(parts[2]);
58-
} catch (Exception e) {
59-
LOGGER.warn("Could not parse int in {}; This line will be ignored: {}", resourceName, line);
60-
continue;
61-
}
62-
back = parts[2];
63-
} else {
64-
LOGGER.warn("Invalid line in {}; This line will be ignored: {}", resourceName, line);
65-
continue;
66-
}
67-
68-
// ローマ字をキーにしてひらがなと巻き戻り数を保存
69-
hiraganaMap.put(romaji, new String[]{hiragana, back});
70-
}
71-
} catch (java.io.IOException e) {
72-
throw new RuntimeException(e);
73-
}
74-
}
75-
76-
static {
77-
initMap("/assets/japaneseromajiconverter/romaji_to_hiragana.txt");
78-
}
21+
private static final RomajiToHiragana ROMAJI_TO_HIRAGANA = RomajiToHiragana.fromResource(
22+
"/assets/japaneseromajiconverter/romaji_to_hiragana.txt",
23+
LOGGER::warn
24+
);
7925

8026
/**
8127
* ローマ字をひらがなに変換する
8228
* @param romaji ローマ字
8329
* @return ひらがな
8430
*/
8531
public static String romajiToHiragana(String romaji) {
86-
StringBuilder hiragana = new StringBuilder();
87-
int i = 0;
88-
while (i < romaji.length()) {
89-
boolean found = false;
90-
// ↓ MAGIC NUMBER !!!!
91-
for (int j = 4; j >= 1; j--) {
92-
if (!(i + j <= romaji.length())) continue;
93-
String substring = romaji.substring(i, i + j);
94-
95-
if (!hiraganaMap.containsKey(substring)) continue;
96-
hiragana.append(hiraganaMap.get(substring)[0]);
97-
i += j + Integer.parseInt(hiraganaMap.get(substring)[1]);
98-
found = true;
99-
break;
100-
}
101-
if (!found) {
102-
hiragana.append(romaji.charAt(i));
103-
i++;
104-
}
105-
}
106-
return hiragana.toString();
32+
return ROMAJI_TO_HIRAGANA.convert(romaji);
10733
}
10834

10935
/**
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package io.github.meatwo310.tsukichat.util;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.nio.charset.StandardCharsets;
8+
import java.util.LinkedHashMap;
9+
import java.util.Map;
10+
import java.util.Objects;
11+
12+
/**
13+
* ローマ字からひらがなへの変換を行うクラス。
14+
* Minecraft / Forge に依存しない純粋なJavaクラス。
15+
*/
16+
public class RomajiToHiragana {
17+
18+
/**
19+
* 変換テーブルの1エントリ。
20+
* @param hiragana 変換先のひらがな(またはそのまま出力する文字列)
21+
* @param backtrack 巻き戻り文字数(通常0、促音などで負の値)
22+
*/
23+
record Entry(String hiragana, int backtrack) {}
24+
25+
private final Map<String, Entry> conversionTable;
26+
private final int maxKeyLength;
27+
28+
RomajiToHiragana(Map<String, Entry> conversionTable) {
29+
this.conversionTable = Map.copyOf(conversionTable);
30+
this.maxKeyLength = conversionTable.keySet().stream()
31+
.mapToInt(String::length)
32+
.max()
33+
.orElse(0);
34+
}
35+
36+
/**
37+
* リソースファイルから変換テーブルを読み込んでインスタンスを生成する。
38+
*
39+
* <p>ファイル形式(1行ごと):</p>
40+
* <ul>
41+
* <li>{@code ローマ字:ひらがな} — 巻き戻りなし</li>
42+
* <li>{@code ローマ字:ひらがな:巻き戻り数} — 促音など巻き戻りあり</li>
43+
* </ul>
44+
*
45+
* @param resourcePath クラスパス上のリソースパス
46+
* @param logger 警告ログの出力先(nullの場合はログを出力しない)
47+
* @return 生成されたインスタンス
48+
* @throws IllegalArgumentException リソースが見つからない場合
49+
* @throws RuntimeException 読み込み中にIOエラーが発生した場合
50+
*/
51+
public static RomajiToHiragana fromResource(String resourcePath, Logger logger) {
52+
InputStream stream = RomajiToHiragana.class.getResourceAsStream(resourcePath);
53+
if (stream == null) {
54+
throw new IllegalArgumentException("Resource not found: " + resourcePath);
55+
}
56+
57+
LinkedHashMap<String, Entry> table = new LinkedHashMap<>();
58+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
59+
String line;
60+
while ((line = reader.readLine()) != null) {
61+
if (line.isBlank()) continue;
62+
parseLine(line, resourcePath, table, logger);
63+
}
64+
} catch (IOException e) {
65+
throw new RuntimeException("Failed to read resource: " + resourcePath, e);
66+
}
67+
68+
return new RomajiToHiragana(table);
69+
}
70+
71+
/**
72+
* ローマ字をひらがなに変換する。
73+
* 変換テーブルに一致しない文字はそのまま出力される。
74+
*
75+
* @param romaji 変換元のローマ字文字列
76+
* @return 変換後のひらがな文字列
77+
*/
78+
public String convert(String romaji) {
79+
StringBuilder result = new StringBuilder();
80+
int pos = 0;
81+
82+
while (pos < romaji.length()) {
83+
Entry matched = null;
84+
int matchedLength = 0;
85+
86+
// 最長一致: 長い候補から順に試す
87+
int maxLen = Math.min(maxKeyLength, romaji.length() - pos);
88+
for (int len = maxLen; len >= 1; len--) {
89+
String candidate = romaji.substring(pos, pos + len);
90+
Entry entry = conversionTable.get(candidate);
91+
if (entry != null) {
92+
matched = entry;
93+
matchedLength = len;
94+
break;
95+
}
96+
}
97+
98+
if (matched != null) {
99+
result.append(matched.hiragana());
100+
pos += matchedLength + matched.backtrack();
101+
} else {
102+
// 一致なし: 1文字そのまま出力
103+
result.append(romaji.charAt(pos));
104+
pos++;
105+
}
106+
}
107+
108+
return result.toString();
109+
}
110+
111+
/**
112+
* 変換テーブルの1行をパースしてテーブルに追加する。
113+
*/
114+
private static void parseLine(String line, String resourceName,
115+
Map<String, Entry> table, Logger logger) {
116+
String[] parts = line.split(":");
117+
if (parts.length == 2) {
118+
table.put(parts[0], new Entry(parts[1], 0));
119+
} else if (parts.length == 3) {
120+
try {
121+
int backtrack = Integer.parseInt(parts[2]);
122+
table.put(parts[0], new Entry(parts[1], backtrack));
123+
} catch (NumberFormatException e) {
124+
if (logger != null) {
125+
logger.warn("Could not parse backtrack value in {}; line ignored: {}", resourceName, line);
126+
}
127+
}
128+
} else {
129+
if (logger != null) {
130+
logger.warn("Invalid line in {}; line ignored: {}", resourceName, line);
131+
}
132+
}
133+
}
134+
135+
/**
136+
* ログ出力用のインターフェース。
137+
* slf4j等の具体的なロガーに依存しないようにするための抽象化。
138+
*/
139+
@FunctionalInterface
140+
public interface Logger {
141+
void warn(String format, Object... args);
142+
}
143+
}

0 commit comments

Comments
 (0)