From 588a64eb68fe18529eae4ba7d62cd2cfe7203b48 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 14 Sep 2025 09:16:23 -0300 Subject: [PATCH 1/6] Add support for hiragana --- .../languages/japanese/JapaneseTokenizer.kt | 24 +++++++++++++++++-- src/test/kotlin/MockValues.kt | 2 +- src/test/kotlin/ParserTest.kt | 12 ++++++++++ .../dictionaries/kanji/hiragana.json | 16 +++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/test/resources/dictionaries/kanji/hiragana.json diff --git a/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt b/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt index 00dcff1..18e0a6d 100644 --- a/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt +++ b/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt @@ -25,15 +25,35 @@ object JapaneseTokenizer : Tokenizer { while (i < chars.size) { val char = chars[i].toString() - tokenizeKanji(char, input.substring(i))?.let { + + // tokenize hiragana first to parse kanji words that are prefixed with a kana character (e.g. お願い, お姉さん) + val foundWord = tokenizeHiragana(char, input.substring(i)) ?: + tokenizeKanji(char, input.substring(i)) + + foundWord?.let { wordsFound.add(it) - i += it.word.length // Skip characters based on the word's length + i += it.word.length } ?: i++ } return wordsFound } + private fun tokenizeHiragana(char: String, original: String): Word? { + if (char.isEmpty() || char[0].code !in 0x3040..0x309F) return null + + wordMap[char]?.let { hiraganaWord -> + @Suppress("UNNECESSARY_SAFE_CALL") + val word = hiraganaWord.derivations?.find { original.startsWith(it.word) } ?: hiraganaWord + + // A single hiragana character cannot constitute a word + return if (word.word.length == 1 && word.word == char) null + else word + } + + return null + } + private fun tokenizeKanji(char: String, original: String): Word? { if (char.isEmpty() || char[0].code !in 0x4E00..0x9FFF) return null diff --git a/src/test/kotlin/MockValues.kt b/src/test/kotlin/MockValues.kt index e61c2bb..bd56eb6 100644 --- a/src/test/kotlin/MockValues.kt +++ b/src/test/kotlin/MockValues.kt @@ -1,6 +1,6 @@ const val englishDictionaryJsonString = "[{\"parent\":\"run\",\"word\":\"run\",\"derivations\":[{\"parent\":\"running\",\"word\":\"running\"},{\"parent\":\"ran\",\"word\":\"ran\"}]}]" const val kanjiDictionaryJsonString = - "[{\"parent\":\"run\",\"word\":\"走\",\"derivations\":[{\"parent\":\"run\",\"word\":\"走る\"},{\"parent\":\"ran\",\"word\":\"走ってた\"},{\"parent\":\"running\",\"word\":\"走っています\"}]},{\"parent\":\"rain\",\"word\":\"雨\",\"representable\":true,\"pronunciation\":\"ame\"},{\"parent\":\"i\",\"word\":\"私\"}]" + "[{\"parent\":\"run\",\"word\":\"走\",\"derivations\":[{\"parent\":\"run\",\"word\":\"走る\"},{\"parent\":\"ran\",\"word\":\"走ってた\"},{\"parent\":\"running\",\"word\":\"走っています\"}]},{\"parent\":\"rain\",\"word\":\"雨\",\"representable\":true,\"pronunciation\":\"ame\"},{\"parent\":\"i\",\"word\":\"私\"},{\"parent\":\"\",\"word\":\"変\",\"representable\":false,\"derivations\":[{\"parent\":\"wont_change\",\"word\":\"変わらない\"}]},{\"parent\":\"\",\"word\":\"近\",\"representable\":false,\"derivations\":[{\"parent\":\"if_get_close\",\"word\":\"近付いたら\",\"representable\":true}]},{\"parent\":\"sad\",\"word\":\"悲\",\"representable\":true,\"derivations\":[{\"parent\":\"sad\",\"translation_parent\":\"sadness\",\"word\":\"悲しみ\",\"representable\":true}]},{\"parent\":\"\",\"word\":\"お\",\"derivations\":[{\"parent\":\"please\",\"word\":\"お願い\"},{\"parent\":\"congratulations\",\"word\":\"おめでとう\"}]}]" const val portugueseDictionaryJsonString = "[{\"parent\":\"run\",\"word\":\"correr\",\"derivations\":[{\"parent\":\"ran\",\"word\":\"corri\"},{\"parent\":\"running\",\"word\":\"correndo\"},{\"parent\":\"run\",\"word\":\"corra\"}]},{\"parent\":\"umbrella\",\"word\":\"guarda-chuva\",\"representable\":true}]" \ No newline at end of file diff --git a/src/test/kotlin/ParserTest.kt b/src/test/kotlin/ParserTest.kt index 7393359..87b8305 100644 --- a/src/test/kotlin/ParserTest.kt +++ b/src/test/kotlin/ParserTest.kt @@ -51,6 +51,12 @@ class ParserTest : GengolexTest() { words = parser.findWords("雨が降る") assertEquals(1, words.size) + + words = parser.findWords("お誕生日おめでとう") + assertEquals(1, words.size) + + words = parser.findWords("早く行けお願い") + assertEquals(1, words.size) } @Test @@ -145,6 +151,12 @@ class ParserTest : GengolexTest() { words = parser.findWords("走る、走ってた、僕は走っています。") assertEquals(3, words.size) + + words = parser.findWords("お誕生日おめでとう") + assertEquals(1, words.size) + + words = parser.findWords("早く行けお願い") + assertEquals(1, words.size) } @Test diff --git a/src/test/resources/dictionaries/kanji/hiragana.json b/src/test/resources/dictionaries/kanji/hiragana.json new file mode 100644 index 0000000..f3b7680 --- /dev/null +++ b/src/test/resources/dictionaries/kanji/hiragana.json @@ -0,0 +1,16 @@ +[ + { + "parent": "", + "word": "お", + "derivations": [ + { + "parent": "please", + "word": "お願い" + }, + { + "parent": "congratulations", + "word": "おめでとう" + } + ] + } +] \ No newline at end of file From c82bf92c2a383194c09174ac515dcdfe518cb2b2 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 14 Sep 2025 09:23:19 -0300 Subject: [PATCH 2/6] Improve japanese WordFieldTest --- src/test/kotlin/parser/WordFieldTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/kotlin/parser/WordFieldTest.kt b/src/test/kotlin/parser/WordFieldTest.kt index d5f5ff8..bfe1891 100644 --- a/src/test/kotlin/parser/WordFieldTest.kt +++ b/src/test/kotlin/parser/WordFieldTest.kt @@ -15,12 +15,29 @@ class WordFieldTest : GengolexTest() { testWord("走", "走", Language.JAPANESE) testWord("僕は走っています", "走っています", Language.JAPANESE) + testWord("彼が走っています", "走っています", Language.JAPANESE) + testWord("道で走っています", "走っています", Language.JAPANESE) testWord("僕は走ってた", "走ってた", Language.JAPANESE) + testWord("昨日も走ってた", "走ってた", Language.JAPANESE) + testWord("友達と走ってた", "走ってた", Language.JAPANESE) testWord("僕は走る", "走る", Language.JAPANESE) + testWord("明日走るつもりだ", "走る", Language.JAPANESE) + testWord("毎日走るよ", "走る", Language.JAPANESE) testWord("私は", "私", Language.JAPANESE) + testWord("私の家はここです", "私", Language.JAPANESE) + testWord("私だけが知っている", "私", Language.JAPANESE) testWord("雨が降る", "雨", Language.JAPANESE) + testWord("雨が降っている", "雨", Language.JAPANESE) + testWord("強い雨が降った", "雨", Language.JAPANESE) testWord("変わらないよ", "変わらない", Language.JAPANESE) + testWord("変わらない景色", "変わらない", Language.JAPANESE) + testWord("心は変わらない", "変わらない", Language.JAPANESE) testWord("近付いたら壊れてしまいそう", "近付いたら", Language.JAPANESE) + testWord("君に近付いたら嬉しい", "近付いたら", Language.JAPANESE) + testWord("犬に近付いたら噛まれた", "近付いたら", Language.JAPANESE) + testWord("お願いが叶うなら", "お願い", Language.JAPANESE) + testWord("お願いを聞いてくれますか", "お願い", Language.JAPANESE) + testWord("小さなお願いがあるんだ", "お願い", Language.JAPANESE) } @Test From 504c4aa9bf365ee75454610c09add0872dc203bf Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 14 Sep 2025 10:09:18 -0300 Subject: [PATCH 3/6] Add a parser benchmark Adds a parser benchmark to check how performance is scaling --- src/test/kotlin/ParserBenchmarkTest.kt | 120 +++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/test/kotlin/ParserBenchmarkTest.kt diff --git a/src/test/kotlin/ParserBenchmarkTest.kt b/src/test/kotlin/ParserBenchmarkTest.kt new file mode 100644 index 0000000..cbcc009 --- /dev/null +++ b/src/test/kotlin/ParserBenchmarkTest.kt @@ -0,0 +1,120 @@ +import cc.wordview.gengolex.Language +import cc.wordview.gengolex.Parser +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.time.Duration +import kotlin.time.measureTimedValue + +@Ignore("Run the benchmark separately from the tests suites") +class ParserBenchmarkTest { + // Benchmark only japanese because it is the most complex one at the moment + @Test + fun parseJapanese8chars() { + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords("僕は走っています") + } + + printRes("parseJapanese8chars", time) + } + + @Test + fun parseJapanese19chars() { + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords("昨日の朝、僕は友達と公園で走っています") + } + + printRes("parseJapanese19chars", time) + } + + @Test + fun parseJapanese56chars() { + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords("昨日の夕方、雨が少し降っていたけれど、僕は友達と公園を何周も走っていますし、その後も音楽を聴きながら走っています") + } + + printRes("parseJapanese56chars", time) + } + + @Test + fun parseJapanese233chars() { + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords( + "昨日の朝早く起きて、朝ご飯を食べてから公園に行き、友達と一緒に準備運動をして、空が少し曇っていたけれど雨は降らなくて、風が気持ちよかったので、僕たちは話しながらゆっくり走り始め、少しずつスピードを上げていき、汗をかきながら笑顔で走っていますが、その途中で犬の散歩をしている人に挨拶をしたり、小さな子供たちが遊んでいるのを見たりして、さらに川沿いの道まで足を伸ばして景色を楽しみながら走っていますし、最後にはベンチに座って水を飲み、今日もいい運動ができたと感じています" + ) + } + + printRes("parseJapanese233chars", time) + } + + @Test + fun parseJapanese512chars() { + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords( + "静かな夜の街を抜け出して、まだ眠らない夢を探していると、遠くの灯りが揺れて、心の奥に隠していた記憶が蘇り、笑った日も泣いた日もすべて繋がって、僕の背中を押してくれる。君と出会った奇跡を思い出しながら、失いたくないものを胸に抱いて、僕は走っています。たとえ暗闇に包まれても、必ず朝はやってきて、雲の隙間から差し込む光を信じれば、もう一度立ち上がれる。繰り返す日々の中で迷ったとしても、君の声が聞こえるたびに、未来への道を選べると信じて、走っています。 雨が頬を濡らしても、涙と混ざり合っても、心の奥で燃えている小さな炎は決して消えなくて、その炎が僕を導いていく。誰かに笑われても構わない、夢を追いかけるために、僕は今日も走っています。果てしない空の下で、流れる雲を追い越して、希望の歌を胸に響かせながら、走っています。 そして、疲れ果てて立ち止まりそうなときも、君の笑顔を思い出せば、また一歩を踏み出せる。遠く離れていても、心は確かに繋がっていて、見えない絆が強く結んでいるから、僕は明日も走っています。どこまでも続く道の上で、夢と現実の狭間を超えて、傷だらけになりながらも、それでも信じる心を失わず、僕はずっと走っています。" + ) + } + + printRes("parseJapanese512chars", time) + } + + @Test + fun parseJapanese1250chars() { + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords( + "朝日が昇るころ、街は少しずつ目を覚まし、人々が家を出て仕事や学校へ向かって歩いていく。子供たちは元気に笑いながら走り回り、大人たちは忙しそうに足を速め、信号が青に変わるたびに流れのように動き出す。その中で僕は静かに深呼吸をして、ゆっくりと準備を整え、走るための靴紐を結び直す。体がまだ眠っているように重いと感じても、一歩を踏み出せば次の一歩は自然とついてきて、やがてリズムが生まれる。そうして気付けば僕は走っています。 公園を抜けて川沿いの道に出ると、木々の間から差し込む光が眩しく、風が頬を撫で、鳥の声が遠くから聞こえてくる。走っているとき、頭の中には色々な考えが浮かんでは消えていく。昨日の出来事、これからの予定、叶えたい夢や避けられない不安。けれども足を動かし続けていると、それらは次第に薄れていき、代わりに「今ここにいる」という感覚だけが残る。そうして僕は走っています。汗が背中を流れ、呼吸が荒くなっても、走り続けることがすべてを洗い流してくれるように感じる。 道の途中で犬を散歩している人に出会い、軽く会釈を交わす。自転車に乗った学生たちが僕を追い越していき、遠くからは工事の音や車のクラクションが聞こえる。世界は絶えず動いていて、立ち止まれば置いていかれそうになる。それでも僕は自分の歩幅で、いや、走るリズムで進んでいく。小さな子供が「頑張って」と声をかけてくれると、思わず笑顔になり、さらにスピードを上げる。こうして僕は走っています。 長い直線の道を抜けると、街の雑踏が遠ざかり、代わりに静かな田舎の風景が広がる。稲穂が風に揺れ、遠くの山並みが青く霞み、雲がゆっくりと流れている。都会の喧騒を離れることで、心は不思議と落ち着いていき、自然の中に身を置くことで余計なことは考えなくなる。僕はただ呼吸を感じ、足の動きを感じ、そして「生きている」という実感を覚える。走っています。誰に見せるわけでもなく、記録を残すわけでもなく、ただ自分のために走っている。 それでも時々、立ち止まりたくなる瞬間がある。膝が痛んだり、呼吸が苦しくなったり、心が弱音を吐こうとしたり。それでも足を止めずに進むのは、きっとその先に見える景色を知りたいからだろう。知らない道を抜けたときに出会う景色、予想していなかった瞬間、そういう小さな発見が僕を前へと動かす力になる。だから今日も僕は走っています。どんなに疲れていても、立ち止まる理由よりも、進み続ける理由の方が強い。 太陽が高くなり、汗が滴り落ち、喉が渇いても、走り続けることでしか得られない感覚がある。それは達成感かもしれないし、ただの自己満足かもしれない。けれども、足を動かすたびに確かに「自分は生きている」と思える瞬間があるのだ。だから僕はこれからも走っています。" + ) + } + + printRes("parseJapanese1250chars", time) + } + + @Test + @Ignore("This is only a stress test it's very unlikely that we will have to deal with 10k+ inputs") + fun parseJapaneseStressTest10kChars() { + // Generate a large text by repeating a block multiple times + val block = """ + 朝日が昇るころ、街は少しずつ目を覚まし、人々が家を出て仕事や学校へ向かって歩いていく。 + 子供たちは元気に笑いながら走り回り、大人たちは忙しそうに足を速め、信号が青に変わるたびに流れのように動き出す。 + その中で僕は静かに深呼吸をして、ゆっくりと準備を整え、走るための靴紐を結び直す。体がまだ眠っているように重いと感じても、一歩を踏み出せば次の一歩は自然とついてきて、やがてリズムが生まれる。 + そうして気付けば僕は走っています。 + """.trimIndent() + + // Repeat the block enough times to reach ~10,000 characters + val repetitions = 50 + val largeText = block.repeat(repetitions) + + val time = calculateAverageExecutionTime { + val parser = Parser(Language.JAPANESE) + parser.addDictionary("kanji", kanjiDictionaryJsonString) + parser.findWords(largeText) + } + + printRes("parseJapaneseStressTest", time) + } + + private fun calculateAverageExecutionTime(block: () -> T): Duration { + val (_, time1) = measureTimedValue(block) + val (_, time2) = measureTimedValue(block) + val (_, time3) = measureTimedValue(block) + val (_, time4) = measureTimedValue(block) + val (_, time5) = measureTimedValue(block) + + return (time1+time2+time3+time4+time5) / 5 + } + + private fun printRes(name: String, duration: Duration) { + print("$name: Average execution time is ${duration.inWholeMilliseconds}ms\n") + } +} \ No newline at end of file From f0c35d25aa32fe1362ae020aa6453fe724ad9cae Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 14 Sep 2025 10:44:58 -0300 Subject: [PATCH 4/6] Add support for katakana words --- .../languages/japanese/JapaneseTokenizer.kt | 21 ++++++++++++++++-- src/test/kotlin/MockValues.kt | 2 +- src/test/kotlin/ParserTest.kt | 8 +++++++ .../dictionaries/kanji/katakana.json | 22 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/test/resources/dictionaries/kanji/katakana.json diff --git a/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt b/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt index 18e0a6d..76cf04d 100644 --- a/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt +++ b/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt @@ -27,8 +27,10 @@ object JapaneseTokenizer : Tokenizer { val char = chars[i].toString() // tokenize hiragana first to parse kanji words that are prefixed with a kana character (e.g. お願い, お姉さん) - val foundWord = tokenizeHiragana(char, input.substring(i)) ?: - tokenizeKanji(char, input.substring(i)) + val foundWord = + tokenizeHiragana(char, input.substring(i)) ?: + tokenizeKanji(char, input.substring(i)) ?: + tokenizeKatakana(char, input.substring(i)) foundWord?.let { wordsFound.add(it) @@ -39,6 +41,21 @@ object JapaneseTokenizer : Tokenizer { return wordsFound } + private fun tokenizeKatakana(char: String, original: String): Word? { + if (char.isEmpty() || char[0].code !in 0x30A0..0x30FF) return null + + wordMap[char]?.let { katakanaWord -> + @Suppress("UNNECESSARY_SAFE_CALL") + val word = katakanaWord.derivations?.find { original.startsWith(it.word) } ?: katakanaWord + + // A single katakana character cannot constitute a word + return if (word.word.length == 1 && word.word == char) null + else word + } + + return null + } + private fun tokenizeHiragana(char: String, original: String): Word? { if (char.isEmpty() || char[0].code !in 0x3040..0x309F) return null diff --git a/src/test/kotlin/MockValues.kt b/src/test/kotlin/MockValues.kt index bd56eb6..6c0a81c 100644 --- a/src/test/kotlin/MockValues.kt +++ b/src/test/kotlin/MockValues.kt @@ -1,6 +1,6 @@ const val englishDictionaryJsonString = "[{\"parent\":\"run\",\"word\":\"run\",\"derivations\":[{\"parent\":\"running\",\"word\":\"running\"},{\"parent\":\"ran\",\"word\":\"ran\"}]}]" const val kanjiDictionaryJsonString = - "[{\"parent\":\"run\",\"word\":\"走\",\"derivations\":[{\"parent\":\"run\",\"word\":\"走る\"},{\"parent\":\"ran\",\"word\":\"走ってた\"},{\"parent\":\"running\",\"word\":\"走っています\"}]},{\"parent\":\"rain\",\"word\":\"雨\",\"representable\":true,\"pronunciation\":\"ame\"},{\"parent\":\"i\",\"word\":\"私\"},{\"parent\":\"\",\"word\":\"変\",\"representable\":false,\"derivations\":[{\"parent\":\"wont_change\",\"word\":\"変わらない\"}]},{\"parent\":\"\",\"word\":\"近\",\"representable\":false,\"derivations\":[{\"parent\":\"if_get_close\",\"word\":\"近付いたら\",\"representable\":true}]},{\"parent\":\"sad\",\"word\":\"悲\",\"representable\":true,\"derivations\":[{\"parent\":\"sad\",\"translation_parent\":\"sadness\",\"word\":\"悲しみ\",\"representable\":true}]},{\"parent\":\"\",\"word\":\"お\",\"derivations\":[{\"parent\":\"please\",\"word\":\"お願い\"},{\"parent\":\"congratulations\",\"word\":\"おめでとう\"}]}]" + "[{\"parent\":\"run\",\"word\":\"走\",\"derivations\":[{\"parent\":\"run\",\"word\":\"走る\"},{\"parent\":\"ran\",\"word\":\"走ってた\"},{\"parent\":\"running\",\"word\":\"走っています\"}]},{\"parent\":\"rain\",\"word\":\"雨\",\"representable\":true,\"pronunciation\":\"ame\"},{\"parent\":\"i\",\"word\":\"私\"},{\"parent\":\"\",\"word\":\"変\",\"representable\":false,\"derivations\":[{\"parent\":\"wont_change\",\"word\":\"変わらない\"}]},{\"parent\":\"\",\"word\":\"近\",\"representable\":false,\"derivations\":[{\"parent\":\"if_get_close\",\"word\":\"近付いたら\",\"representable\":true}]},{\"parent\":\"sad\",\"word\":\"悲\",\"representable\":true,\"derivations\":[{\"parent\":\"sad\",\"translation_parent\":\"sadness\",\"word\":\"悲しみ\",\"representable\":true}]},{\"parent\":\"\",\"word\":\"ア\",\"derivations\":[{\"parent\":\"hideout\",\"word\":\"アジト\"}]},{\"parent\":\"\",\"word\":\"パ\",\"derivations\":[{\"parent\":\"computer\",\"word\":\"パソコン\"}]},{\"parent\":\"\",\"word\":\"お\",\"derivations\":[{\"parent\":\"please\",\"word\":\"お願い\"},{\"parent\":\"congratulations\",\"word\":\"おめでとう\"}]}]" const val portugueseDictionaryJsonString = "[{\"parent\":\"run\",\"word\":\"correr\",\"derivations\":[{\"parent\":\"ran\",\"word\":\"corri\"},{\"parent\":\"running\",\"word\":\"correndo\"},{\"parent\":\"run\",\"word\":\"corra\"}]},{\"parent\":\"umbrella\",\"word\":\"guarda-chuva\",\"representable\":true}]" \ No newline at end of file diff --git a/src/test/kotlin/ParserTest.kt b/src/test/kotlin/ParserTest.kt index 87b8305..7664c49 100644 --- a/src/test/kotlin/ParserTest.kt +++ b/src/test/kotlin/ParserTest.kt @@ -152,11 +152,19 @@ class ParserTest : GengolexTest() { words = parser.findWords("走る、走ってた、僕は走っています。") assertEquals(3, words.size) + // hiragana words = parser.findWords("お誕生日おめでとう") assertEquals(1, words.size) words = parser.findWords("早く行けお願い") assertEquals(1, words.size) + + // katakana + words = parser.findWords("アジトに行こうぜ") + assertEquals(1, words.size) + + words = parser.findWords("俺のパソコンが遅い") + assertEquals(1, words.size) } @Test diff --git a/src/test/resources/dictionaries/kanji/katakana.json b/src/test/resources/dictionaries/kanji/katakana.json new file mode 100644 index 0000000..fcefd28 --- /dev/null +++ b/src/test/resources/dictionaries/kanji/katakana.json @@ -0,0 +1,22 @@ +[ + { + "parent": "", + "word": "ア", + "derivations": [ + { + "parent": "hideout", + "word": "アジト" + } + ] + }, + { + "parent": "", + "word": "パ", + "derivations": [ + { + "parent": "computer", + "word": "パソコン" + } + ] + } +] \ No newline at end of file From 581402623ce56293bcaa40550630615d1196e6f5 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 14 Sep 2025 14:29:55 -0300 Subject: [PATCH 5/6] Fix `tokenizeHiragana` and `tokenizeKatakana` ignoring the strategy --- .../languages/japanese/JapaneseTokenizer.kt | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt b/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt index 76cf04d..a64a277 100644 --- a/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt +++ b/src/main/kotlin/cc/wordview/gengolex/languages/japanese/JapaneseTokenizer.kt @@ -28,9 +28,9 @@ object JapaneseTokenizer : Tokenizer { // tokenize hiragana first to parse kanji words that are prefixed with a kana character (e.g. お願い, お姉さん) val foundWord = - tokenizeHiragana(char, input.substring(i)) ?: + tokenizeHiragana(char, input.substring(i), wordsFound) ?: tokenizeKanji(char, input.substring(i)) ?: - tokenizeKatakana(char, input.substring(i)) + tokenizeKatakana(char, input.substring(i), wordsFound) foundWord?.let { wordsFound.add(it) @@ -41,31 +41,35 @@ object JapaneseTokenizer : Tokenizer { return wordsFound } - private fun tokenizeKatakana(char: String, original: String): Word? { + private fun tokenizeKatakana(char: String, original: String, wordsFound: ArrayList): Word? { + if (wordsFound.any { it.word == char }) return null if (char.isEmpty() || char[0].code !in 0x30A0..0x30FF) return null wordMap[char]?.let { katakanaWord -> - @Suppress("UNNECESSARY_SAFE_CALL") - val word = katakanaWord.derivations?.find { original.startsWith(it.word) } ?: katakanaWord - - // A single katakana character cannot constitute a word - return if (word.word.length == 1 && word.word == char) null - else word + return when (kanjiStrategy) { + PREFER_PARENT -> katakanaWord + PREFER_DERIVATION -> { + @Suppress("UNNECESSARY_SAFE_CALL") + katakanaWord.derivations?.find { original.startsWith(it.word) } + } + } } return null } - private fun tokenizeHiragana(char: String, original: String): Word? { + private fun tokenizeHiragana(char: String, original: String, wordsFound: ArrayList): Word? { + if (wordsFound.any { it.word == char }) return null if (char.isEmpty() || char[0].code !in 0x3040..0x309F) return null wordMap[char]?.let { hiraganaWord -> - @Suppress("UNNECESSARY_SAFE_CALL") - val word = hiraganaWord.derivations?.find { original.startsWith(it.word) } ?: hiraganaWord - - // A single hiragana character cannot constitute a word - return if (word.word.length == 1 && word.word == char) null - else word + return when (kanjiStrategy) { + PREFER_PARENT -> hiraganaWord + PREFER_DERIVATION -> { + @Suppress("UNNECESSARY_SAFE_CALL") + hiraganaWord.derivations?.find { original.startsWith(it.word) } + } + } } return null From 123af072671853e1594547f6fbc4f6d35d76812d Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 14 Sep 2025 17:45:46 -0300 Subject: [PATCH 6/6] Change java distribution to corretto --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 1e5fc45..90acca2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v4 with: java-version: '21' - distribution: 'temurin' + distribution: 'corretto' cache: maven - name: Build with Maven run: mvn -B package --file pom.xml