romanizer.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  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 = []
  131. for n in lname.split("+"):
  132. _k = _kor_lname_rom(n)
  133. if _k:
  134. lname_rom_ls.append(_k)
  135. if not any(lname_rom_ls):
  136. warnings.append(f"{parsed} is not a recognized Korean name.")
  137. return "", warnings
  138. lname_rom = " ".join(lname_rom_ls)
  139. # Add comma after the last name for certain MARC fields.
  140. marc_field = options.get("marc_field")
  141. if marc_field in ("100", "600", "700", "800"):
  142. rom = f"{lname_rom}, {fname_rom}"
  143. else:
  144. rom = f"{lname_rom} {fname_rom}"
  145. if False:
  146. # TODO add option for authoritative name.
  147. rom_ls = rom.rsplit(" ", 1)
  148. rom = ", ".join(rom_ls)
  149. return rom, warnings
  150. else:
  151. warnings.append("Romanized as a foreign name.")
  152. return parsed, warnings
  153. warnings.append(f"{src} is not a recognized Korean name.")
  154. return "", warnings
  155. def _parse_kor_name(src):
  156. parsed = None
  157. warnings = []
  158. # FKR004: Check first two characters. Two-syllable family name or not?
  159. two_syl_fname = False
  160. for ptn in KCONF["fkr004"]:
  161. if src.startswith(ptn):
  162. two_syl_fname = True
  163. break
  164. src_len = len(src)
  165. # FKR005: Error if more than 7 syllables
  166. if src_len > 7 or src_len < 2 or " " in src[3:]:
  167. return _kor_corp_name_rom(src), warnings
  168. ct_spaces = src.count(" ")
  169. # FKR0006: Error if more than 2 spaces
  170. if ct_spaces > 2:
  171. warnings.append("ERROR: not a name (too many spaces)")
  172. return parsed, warnings
  173. # FKR007: 2 spaces (two family names)
  174. if ct_spaces == 2:
  175. parsed = src.replace(" ", "+", 1).replace(" ", "~", 1)
  176. elif ct_spaces == 1:
  177. # FKR008: 1 space (2nd position)
  178. if src[1] == " ":
  179. parsed = src.replace(" ", "~")
  180. # FKR009: 1 space (3nd position)
  181. if src[2] == " ":
  182. if two_syl_fname:
  183. parsed = "+" + src.replace(" ", "~")
  184. # FKR010: When there is no space
  185. else:
  186. if src_len == 2:
  187. parsed = src[0] + "~" + src[1:]
  188. elif src_len > 2:
  189. if two_syl_fname:
  190. parsed = src[:1] + "~" + src[2:]
  191. else:
  192. parsed = src[0] + "~" + src[1:]
  193. return parsed, warnings
  194. def _kor_corp_name_rom(src):
  195. chu = yu = 0
  196. if src.startswith("(주) "):
  197. src = src[4:]
  198. chu = "L"
  199. if src.endswith(" (주)"):
  200. src = src[:-4]
  201. chu = "R"
  202. if src.startswith("(유) "):
  203. src = src[4:]
  204. yu = "L"
  205. if src.endswith(" (유)"):
  206. src = src[:-4]
  207. yu = "R"
  208. rom_tok = []
  209. for tok in src.split(" "):
  210. rom_tok.append(_romanize_oclc_auto(tok))
  211. rom = _capitalize(" ".join(rom_tok))
  212. if chu == "L":
  213. rom = "(Chu) " + rom
  214. elif chu == "R":
  215. rom = rom + " (Chu)"
  216. if yu == "L":
  217. rom = "(Yu) " + rom
  218. elif yu == "R":
  219. rom = rom + " (Yu)"
  220. # FKR035: Replace established names
  221. rom = _replace_map(rom, KCONF["fkr035"])
  222. return rom
  223. def _romanize_oclc_auto(kor):
  224. # FKR050: Starts preprocessing symbol
  225. _fkr_log(50)
  226. for rname, rule in KCONF["fkr050"].items():
  227. logger.debug(f"Applying fkr050[{rname}]")
  228. kor = _replace_map(kor, rule)
  229. # See https://github.com/lcnetdev/scriptshifter/issues/19
  230. kor = re.sub("제([0-9])", "제 \\1", kor)
  231. # FKR052: Replace Che+number
  232. _fkr_log(52)
  233. for rname, rule in KCONF["fkr052"].items():
  234. logger.debug(f"Applying fkr052[{rname}]")
  235. kor = _replace_map(kor, rule)
  236. # Strip end and multiple whitespace.
  237. kor = re.sub(r"\s{2,}", " ", kor.strip())
  238. kor = kor.replace("^", " GLOTTAL ")
  239. logger.debug(f"Korean before romanization: {kor}")
  240. rom_ls = []
  241. for word in kor.split(" "):
  242. rom_ls.append(_kor_rom(word))
  243. rom = " ".join(rom_ls)
  244. # FKR059: Apply glottalization
  245. rom = _replace_map(
  246. f" {rom.strip()} ", {" GLOTTAL ": "", "*": "", "^": ""})
  247. # FKR060: Process number + -년/-년도/-년대
  248. # TODO Add leading whitespace as per L1221? L1202 already added one.
  249. rom = _replace_map(rom, KCONF["fkr060"])
  250. rom = re.sub(r"\s{2,}", " ", f" {rom.strip()} ")
  251. # FKR061: Jurisdiction (시)
  252. # FKR062: Historical place names
  253. # FKR063: Jurisdiction (국,도,군,구)
  254. # FKR064: Temple names of Kings, Queens, etc. (except 조/종)
  255. # FKR065: Frequent historical names
  256. for i in range(61, 66):
  257. _fkr_log(i)
  258. rom = _replace_map(rom, KCONF[f"fkr{i:03}"])
  259. # FKR066: Starts restore symbols
  260. _fkr_log(66)
  261. for rname, rule in KCONF["fkr066"].items():
  262. logger.debug(f"Applying FKR066[{rname}]")
  263. rom = _replace_map(rom, rule)
  264. # Remove spaces from before punctuation signs.
  265. rom = re.sub(r" (?=[,.;:?!])", "", rom.strip())
  266. rom = re.sub(r"\s{2,}", " ", rom)
  267. return rom
  268. # FKR068: Exceptions, Exceptions to initial sound law, Proper names
  269. def _kor_rom(kor):
  270. kor = re.sub(r"\s{2,}", " ", kor.strip())
  271. orig = kor
  272. # FKR069: Irregular sound change list
  273. kor = _replace_map(kor, KCONF["fkr069"])
  274. # FKR070: [n] insertion position mark +
  275. niun = kor.find("+")
  276. if niun > -1:
  277. kor = kor.replace("+", "")
  278. orig = kor
  279. non_kor = 0
  280. cpoints = tuple(ord(c) for c in kor)
  281. for cp in cpoints:
  282. if cp < CP_MIN:
  283. non_kor += 1
  284. kor = kor[1:]
  285. rom_ls = []
  286. if non_kor > 0:
  287. # Rebuild code point list with non_kor removed.
  288. cpoints = tuple(ord(c) for c in kor)
  289. for i in range(len(kor)):
  290. cp = cpoints[i] - CP_MIN
  291. ini = "i" + str(cp // 588)
  292. med = "m" + str((cp // 28) % 21)
  293. fin = "f" + str(cp % 28)
  294. rom_ls.append("#".join((ini, med, fin)))
  295. rom = "~".join(rom_ls)
  296. if len(rom):
  297. rom = rom + "E"
  298. # FKR071: [n] insertion
  299. if niun > -1:
  300. niun_loc = rom.find("~")
  301. # Advance until the niun'th occurrence of ~
  302. # If niun is 0 or 1 the loop will be skipped.
  303. for i in range(niun - 1):
  304. niun_loc = rom.find("~", niun_loc + 1)
  305. rom_niun_a = rom[:niun_loc]
  306. rom_niun_b = rom[niun_loc + 1:]
  307. if re.match("i11#m(?:2|6|12|17|20)", rom_niun_b):
  308. _fkr_log(71)
  309. rom_niun_b = rom_niun_b.replace("i11#m", "i2#m", 1)
  310. # FKR072: [n]+[l] >[l] + [l]
  311. if rom_niun_b.startswith("i5#") and rom_niun_a.endswith("f4"):
  312. _fkr_log(72)
  313. rom_niun_b = rom_niun_b.replace("i5#", "i2", 1)
  314. rom = f"{rom_niun_a}~{rom_niun_b}"
  315. # FKR073: Palatalization: ㄷ+이,ㄷ+여,ㄷ+히,ㄷ+혀
  316. # FKR074: Palatalization: ㅌ+이,ㅌ+히,ㅌ+히,ㅌ+혀
  317. # FKR075: Consonant assimilation ㄱ
  318. # FKR076: Consonant assimilation ㄲ
  319. # FKR077: Consonant assimilation ㄳ : ㄱ,ㄴ,ㄹ,ㅁ,ㅇ
  320. # FKR078: Consonant assimilation ㄴ
  321. # FKR079: Consonant assimilation ㄵ: ㄱ,ㄴ,ㄷ,ㅈ"
  322. # FKR080: Consonant assimilation ㄶ : ㄱ,ㄴ,ㄷ,ㅈ
  323. # FKR081: Consonant assimilation ㄷ
  324. # FKR082: Consonant assimilation ㄹ
  325. # FKR083: Consonant assimilation ㄺ : ㄱ,ㄴ,ㄷ,ㅈ
  326. # FKR084: Consonant assimilation ㄻ : ㄱ,ㄴ,ㄷ,ㅈ
  327. # FKR085: Consonant assimilation ㄼ : ㄱ,ㄴ,ㄷ,ㅈ
  328. # FKR086: Consonant assimilation ㄾ : ㄱ,ㄴ,ㄷ,ㅈ
  329. # FKR087: Consonant assimilation ㄿ : ㄱ,ㄴ,ㄷ,ㅈ
  330. # FKR088: Consonant assimilation ㅀ : ㄱ,ㄴ,ㄷ,ㅈ
  331. # FKR089: Consonant assimilation ㅁ
  332. # FKR090: Consonant assimilation ㅂ
  333. # FKR091: Consonant assimilation ㅄ
  334. # FKR092: Consonant assimilation ㅅ
  335. # FKR093: Consonant assimilation ㅆ
  336. # FKR094: Consonant assimilation ㅇ
  337. # FKR095: Consonant assimilation ㅈ
  338. # FKR096: Consonant assimilation ㅊ
  339. # FKR097: Consonant assimilation ㅋ
  340. # FKR098: Consonant assimilation ㅌ
  341. # FKR099: Consonant assimilation ㅍ
  342. # FKR100: Consonant assimilation ㅎ
  343. # FKR101: digraphic coda + ㅇ: ㄵ,ㄶ,ㄺ,ㄻ,ㄼ,ㄽ,ㄾ,ㄿ,ㅀ
  344. # FKR102: digraphic coda + ㅎ: ㄵ,ㄶ,ㄺ,ㄻ,ㄼ,(ㄽ),ㄾ,ㄿ,ㅀ
  345. # FKR103: Vocalization 1 (except ㄹ+ㄷ, ㄹ+ㅈ 제외) voiced + unvoiced
  346. # FKR104: Vocalization 2 (except ㄹ+ㄷ, ㄹ+ㅈ 제외) unvoiced + voiced
  347. # FKR105: Vocalization 3 (ㄹ+ㄷ, ㄹ+ㅈ)
  348. # FKR106: Final sound law
  349. # FKR107: Exception for '쉬' = shi
  350. # FKR108: Exception for 'ㄴㄱ'= n'g
  351. for fkr_i in range(73, 109):
  352. _fkr_log(fkr_i)
  353. _bk = rom
  354. rom = _replace_map(rom, KCONF[f"fkr{fkr_i:03}"])
  355. if _bk != rom:
  356. logger.debug(f"FKR{fkr_i} substitution: {rom} (was: {_bk})")
  357. # FKR109: Convert everything else
  358. _fkr_log(109)
  359. for pos, data in KCONF["fkr109"].items():
  360. rom = _replace_map(rom, data)
  361. # FKR110: Convert symbols
  362. rom = _replace_map(rom, {"#": "", "~": ""})
  363. if non_kor > 0:
  364. # Modified from K-Romanizer:1727 in that it does not append a hyphen
  365. # if the whole word is non-Korean.
  366. rom = f"{orig[:non_kor]}-{rom}" if len(rom) else orig
  367. # FKR111: ㄹ + 모음/ㅎ/ㄹ, ["lr","ll"] must be in the last of the array
  368. rom = _replace_map(rom, KCONF["fkr111"])
  369. # FKR112: Exceptions to initial sound law
  370. is_non_kor = False
  371. # FKR113: Check loan words by the first 1 letter
  372. # FKR114: Check loan words by the first 2 letters
  373. # FKR115: Check loan words by the first 3 letters
  374. if orig.startswith(tuple(KCONF["fkr113-115"])):
  375. is_non_kor = True
  376. # FKR116: Exceptions to initial sound law - particles
  377. is_particle = False
  378. if orig.startswith(tuple(KCONF["fkr116"]["particles"])):
  379. is_particle = True
  380. if len(orig) > 1 and not is_non_kor and not is_particle:
  381. if rom.startswith(tuple(KCONF["fkr116"]["replace_initials"].keys())):
  382. rom = _replace_map(rom, KCONF["fkr116"]["replace_initials"])
  383. # FKR117: Proper names _StringPoper Does not work because of breves
  384. if (
  385. # FKR118
  386. orig in KCONF["fkr118"] or
  387. # FKR119
  388. orig in KCONF["fkr119"]["word"] or
  389. (
  390. orig[:-1] in KCONF["fkr119"]["word"] and
  391. orig.endswith(tuple(KCONF["fkr119"]["suffix"]))
  392. ) or
  393. # FKR120
  394. orig.endswith(tuple(KCONF["fkr120"]))):
  395. rom = rom[0].upper() + rom[1:]
  396. # FKR121: Loan words beginning with L
  397. if f" {orig} " in KCONF["fkr121"]:
  398. rom = _replace_map(rom[0], {"R": "L", "r": "l"}) + rom[1:]
  399. # @TODO Move this to a generic normalization step (not only for K)
  400. rom = _replace_map(rom, {"ŏ": "ŏ", "ŭ": "ŭ", "Ŏ": "Ŏ", "Ŭ": "Ŭ"})
  401. return rom
  402. def _marc8_hancha(data):
  403. # FKR142: Chinese character list
  404. _fkr_log(142)
  405. return _replace_map(data, KCONF["fkr142"])
  406. def _hancha2hangul(data):
  407. data = " " + data.replace("\n", "\n ")
  408. # FKR143: Process exceptions first
  409. # FKR144: Apply initial sound law (Except: 列, 烈, 裂, 劣)
  410. # FKR145: Simplified characters, variants
  411. # FKR146: Some characters from expanded list
  412. # FKR147: Chinese characters 1-500 車=차
  413. # FKR148: Chinese characters 501-750 串=관
  414. # FKR149: Chinese characters 751-1000 金=금, 娘=랑
  415. # FKR150: Chinese characters 1001-1250
  416. # FKR151: Chinese characters 1251-1500 제외: 列, 烈, 裂, 劣
  417. # FKR152: Chinese characters 1501-1750 제외: 律, 率, 栗, 慄
  418. # FKR153: Chinese characters 1751-2000
  419. # FKR154: 不,Chinese characters 2001-2250 제외: 不
  420. # FKR155: Chinese characters 2251-2500 塞=색
  421. # FKR156: Chinese characters 2501-2750
  422. # FKR157: Chinese characters 2751-3000
  423. # FKR158: Chinese characters 3001-2250
  424. # FKR159: Chinese characters 3251-3500
  425. # FKR160: Chinese characters 3501-3750
  426. # FKR161: Chinese characters 3751-4000
  427. # FKR162: Chinese characters 4001-4250
  428. # FKR163: Chinese characters 4251-4500
  429. # FKR164: Chinese characters 4501-4750
  430. # FKR165: Chinese characters 4751-5000
  431. # FKR166: Chinese characters 5001-5250
  432. # FKR167: Chinese characters 5251-5500
  433. # FKR168: Chinese characters 5501-5750
  434. # FKR169: Chinese characters 5751-5978
  435. # FKR170: Chinese characters 일본Chinese characters
  436. for i in range(143, 171):
  437. _fkr_log(i)
  438. data = _replace_map(data, KCONF[f"fkr{i}"])
  439. # FKR171: Chinese characters 不(부)의 발음 처리
  440. # Write down indices of occurrences of "不"
  441. idx = [i for i, item in enumerate(data) if item == "不"]
  442. for i in idx:
  443. val = ord(data[i + 1])
  444. if (val > 45795 and val < 46384) or (val > 51087 and val < 51676):
  445. data = data.replace("不", "부", 1)
  446. else:
  447. data = data.replace("不", "불", 1)
  448. # FKR172: Chinese characters 列(렬)의 발음 처리
  449. # FKR173: Chinese characters 烈(렬)의 발음 처리
  450. # FKR174: Chinese characters 裂(렬)의 발음 처리
  451. # FKR175: Chinese characters 劣(렬)의 발음 처리
  452. # FKR176: Chinese characters 律(률)의 발음 처리
  453. # FKR177: Chinese characters 率(률)의 발음 처리
  454. # FKR178: Chinese characters 慄(률)의 발음 처리
  455. # FKR179: Chinese characters 栗(률)의 발음 처리
  456. for char in KCONF["fkr172-179"]:
  457. idx = [i for i, item in enumerate(data) if item == char]
  458. for i in idx:
  459. val = ord(data[i - 1])
  460. coda_value = (val - CP_MIN) % 28
  461. if coda_value == 0 or coda_value == 4 or val < 100: # TODO verify
  462. data = data.replace(char, "열", 1)
  463. else:
  464. data = data.replace(char, "렬", 1)
  465. # FKR180: Katakana
  466. _fkr_log(180)
  467. data = _replace_map(data, KCONF["fkr180"])
  468. return re.sub(r"\s{2,}", " ", data.strip())
  469. def _replace_map(src, rmap, *args, **kw):
  470. """ Replace occurrences in a string according to a map. """
  471. for k, v in rmap.items():
  472. src = src.replace(k, v, *args, **kw)
  473. return src
  474. def _kor_fname_rom(fname):
  475. rom_ls = []
  476. cpoints = tuple(ord(c) for c in fname)
  477. for i in range(len(fname)):
  478. cp = cpoints[i] - CP_MIN
  479. ini = "i" + str(cp // 588)
  480. med = "m" + str((cp // 28) % 21)
  481. fin = "f" + str(cp % 28)
  482. rom_ls.append("#".join((ini, med, fin)))
  483. rom = "~".join(rom_ls) + "E"
  484. # FKR011: Check native Korean name, by coda
  485. origin_by_fin = "sino"
  486. for tok in KCONF["fkr011"]["nat_fin"]:
  487. if tok in rom:
  488. origin_by_fin = "native"
  489. break
  490. j = False
  491. for tok in KCONF["fkr011"]["nat_ini"]:
  492. if tok in rom:
  493. j = True
  494. k = False
  495. for tok in KCONF["fkr011"]["sino_ini"]:
  496. if tok in rom:
  497. k = True
  498. if j:
  499. if k:
  500. origin_by_ini = "sino"
  501. else:
  502. origin_by_ini = "native"
  503. else:
  504. origin_by_ini = "sino"
  505. # FKR012: Check native Korean name, by vowel & coda
  506. origin_by_med = "sino"
  507. for tok in KCONF["fkr011"]:
  508. if tok in rom:
  509. origin_by_med = "native"
  510. break
  511. # FKR013: Check native Korean name, by ㅢ
  512. if "m19#" in rom:
  513. if "의" in fname or "희" in fname:
  514. origin_by_med = "sino"
  515. else:
  516. origin_by_med = "native"
  517. # FKR014: Consonant assimilation ㄱ
  518. # FKR015: Consonant assimilation ㄲ
  519. # FKR016: Consonant assimilation ㄴ
  520. # FKR017: Consonant assimilation ㄷ
  521. # FKR018: Consonant assimilation ㄹ
  522. # FKR019: Consonant assimilation ㅁ
  523. # FKR020: Consonant assimilation ㅂ
  524. # FKR021: Consonant assimilation ㅅ
  525. # FKR022: Consonant assimilation ㅆ
  526. # FKR023: Consonant assimilation ㅇ
  527. # FKR024: Consonant assimilation ㅈ
  528. # FKR025: Consonant assimilation ㅊ
  529. # FKR026: Consonant assimilation ㅎ
  530. # FKR027: Final sound law
  531. # FKR028: Vocalization 1 (except ㄹ+ㄷ, ㄹ+ㅈ): voiced+unvoiced
  532. # FKR029: Vocalization 2 unvoiced+voiced
  533. for i in range(14, 30):
  534. _fkr_log(i)
  535. rom = _replace_map(rom, KCONF[f"fkr{i:03}"])
  536. # FKR030: Convert everything else
  537. _fkr_log(30)
  538. for k, cmap in KCONF["fkr030"].items():
  539. logger.debug(f"Applying FKR030[\"{k}\"]")
  540. rom = _replace_map(rom, cmap)
  541. rom = _replace_map(rom.replace("#", ""), {"swi": "shwi", "Swi": "Shwi"}, 1)
  542. if len(fname) == 2:
  543. rom = rom.replace("~", "-")
  544. else:
  545. rom = _replace_map(rom, {"n~g": "n'g", "~": ""})
  546. # FKR031: ㄹ + vowels/ㅎ/ㄹ ["l-r","l-l"] does not work USE alternative
  547. _fkr_log(31)
  548. for k, cmap in KCONF["fkr031"].items():
  549. logger.debug(f"Applying FKR031[\"{k}\"]")
  550. rom = _replace_map(rom, cmap)
  551. # FKR032: Capitalization
  552. rom = rom[0].upper() + rom[1:]
  553. # FKR033: Remove hyphen in bisyllabic native Korean first name
  554. if (
  555. len(fname) == 2
  556. and "native" in (origin_by_ini, origin_by_fin, origin_by_med)):
  557. rom = _replace_map(rom, {"n-g": "n'g", "-": ""})
  558. # FKR034: First name, initial sound law
  559. for k, v in KCONF["fkr034"].items():
  560. if rom.startswith(k):
  561. rom = rom.replace(k, v)
  562. return rom
  563. def _kor_lname_rom(lname):
  564. if len(lname) == 2:
  565. # FKR181: 2-charater names.
  566. _fkr_log(181)
  567. rom = _replace_map(lname, KCONF["fkr181"])
  568. else:
  569. # FKR182: 1-charater Chinese names.
  570. _fkr_log(182)
  571. lname = _replace_map(lname, KCONF["fkr182"])
  572. # FKR183: 1-charater names.
  573. _fkr_log(183)
  574. rom = _replace_map(lname, KCONF["fkr183"])
  575. return rom if lname != rom else False
  576. def _capitalize(src):
  577. """ Only capitalize first word and words preceded by space."""
  578. orig_ls = src.split(" ")
  579. cap_ls = [orig[0].upper() + orig[1:] for orig in orig_ls]
  580. return " ".join(cap_ls)
  581. def _fkr_log(fkr_i):
  582. fkr_k = f"FKR{fkr_i:03}"
  583. logger.debug(f"Applying {fkr_k}: {FKR_IDX[fkr_k]}")