romanizer.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. # @package ext.korean
  2. #
  3. __doc__ = """
  4. Korean transcription functions.
  5. Ported from K-Romanizer: https://library.princeton.edu/eastasian/k-romanizer
  6. Only script-to-Roman is possible for Korean.
  7. Note that Korean Romanization must be done separately for strings containing
  8. only personal names and strings that do not contain personal names, due to
  9. ambiguities in the language. A non-deterministic approach using machine
  10. learning that separates words depending on context is being attempted by other
  11. parties, and it may be possible to eventually integrate such services here in
  12. the future, technology and licensing permitting. At the moment there are no
  13. such plans.
  14. Many thanks to Hyoungbae Lee for kindly providing the original K-Romanizer
  15. program and assistance in porting it to Python.
  16. """
  17. import logging
  18. import re
  19. from os import path
  20. from csv import reader
  21. from scriptshifter.exceptions import BREAK
  22. from scriptshifter.hooks.korean import KCONF
  23. PWD = path.dirname(path.realpath(__file__))
  24. CP_MIN = 44032
  25. # Buid FKR index for better logging.
  26. with open(path.join(PWD, "FKR_index.csv"), newline='') as fh:
  27. csv = reader(fh)
  28. FKR_IDX = {row[0]: row[2] for row in csv}
  29. logger = logging.getLogger(__name__)
  30. def s2r_nonames_post_config(ctx):
  31. """ Romanize a regular string NOT containing personal names. """
  32. ctx.dest, ctx.warnings = _romanize_nonames(
  33. ctx.src, ctx.options)
  34. return BREAK
  35. def s2r_names_post_config(ctx):
  36. """
  37. Romanize a string containing ONLY Korean personal names.
  38. One or more names can be transcribed. A comma or middle dot (U+00B7) is
  39. to be used as separator for multiple names.
  40. """
  41. ctx.dest, ctx.warnings = _romanize_names(ctx.src, ctx.options)
  42. return BREAK
  43. def _romanize_nonames(src, options):
  44. """ Main Romanization function for non-name strings. """
  45. # FKR038: Convert Chinese characters to Hangul
  46. if options.get("hancha", True):
  47. kor = _hancha2hangul(_marc8_hancha(src))
  48. else:
  49. kor = src
  50. # Replace ideographic spaces with ASCII space.
  51. kor = re.sub(r"\s+", " ", kor)
  52. kor = f" {kor} "
  53. # FKR039: Replace Proper name with spaces in advance
  54. # FKR040: Replace Proper name with a hyphen in advance
  55. # FKR041: Romanize names of Hangul consonants
  56. for i in range(39, 42):
  57. _fkr_log(i)
  58. kor = _replace_map(kor, KCONF[f"fkr{i:03}"])
  59. # NOTE This is slightly different from LL 929-930 in that it doesn't
  60. # result in double spaces.
  61. kor = kor.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
  62. # This is more compact but I'm unsure if the replacement order is kept.
  63. # kor = kor.replace({"\r\n": " ", "\r": " ", "\n": " "})
  64. rom = _romanize_oclc_auto(kor)
  65. logger.debug(f"Before capitalization: {rom}")
  66. # FKR042: Capitalize all first letters
  67. if options["capitalize"] == "all":
  68. rom = _capitalize(rom)
  69. # FKR043: Capitalize the first letter
  70. elif options["capitalize"] == "first":
  71. rom = rom[0].upper() + rom[1:]
  72. # FKR044: Ambiguities
  73. ambi = re.sub("[,.\";: ]+", " ", rom)
  74. # TODO Decide what to do with these. There is no facility for outputting
  75. # warnings or notes to the user yet.
  76. warnings = []
  77. _fkr_log(45)
  78. for exp, warn in KCONF["fkr045"].items():
  79. if exp in ambi:
  80. warnings.append(ambi if warn == "" else warn)
  81. if rom:
  82. rom = rom.replace("kkk", "kk")
  83. return rom, warnings
  84. def _romanize_names(src, options):
  85. """
  86. Main Romanization function for names.
  87. Separate and romanize multiple names sepearated by comma or middle dot.
  88. K-Romanizer: KorNameRom20
  89. """
  90. rom_ls = []
  91. warnings = []
  92. if "," in src and "·" in src:
  93. warnings.append(
  94. "both commas and middle dots are being used to separate "
  95. "names. Only one of the two types should be used, or "
  96. "unexpected results may occur.")
  97. kor_ls = src.split(",") if "," in src else src.split("·")
  98. for kor in kor_ls:
  99. rom, _warnings = _romanize_name(kor.strip(), options)
  100. rom_ls.append(rom)
  101. warnings.extend(_warnings)
  102. return ", ".join(rom_ls), warnings
  103. def _romanize_name(src, options):
  104. warnings = []
  105. # FKR001: Conversion, Family names in Chinese (dealing with 金 and 李)
  106. # FKR002: Family names, Initial sound law
  107. replaced = False
  108. for ss, r in KCONF["fkr001-002"]:
  109. if replaced:
  110. break
  111. for s in ss:
  112. if src.startswith(s):
  113. src = r + src[1:]
  114. replaced = True
  115. break
  116. # FKR003: First name, Chinese Character Conversion
  117. src = _hancha2hangul(_marc8_hancha(src))
  118. if re.search("[a-zA-Z0-9]", src):
  119. warnings.append(f"{src} is not a recognized personal name.")
  120. return "", warnings
  121. # `parsed` can either be a modified Korean string with markers, or in case
  122. # of a foreign name, the final romanized name.
  123. parsed, _warnings = _parse_kor_name(re.sub(r"\s{2,}", " ", src.strip()))
  124. if len(_warnings):
  125. warnings += _warnings
  126. if parsed:
  127. if "~" in parsed:
  128. lname, fname = parsed.split("~", 1)
  129. fname_rom = _kor_fname_rom(fname)
  130. lname_rom_ls = [_kor_lname_rom(n) for n in lname.split("+")]
  131. if not any(lname_rom_ls):
  132. warnings.append(f"{parsed} is not a recognized Korean name.")
  133. return "", warnings
  134. lname_rom = " ".join(lname_rom_ls)
  135. # Add comma after the last name for certain MARC fields.
  136. marc_field_str = options.get("marc_field", "0")
  137. try:
  138. marc_field = int(marc_field_str)
  139. except TypeError:
  140. raise ValueError(
  141. f"{marc_field_str} is not a valid MARC field code.")
  142. if marc_field in (100, 600, 700, 800):
  143. rom = f"{lname_rom}, {fname_rom}"
  144. else:
  145. rom = f"{lname_rom} {fname_rom}"
  146. if False:
  147. # TODO add option for authoritative name.
  148. rom_ls = rom.rsplit(" ", 1)
  149. rom = ", ".join(rom_ls)
  150. return rom, warnings
  151. else:
  152. warnings.append("Romanized as a foreign name.")
  153. return parsed, warnings
  154. warnings.append(f"{src} is not a recognized Korean name.")
  155. return "", warnings
  156. def _parse_kor_name(src):
  157. parsed = None
  158. warnings = []
  159. # FKR004: Check first two characters. Two-syllable family name or not?
  160. two_syl_fname = False
  161. for ptn in KCONF["fkr004"]:
  162. if src.startswith(ptn):
  163. two_syl_fname = True
  164. break
  165. src_len = len(src)
  166. # FKR005: Error if more than 7 syllables
  167. if src_len > 7 or src_len < 2 or " " in src[3:]:
  168. return _kor_corp_name_rom(src), warnings
  169. ct_spaces = src.count(" ")
  170. # FKR0006: Error if more than 2 spaces
  171. if ct_spaces > 2:
  172. warnings.append("ERROR: not a name (too many spaces)")
  173. return parsed, warnings
  174. # FKR007: 2 spaces (two family names)
  175. if ct_spaces == 2:
  176. parsed = src.replace(" ", "+", 1).replace(" ", "~", 1)
  177. elif ct_spaces == 1:
  178. # FKR008: 1 space (2nd position)
  179. if src[1] == " ":
  180. parsed = src.replace(" ", "~")
  181. # FKR009: 1 space (3nd position)
  182. if src[2] == " ":
  183. if two_syl_fname:
  184. parsed = "+" + src.replace(" ", "~")
  185. # FKR010: When there is no space
  186. else:
  187. if src_len == 2:
  188. parsed = src[0] + "~" + src[1:]
  189. elif src_len > 2:
  190. if two_syl_fname:
  191. parsed = src[:1] + "~" + src[2:]
  192. else:
  193. parsed = src[0] + "~" + src[1:]
  194. return parsed, warnings
  195. def _kor_corp_name_rom(src):
  196. chu = yu = 0
  197. if src.startswith("(주) "):
  198. src = src[4:]
  199. chu = "L"
  200. if src.endswith(" (주)"):
  201. src = src[:-4]
  202. chu = "R"
  203. if src.startswith("(유) "):
  204. src = src[4:]
  205. yu = "L"
  206. if src.endswith(" (유)"):
  207. src = src[:-4]
  208. yu = "R"
  209. rom_tok = []
  210. for tok in src.split(" "):
  211. rom_tok.append(_romanize_oclc_auto(tok))
  212. rom = _capitalize(" ".join(rom_tok))
  213. if chu == "L":
  214. rom = "(Chu) " + rom
  215. elif chu == "R":
  216. rom = rom + " (Chu)"
  217. if yu == "L":
  218. rom = "(Yu) " + rom
  219. elif yu == "R":
  220. rom = rom + " (Yu)"
  221. # FKR035: Replace established names
  222. rom = _replace_map(rom, KCONF["fkr035"])
  223. return rom
  224. def _romanize_oclc_auto(kor):
  225. # FKR050: Starts preprocessing symbol
  226. _fkr_log(50)
  227. for rname, rule in KCONF["fkr050"].items():
  228. logger.debug(f"Applying fkr050[{rname}]")
  229. kor = _replace_map(kor, rule)
  230. # See https://github.com/lcnetdev/scriptshifter/issues/19
  231. kor = re.sub("제([0-9])", "제 \\1", kor)
  232. # FKR052: Replace Che+number
  233. _fkr_log(52)
  234. for rname, rule in KCONF["fkr052"].items():
  235. logger.debug(f"Applying fkr052[{rname}]")
  236. kor = _replace_map(kor, rule)
  237. # Strip end and multiple whitespace.
  238. kor = re.sub(r"\s{2,}", " ", kor.strip())
  239. kor = kor.replace("^", " GLOTTAL ")
  240. logger.debug(f"Korean before romanization: {kor}")
  241. rom_ls = []
  242. for word in kor.split(" "):
  243. rom_ls.append(_kor_rom(word))
  244. rom = " ".join(rom_ls)
  245. # FKR059: Apply glottalization
  246. rom = _replace_map(
  247. f" {rom.strip()} ", {" GLOTTAL ": "", "*": "", "^": ""})
  248. # FKR060: Process number + -년/-년도/-년대
  249. # TODO Add leading whitespace as per L1221? L1202 already added one.
  250. rom = _replace_map(rom, KCONF["fkr060"])
  251. rom = re.sub(r"\s{2,}", " ", f" {rom.strip()} ")
  252. # FKR061: Jurisdiction (시)
  253. # FKR062: Historical place names
  254. # FKR063: Jurisdiction (국,도,군,구)
  255. # FKR064: Temple names of Kings, Queens, etc. (except 조/종)
  256. # FKR065: Frequent historical names
  257. for i in range(61, 66):
  258. _fkr_log(i)
  259. rom = _replace_map(rom, KCONF[f"fkr{i:03}"])
  260. # FKR066: Starts restore symbols
  261. _fkr_log(66)
  262. for rname, rule in KCONF["fkr066"].items():
  263. logger.debug(f"Applying FKR066[{rname}]")
  264. rom = _replace_map(rom, rule)
  265. # Remove spaces from before punctuation signs.
  266. rom = re.sub(r" (?=[,.;:?!])", "", rom.strip())
  267. rom = re.sub(r"\s{2,}", " ", rom)
  268. return rom
  269. # FKR068: Exceptions, Exceptions to initial sound law, Proper names
  270. def _kor_rom(kor):
  271. kor = re.sub(r"\s{2,}", " ", kor.strip())
  272. orig = kor
  273. # FKR069: Irregular sound change list
  274. kor = _replace_map(kor, KCONF["fkr069"])
  275. # FKR070: [n] insertion position mark +
  276. niun = kor.find("+")
  277. if niun > -1:
  278. kor = kor.replace("+", "")
  279. orig = kor
  280. non_kor = 0
  281. cpoints = tuple(ord(c) for c in kor)
  282. for cp in cpoints:
  283. if cp < CP_MIN:
  284. non_kor += 1
  285. kor = kor[1:]
  286. rom_ls = []
  287. if non_kor > 0:
  288. # Rebuild code point list with non_kor removed.
  289. cpoints = tuple(ord(c) for c in kor)
  290. for i in range(len(kor)):
  291. cp = cpoints[i] - CP_MIN
  292. ini = "i" + str(cp // 588)
  293. med = "m" + str((cp // 28) % 21)
  294. fin = "f" + str(cp % 28)
  295. rom_ls.append("#".join((ini, med, fin)))
  296. rom = "~".join(rom_ls)
  297. if len(rom):
  298. rom = rom + "E"
  299. # FKR071: [n] insertion
  300. if niun > -1:
  301. niun_loc = rom.find("~")
  302. # Advance until the niun'th occurrence of ~
  303. # If niun is 0 or 1 the loop will be skipped.
  304. for i in range(niun - 1):
  305. niun_loc = rom.find("~", niun_loc + 1)
  306. rom_niun_a = rom[:niun_loc]
  307. rom_niun_b = rom[niun_loc + 1:]
  308. if re.match("i11#m(?:2|6|12|17|20)", rom_niun_b):
  309. _fkr_log(71)
  310. rom_niun_b = rom_niun_b.replace("i11#m", "i2#m", 1)
  311. # FKR072: [n]+[l] >[l] + [l]
  312. if rom_niun_b.startswith("i5#") and rom_niun_a.endswith("f4"):
  313. _fkr_log(72)
  314. rom_niun_b = rom_niun_b.replace("i5#", "i2", 1)
  315. rom = f"{rom_niun_a}~{rom_niun_b}"
  316. # FKR073: Palatalization: ㄷ+이,ㄷ+여,ㄷ+히,ㄷ+혀
  317. # FKR074: Palatalization: ㅌ+이,ㅌ+히,ㅌ+히,ㅌ+혀
  318. # FKR075: Consonant assimilation ㄱ
  319. # FKR076: Consonant assimilation ㄲ
  320. # FKR077: Consonant assimilation ㄳ : ㄱ,ㄴ,ㄹ,ㅁ,ㅇ
  321. # FKR078: Consonant assimilation ㄴ
  322. # FKR079: Consonant assimilation ㄵ: ㄱ,ㄴ,ㄷ,ㅈ"
  323. # FKR080: Consonant assimilation ㄶ : ㄱ,ㄴ,ㄷ,ㅈ
  324. # FKR081: Consonant assimilation ㄷ
  325. # FKR082: Consonant assimilation ㄹ
  326. # FKR083: Consonant assimilation ㄺ : ㄱ,ㄴ,ㄷ,ㅈ
  327. # FKR084: Consonant assimilation ㄻ : ㄱ,ㄴ,ㄷ,ㅈ
  328. # FKR085: Consonant assimilation ㄼ : ㄱ,ㄴ,ㄷ,ㅈ
  329. # FKR086: Consonant assimilation ㄾ : ㄱ,ㄴ,ㄷ,ㅈ
  330. # FKR087: Consonant assimilation ㄿ : ㄱ,ㄴ,ㄷ,ㅈ
  331. # FKR088: Consonant assimilation ㅀ : ㄱ,ㄴ,ㄷ,ㅈ
  332. # FKR089: Consonant assimilation ㅁ
  333. # FKR090: Consonant assimilation ㅂ
  334. # FKR091: Consonant assimilation ㅄ
  335. # FKR092: Consonant assimilation ㅅ
  336. # FKR093: Consonant assimilation ㅆ
  337. # FKR094: Consonant assimilation ㅇ
  338. # FKR095: Consonant assimilation ㅈ
  339. # FKR096: Consonant assimilation ㅊ
  340. # FKR097: Consonant assimilation ㅋ
  341. # FKR098: Consonant assimilation ㅌ
  342. # FKR099: Consonant assimilation ㅍ
  343. # FKR100: Consonant assimilation ㅎ
  344. # FKR101: digraphic coda + ㅇ: ㄵ,ㄶ,ㄺ,ㄻ,ㄼ,ㄽ,ㄾ,ㄿ,ㅀ
  345. # FKR102: digraphic coda + ㅎ: ㄵ,ㄶ,ㄺ,ㄻ,ㄼ,(ㄽ),ㄾ,ㄿ,ㅀ
  346. # FKR103: Vocalization 1 (except ㄹ+ㄷ, ㄹ+ㅈ 제외) voiced + unvoiced
  347. # FKR104: Vocalization 2 (except ㄹ+ㄷ, ㄹ+ㅈ 제외) unvoiced + voiced
  348. # FKR105: Vocalization 3 (ㄹ+ㄷ, ㄹ+ㅈ)
  349. # FKR106: Final sound law
  350. # FKR107: Exception for '쉬' = shi
  351. # FKR108: Exception for 'ㄴㄱ'= n'g
  352. for fkr_i in range(73, 109):
  353. _fkr_log(fkr_i)
  354. _bk = rom
  355. rom = _replace_map(rom, KCONF[f"fkr{fkr_i:03}"])
  356. if _bk != rom:
  357. logger.debug(f"FKR{fkr_i} substitution: {rom} (was: {_bk})")
  358. # FKR109: Convert everything else
  359. _fkr_log(109)
  360. for pos, data in KCONF["fkr109"].items():
  361. rom = _replace_map(rom, data)
  362. # FKR110: Convert symbols
  363. rom = _replace_map(rom, {"#": "", "~": ""})
  364. if non_kor > 0:
  365. # Modified from K-Romanizer:1727 in that it does not append a hyphen
  366. # if the whole word is non-Korean.
  367. rom = f"{orig[:non_kor]}-{rom}" if len(rom) else orig
  368. # FKR111: ㄹ + 모음/ㅎ/ㄹ, ["lr","ll"] must be in the last of the array
  369. rom = _replace_map(rom, KCONF["fkr111"])
  370. # FKR112: Exceptions to initial sound law
  371. is_non_kor = False
  372. # FKR113: Check loan words by the first 1 letter
  373. # FKR114: Check loan words by the first 2 letters
  374. # FKR115: Check loan words by the first 3 letters
  375. if orig.startswith(tuple(KCONF["fkr113-115"])):
  376. is_non_kor = True
  377. # FKR116: Exceptions to initial sound law - particles
  378. is_particle = False
  379. if orig.startswith(tuple(KCONF["fkr116"]["particles"])):
  380. is_particle = True
  381. if len(orig) > 1 and not is_non_kor and not is_particle:
  382. if rom.startswith(tuple(KCONF["fkr116"]["replace_initials"].keys())):
  383. rom = _replace_map(rom, KCONF["fkr116"]["replace_initials"])
  384. # FKR117: Proper names _StringPoper Does not work because of breves
  385. if (
  386. # FKR118
  387. orig in KCONF["fkr118"] or
  388. # FKR119
  389. orig in KCONF["fkr119"]["word"] or
  390. (
  391. orig[:-1] in KCONF["fkr119"]["word"] and
  392. orig.endswith(tuple(KCONF["fkr119"]["suffix"]))
  393. ) or
  394. # FKR120
  395. orig.endswith(tuple(KCONF["fkr120"]))):
  396. rom = rom[0].upper() + rom[1:]
  397. # FKR121: Loan words beginning with L
  398. if f" {orig} " in KCONF["fkr121"]:
  399. rom = _replace_map(rom[0], {"R": "L", "r": "l"}) + rom[1:]
  400. # @TODO Move this to a generic normalization step (not only for K)
  401. rom = _replace_map(rom, {"ŏ": "ŏ", "ŭ": "ŭ", "Ŏ": "Ŏ", "Ŭ": "Ŭ"})
  402. return rom
  403. def _marc8_hancha(data):
  404. # FKR142: Chinese character list
  405. _fkr_log(142)
  406. return _replace_map(data, KCONF["fkr142"])
  407. def _hancha2hangul(data):
  408. data = " " + data.replace("\n", "\n ")
  409. # FKR143: Process exceptions first
  410. # FKR144: Apply initial sound law (Except: 列, 烈, 裂, 劣)
  411. # FKR145: Simplified characters, variants
  412. # FKR146: Some characters from expanded list
  413. # FKR147: Chinese characters 1-500 車=차
  414. # FKR148: Chinese characters 501-750 串=관
  415. # FKR149: Chinese characters 751-1000 金=금, 娘=랑
  416. # FKR150: Chinese characters 1001-1250
  417. # FKR151: Chinese characters 1251-1500 제외: 列, 烈, 裂, 劣
  418. # FKR152: Chinese characters 1501-1750 제외: 律, 率, 栗, 慄
  419. # FKR153: Chinese characters 1751-2000
  420. # FKR154: 不,Chinese characters 2001-2250 제외: 不
  421. # FKR155: Chinese characters 2251-2500 塞=색
  422. # FKR156: Chinese characters 2501-2750
  423. # FKR157: Chinese characters 2751-3000
  424. # FKR158: Chinese characters 3001-2250
  425. # FKR159: Chinese characters 3251-3500
  426. # FKR160: Chinese characters 3501-3750
  427. # FKR161: Chinese characters 3751-4000
  428. # FKR162: Chinese characters 4001-4250
  429. # FKR163: Chinese characters 4251-4500
  430. # FKR164: Chinese characters 4501-4750
  431. # FKR165: Chinese characters 4751-5000
  432. # FKR166: Chinese characters 5001-5250
  433. # FKR167: Chinese characters 5251-5500
  434. # FKR168: Chinese characters 5501-5750
  435. # FKR169: Chinese characters 5751-5978
  436. # FKR170: Chinese characters 일본Chinese characters
  437. for i in range(143, 171):
  438. _fkr_log(i)
  439. data = _replace_map(data, KCONF[f"fkr{i}"])
  440. # FKR171: Chinese characters 不(부)의 발음 처리
  441. # Write down indices of occurrences of "不"
  442. idx = [i for i, item in enumerate(data) if item == "不"]
  443. for i in idx:
  444. val = ord(data[i + 1])
  445. if (val > 45795 and val < 46384) or (val > 51087 and val < 51676):
  446. data = data.replace("不", "부", 1)
  447. else:
  448. data = data.replace("不", "불", 1)
  449. # FKR172: Chinese characters 列(렬)의 발음 처리
  450. # FKR173: Chinese characters 烈(렬)의 발음 처리
  451. # FKR174: Chinese characters 裂(렬)의 발음 처리
  452. # FKR175: Chinese characters 劣(렬)의 발음 처리
  453. # FKR176: Chinese characters 律(률)의 발음 처리
  454. # FKR177: Chinese characters 率(률)의 발음 처리
  455. # FKR178: Chinese characters 慄(률)의 발음 처리
  456. # FKR179: Chinese characters 栗(률)의 발음 처리
  457. for char in KCONF["fkr172-179"]:
  458. idx = [i for i, item in enumerate(data) if item == char]
  459. for i in idx:
  460. val = ord(data[i + 1])
  461. coda_value = (val - CP_MIN) % 28
  462. if coda_value == 1 or coda_value == 4 or val < 100: # TODO verify
  463. data = data.replace(char, "열", 1)
  464. else:
  465. data = data.replace(char, "렬", 1)
  466. # FKR180: Katakana
  467. _fkr_log(180)
  468. data = _replace_map(data, KCONF["fkr180"])
  469. return re.sub(r"\s{2,}", " ", data.strip())
  470. def _replace_map(src, rmap, *args, **kw):
  471. """ Replace occurrences in a string according to a map. """
  472. for k, v in rmap.items():
  473. src = src.replace(k, v, *args, **kw)
  474. return src
  475. def _kor_fname_rom(fname):
  476. rom_ls = []
  477. cpoints = tuple(ord(c) for c in fname)
  478. for i in range(len(fname)):
  479. cp = cpoints[i] - CP_MIN
  480. ini = "i" + str(cp // 588)
  481. med = "m" + str((cp // 28) % 21)
  482. fin = "f" + str(cp % 28)
  483. rom_ls.append("#".join((ini, med, fin)))
  484. rom = "~".join(rom_ls) + "E"
  485. # FKR011: Check native Korean name, by coda
  486. origin_by_fin = "sino"
  487. for tok in KCONF["fkr011"]["nat_fin"]:
  488. if tok in rom:
  489. origin_by_fin = "native"
  490. break
  491. j = False
  492. for tok in KCONF["fkr011"]["nat_ini"]:
  493. if tok in rom:
  494. j = True
  495. k = False
  496. for tok in KCONF["fkr011"]["sino_ini"]:
  497. if tok in rom:
  498. k = True
  499. if j:
  500. if k:
  501. origin_by_ini = "sino"
  502. else:
  503. origin_by_ini = "native"
  504. else:
  505. origin_by_ini = "sino"
  506. # FKR012: Check native Korean name, by vowel & coda
  507. origin_by_med = "sino"
  508. for tok in KCONF["fkr011"]:
  509. if tok in rom:
  510. origin_by_med = "native"
  511. break
  512. # FKR013: Check native Korean name, by ㅢ
  513. if "m19#" in rom:
  514. if "의" in fname or "희" in fname:
  515. origin_by_med = "sino"
  516. else:
  517. origin_by_med = "native"
  518. # FKR014: Consonant assimilation ㄱ
  519. # FKR015: Consonant assimilation ㄲ
  520. # FKR016: Consonant assimilation ㄴ
  521. # FKR017: Consonant assimilation ㄷ
  522. # FKR018: Consonant assimilation ㄹ
  523. # FKR019: Consonant assimilation ㅁ
  524. # FKR020: Consonant assimilation ㅂ
  525. # FKR021: Consonant assimilation ㅅ
  526. # FKR022: Consonant assimilation ㅆ
  527. # FKR023: Consonant assimilation ㅇ
  528. # FKR024: Consonant assimilation ㅈ
  529. # FKR025: Consonant assimilation ㅊ
  530. # FKR026: Consonant assimilation ㅎ
  531. # FKR027: Final sound law
  532. # FKR028: Vocalization 1 (except ㄹ+ㄷ, ㄹ+ㅈ): voiced+unvoiced
  533. # FKR029: Vocalization 2 unvoiced+voiced
  534. for i in range(14, 30):
  535. _fkr_log(i)
  536. rom = _replace_map(rom, KCONF[f"fkr{i:03}"])
  537. # FKR030: Convert everything else
  538. _fkr_log(30)
  539. for k, cmap in KCONF["fkr030"].items():
  540. logger.debug(f"Applying FKR030[\"{k}\"]")
  541. rom = _replace_map(rom, cmap)
  542. rom = _replace_map(rom.replace("#", ""), {"swi": "shwi", "Swi": "Shwi"}, 1)
  543. if len(fname) == 2:
  544. rom = rom.replace("~", "-")
  545. else:
  546. rom = _replace_map(rom, {"n~g": "n'g", "~": ""})
  547. # FKR031: ㄹ + vowels/ㅎ/ㄹ ["l-r","l-l"] does not work USE alternative
  548. _fkr_log(31)
  549. for k, cmap in KCONF["fkr031"].items():
  550. logger.debug(f"Applying FKR031[\"{k}\"]")
  551. rom = _replace_map(rom, cmap)
  552. # FKR032: Capitalization
  553. rom = rom[0].upper() + rom[1:]
  554. # FKR033: Remove hyphen in bisyllabic native Korean first name
  555. if (
  556. len(fname) == 2
  557. and "native" in (origin_by_ini, origin_by_fin, origin_by_med)):
  558. rom = _replace_map(rom, {"n-g": "n'g", "-": ""})
  559. # FKR034: First name, initial sound law
  560. for k, v in KCONF["fkr034"].items():
  561. if rom.startswith(k):
  562. rom = rom.replace(k, v)
  563. return rom
  564. def _kor_lname_rom(lname):
  565. if len(lname) == 2:
  566. # FKR181: 2-charater names.
  567. _fkr_log(181)
  568. rom = _replace_map(lname, KCONF["fkr181"])
  569. else:
  570. # FKR182: 1-charater Chinese names.
  571. _fkr_log(182)
  572. lname = _replace_map(lname, KCONF["fkr182"])
  573. # FKR183: 1-charater names.
  574. _fkr_log(183)
  575. rom = _replace_map(lname, KCONF["fkr183"])
  576. return rom if lname != rom else False
  577. def _capitalize(src):
  578. """ Only capitalize first word and words preceded by space."""
  579. orig_ls = src.split(" ")
  580. cap_ls = [orig[0].upper() + orig[1:] for orig in orig_ls]
  581. return " ".join(cap_ls)
  582. def _fkr_log(fkr_i):
  583. fkr_k = f"FKR{fkr_i:03}"
  584. logger.debug(f"Applying {fkr_k}: {FKR_IDX[fkr_k]}")