wangzhibo
4 天以前 ae6f40460dcd56af6c5f60ba52c883854c3bac55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/*  Copyright (C) 2022-2024 José Rebelo
 
    This file is part of Gadgetbridge.
 
    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
 
    Gadgetbridge is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
 
    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.language.impl;
 
import java.text.Normalizer;
import java.util.Optional;
 
import nodomain.freeyourgadget.gadgetbridge.util.language.Transliterator;
 
// Implements Revised Romanization of Korean as well as we can without understanding any grammar.
//
// https://en.wikipedia.org/wiki/Revised_Romanization_of_Korean
public class KoreanTransliterator implements Transliterator {
    // https://en.wikipedia.org/wiki/Hangul_Jamo_%28Unicode_block%29
    private static final char JAMO_BLOCK_START = 0x1100;
    private static final char JAMO_BLOCK_END = 0x11FF;
    // https://en.wikipedia.org/wiki/Hangul_Syllables
    private static final char SYLLABLES_BLOCK_START = 0xAC00;
    private static final char SYLLABLES_BLOCK_END = 0xD7A3;
    // https://en.wikipedia.org/wiki/Hangul_Compatibility_Jamo
    private static final char COMPAT_JAMO_BLOCK_START = 0x3131;
    private static final char COMPAT_JAMO_BLOCK_END = 0x318E;
 
    // Returns whether a char is in the given block. Both bounds are inclusive.
    private static boolean inRange(char c, char start, char end) {
        return c >= start && c <= end;
    }
 
    // User input consisting of isolated jamo is usually mapped to the KS X 1001 compatibility
    // block, but jamo resulting from decomposed syllables are mapped to the modern one. This
    // function maps compat jamo to modern ones where possible and returns all other characters
    // unmodified.
    //
    // https://en.wikipedia.org/wiki/Hangul_Compatibility_Jamo
    // https://en.wikipedia.org/wiki/Hangul_Jamo_%28Unicode_block%29
    private static char decompatJamo(char jamo) {
        // KS X 1001 Hangul filler, not used in modern Unicode. A useful landmark in the
        // compatibility jamo block.
        // https://en.wikipedia.org/wiki/KS_X_1001#Hangul_Filler
        final char HANGUL_FILLER = 0x3164;
 
        // Don't do anything to characters outside the compatibility jamo block.
        if (!inRange(jamo, COMPAT_JAMO_BLOCK_START, COMPAT_JAMO_BLOCK_END)) { return jamo; }
 
        // Vowels are contiguous, in the same order, and unambiguous, so it's a simple offset.
        if (jamo >= 0x314F && jamo < HANGUL_FILLER) {
            return (char)(jamo - 0x1FEE);
        }
 
        // Consonants are organized differently. No clean way to do this.
        //
        // The compatibility jamo block doesn't distinguish between Choseong (leading) and Jongseong
        // (final) positions, but the modern block does. We map to Choseong here.
        switch (jamo) {
            case 0x3131: return 0x1100;     // ㄱ
            case 0x3132: return 0x1101;     // ㄲ
            case 0x3134: return 0x1102;     // ㄴ
            case 0x3137: return 0x1103;     // ㄷ
            case 0x3138: return 0x1104;     // ㄸ
            case 0x3139: return 0x1105;     // ㄹ
            case 0x3141: return 0x1106;     // ㅁ
            case 0x3142: return 0x1107;     // ㅂ
            case 0x3143: return 0x1108;     // ㅃ
            case 0x3145: return 0x1109;     // ㅅ
            case 0x3146: return 0x110A;     // ㅆ
            case 0x3147: return 0x110B;     // ㅇ
            case 0x3148: return 0x110C;     // ㅈ
            case 0x3149: return 0x110D;     // ㅉ
            case 0x314A: return 0x110E;     // ㅊ
            case 0x314B: return 0x110F;     // ㅋ
            case 0x314C: return 0x1110;     // ㅌ
            case 0x314D: return 0x1111;     // ㅍ
            case 0x314E: return 0x1112;     // ㅎ
        }
 
        // The rest of the compatibility block consists of archaic compounds that are unlikely to be
        // encountered in modern systems. Just leave them alone.
        return jamo;
    }
 
    // Transliterates jamo one at a time. Returns its input if it isn't in the modern jamo block.
    private static String transliterateSingleJamo(char jamo) {
        jamo = decompatJamo(jamo);
 
        switch (jamo) {
            // Choseong (leading position consonants)
            case 0x1100: return "g";    // ㄱ
            case 0x1101: return "kk";   // ㄲ
            case 0x1102: return "n";    // ㄴ
            case 0x1103: return "d";    // ㄷ
            case 0x1104: return "tt";   // ㄸ
            case 0x1105: return "r";    // ㄹ
            case 0x1106: return "m";    // ㅁ
            case 0x1107: return "b";    // ㅂ
            case 0x1108: return "pp";   // ㅃ
            case 0x1109: return "s";    // ㅅ
            case 0x110A: return "ss";   // ㅆ
            case 0x110B: return "";     // ㅇ
            case 0x110C: return "j";    // ㅈ
            case 0x110D: return "jj";   // ㅉ
            case 0x110E: return "ch";   // ㅊ
            case 0x110F: return "k";    // ㅋ
            case 0x1110: return "t";    // ㅌ
            case 0x1111: return "p";    // ㅍ
            case 0x1112: return "h";    // ㅎ
            // Jungseong (vowels)
            case 0x1161: return "a";    // ㅏ
            case 0x1162: return "ae";   // ㅐ
            case 0x1163: return "ya";   // ㅑ
            case 0x1164: return "yae";  // ㅒ
            case 0x1165: return "eo";   // ㅓ
            case 0x1166: return "e";    // ㅔ
            case 0x1167: return "yeo";  // ㅕ
            case 0x1168: return "ye";   // ㅖ
            case 0x1169: return "o";    // ㅗ
            case 0x116A: return "wa";   // ㅘ
            case 0x116B: return "wae";  // ㅙ
            case 0x116C: return "oe";   // ㅚ
            case 0x116D: return "yo";   // ㅛ
            case 0x116E: return "u";    // ㅜ
            case 0x116F: return "wo";   // ㅝ
            case 0x1170: return "we";   // ㅞ
            case 0x1171: return "wi";   // ㅟ
            case 0x1172: return "yu";   // ㅠ
            case 0x1173: return "eu";   // ㅡ
            case 0x1174: return "ui";   // ㅢ
            case 0x1175: return "i";    // ㅣ
            // Jongseong (final position consonants)
            case 0x11A8: return "k";    // ㄱ
            case 0x11A9: return "k";    // ㄲ
            case 0x11AB: return "n";    // ㄴ
            case 0x11AE: return "t";    // ㄷ
            case 0x11AF: return "l";    // ㄹ
            case 0x11B7: return "m";    // ㅁ
            case 0x11B8: return "p";    // ㅂ
            case 0x11BA: return "t";    // ㅅ
            case 0x11BB: return "t";    // ㅆ
            case 0x11BC: return "ng";   // ㅇ
            case 0x11BD: return "t";    // ㅈ
            case 0x11BE: return "t";    // ㅊ
            case 0x11BF: return "k";    // ㅋ
            case 0x11C0: return "t";    // ㅌ
            case 0x11C1: return "p";    // ㅍ
            case 0x11C2: return "t";    // ㅎ
        }
 
        // Input was not jamo.
        return String.valueOf(jamo);
    }
 
    // Some combinations of ending jamo in one syllable and initial jamo in the next are romanized
    // irregularly. These exceptions are called "special provisions". In cases where multiple
    // romanizations are permitted, we use the one that's least commonly used elsewhere.
    //
    // Returns no value if either character is not in the modern jamo block, or if there is no
    // special provision for that pair of jamo.
    public static Optional<String> transliterateSpecialProvisions(char previousEnding, char nextInitial) {
        // Special provisions only apply if both characters are in the modern jamo block.
        if (!inRange(previousEnding, JAMO_BLOCK_START, JAMO_BLOCK_END)) { return Optional.empty(); }
        if (!inRange(nextInitial, JAMO_BLOCK_START, JAMO_BLOCK_END)) { return Optional.empty(); }
 
        // Jongseong (final position) ㅎ has a number of special provisions.
        if (previousEnding == 0x11C2) { // ㅎ
            switch (nextInitial) {
                case 0x110B: return Optional.of("h");       // ㅇ
                case 0x1100: return Optional.of("k");       // ㄱ
                case 0x1102: return Optional.of("nn");      // ㄴ
                case 0x1103: return Optional.of("t");       // ㄷ
                case 0x1105: return Optional.of("nn");      // ㄹ
                case 0x1106: return Optional.of("nm");      // ㅁ
                case 0x1107: return Optional.of("p");       // ㅂ
                case 0x1109: return Optional.of("hs");      // ㅅ
                case 0x110C: return Optional.of("ch");      // ㅈ
                case 0x1112: return Optional.of("t");       // ㅎ
                default: return Optional.empty();
            }
        }
 
        // Otherwise, special provisions are denser when grouped by the second jamo.
        switch (nextInitial) {
            case 0x1100: // ㄱ
                switch (previousEnding) {
                    case 0x11AB: return Optional.of("n-g"); // ㄴ
                    default: return Optional.empty();
                }
            case 0x1102: // ㄴ
                switch (previousEnding) {
                    case 0x11A8: return Optional.of("ngn"); // ㄱ
                    case 0x11AE:                            // ㄷ
                    case 0x11BA:                            // ㅅ
                    case 0x11BD:                            // ㅈ
                    case 0x11BE:                            // ㅊ
                    case 0x11C0:                            // ㅌ
                        return Optional.of("nn");
                    case 0x11AF: return Optional.of("ll");  // ㄹ
                    case 0x11B8: return Optional.of("mn");  // ㅂ
                    default: return Optional.empty();
                }
            case 0x1105: // ㄹ
                switch (previousEnding) {
                    case 0x11A8:                            // ㄱ
                    case 0x11AB:                            // ㄴ
                    case 0x11AF:                            // ㄹ
                        return Optional.of("ll");
                    case 0x11AE:                            // ㄷ
                    case 0x11BA:                            // ㅅ
                    case 0x11BD:                            // ㅈ
                    case 0x11BE:                            // ㅊ
                    case 0x11C0:                            // ㅌ
                        return Optional.of("nn");
                    case 0x11B7:                            // ㅁ
                    case 0x11B8:                            // ㅂ
                        return Optional.of("mn");
                    case 0x11BC: return Optional.of("ngn"); // ㅇ
                    default: return Optional.empty();
                }
            case 0x1106: // ㅁ
                switch (previousEnding) {
                    case 0x11A8: return Optional.of("ngm"); // ㄱ
                    case 0x11AE:                            // ㄷ
                    case 0x11BA:                            // ㅅ
                    case 0x11BD:                            // ㅈ
                    case 0x11BE:                            // ㅊ
                    case 0x11C0:                            // ㅌ
                        return Optional.of("nm");
                    case 0x11B8: return Optional.of("mm");  // ㅂ
                    default: return Optional.empty();
                }
            case 0x110B: // ㅇ
                switch (previousEnding) {
                    case 0x11A8: return Optional.of("g");   // ㄱ
                    case 0x11AE: return Optional.of("d");   // ㄷ
                    case 0x11AF: return Optional.of("r");   // ㄹ
                    case 0x11B8: return Optional.of("b");   // ㅂ
                    case 0x11BA: return Optional.of("s");   // ㅅ
                    case 0x11BC: return Optional.of("ng-"); // ㅇ
                    case 0x11BD: return Optional.of("j");   // ㅈ
                    case 0x11BE: return Optional.of("ch");  // ㅊ
                    default: return Optional.empty();
                }
            case 0x110F: // ㅋ
                switch (previousEnding) {
                    case 0x11A8: return Optional.of("k-k"); // ㄱ
                    default: return Optional.empty();
                }
            case 0x1110: // ㅌ
                switch (previousEnding) {
                    case 0x11AE:                            // ㄷ
                    case 0x11BA:                            // ㅅ
                    case 0x11BD:                            // ㅈ
                    case 0x11BE:                            // ㅊ
                    case 0x11C0:                            // ㅌ
                        return Optional.of("t-t");
                    default: return Optional.empty();
                }
            case 0x1111: // ㅍ
                switch (previousEnding) {
                    case 0x11B8: return Optional.of("p-p"); // ㅂ
                    default: return Optional.empty();
                }
            default: return Optional.empty();
        }
    }
 
    // Decompose a syllable into several jamo. Returns its input if that isn't possible.
    public static char[] decompose(char syllable) {
        String normalized = Normalizer.normalize(String.valueOf(syllable), Normalizer.Form.NFD);
        return normalized.toCharArray();
    }
 
    // Transliterate any Hangul in the given string. Leaves any non-Hangul characters unmodified.
    @Override
    public String transliterate(String txt) {
        if (txt == null || txt.isEmpty()) {
            return txt;
        }
 
        // Most of the bulk of these loops is for handling special provisions - situations where the
        // last jamo of one syllable and the first of the next need to be romanized as a pair in an
        // irregular way.
        StringBuilder builder = new StringBuilder();
        boolean nextInitialJamoConsumed = false;
        char[] syllables = txt.toCharArray();
        for (int i = 0; i < syllables.length; i++) {
            char thisSyllable = syllables[i];
            // If this isn't in any of the Hangul blocks we know about, emit it as-is.
            if (!inRange(thisSyllable, JAMO_BLOCK_START, JAMO_BLOCK_END)
                    && !inRange(thisSyllable, SYLLABLES_BLOCK_START, SYLLABLES_BLOCK_END)
                    && !inRange(thisSyllable, COMPAT_JAMO_BLOCK_START, COMPAT_JAMO_BLOCK_END)) {
                builder.append(thisSyllable);
                continue;
            }
 
            char[] theseJamo = decompose(thisSyllable);
            for (int j = 0; j < theseJamo.length; j++) {
                char thisJamo = theseJamo[j];
 
                // If we already transliterated the first jamo of this syllable as part of a special
                // provision, skip it. Otherwise, handle it in the unconditional else branch.
                if (j == 0 && nextInitialJamoConsumed) {
                    nextInitialJamoConsumed = false;
                    continue;
                }
 
                // If this is the last jamo of this syllable and not the last syllable of the
                // string, check for special provisions. If the next char is whitespace or not
                // Hangul, it's the responsibility of transliterateSpecialProvisions() to return no
                // value.
                if (j == theseJamo.length - 1 && i < syllables.length - 1) {
                    char nextSyllable = syllables[i + 1];
                    char nextJamo = decompose(nextSyllable)[0];
                    Optional<String> specialProvision = transliterateSpecialProvisions(thisJamo, nextJamo);
                    if (specialProvision.isPresent()) {
                        builder.append(specialProvision.get());
                        nextInitialJamoConsumed = true;
                    } else {
                        // No special provision applies. Transliterate in isolation.
                        builder.append(transliterateSingleJamo(thisJamo));
                    }
                    continue;
                }
 
                // Everything else is transliterated in isolation.
                builder.append(transliterateSingleJamo(thisJamo));
            }
        }
 
        return builder.toString();
    }
}