romanizer.py 23 KB

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