From 6f2a5218a21acc54d6eda646df7ad3e17f74777a Mon Sep 17 00:00:00 2001 From: Miwa / Ensan Date: Tue, 20 Feb 2024 00:34:47 +0900 Subject: [PATCH 1/5] add submodule and add conversion tests --- .gitmodules | 3 + Package.swift | 25 + ...ConverterModuleWithDefaultDictionary.swift | 50 ++ .../azooKey_dictionary_storage | 1 + .../ConverterTests/ConverterTests.swift | 757 ++++++++++++++++++ .../DicdataStoreTests/DicdataStoreTests.swift | 124 +++ 6 files changed, 960 insertions(+) create mode 100644 .gitmodules create mode 100644 Sources/KanaKanjiConverterModuleWithDefaultDictionary/KanaKanjiConverterModuleWithDefaultDictionary.swift create mode 160000 Sources/KanaKanjiConverterModuleWithDefaultDictionary/azooKey_dictionary_storage create mode 100644 Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift create mode 100644 Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/DicdataStoreTests/DicdataStoreTests.swift diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5348585 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Sources/KanaKanjiConverterModuleWithDefaultDictionary/azooKey_dictionary_storage"] + path = Sources/KanaKanjiConverterModuleWithDefaultDictionary/azooKey_dictionary_storage + url = https://github.com/ensan-hcl/azooKey_dictionary_storage diff --git a/Package.swift b/Package.swift index 8354864..37e8209 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,12 @@ let package = Package( name: "SwiftUtils", targets: ["SwiftUtils"] ), + /// デフォルト辞書データを含むバージョンの辞書モジュール + .library( + name: "KanaKanjiConverterModuleWithDefaultDictionary", + targets: ["KanaKanjiConverterModuleWithDefaultDictionary"] + ), + /// 辞書データを含まないバージョンの辞書モジュール .library( name: "KanaKanjiConverterModule", targets: ["KanaKanjiConverterModule"] @@ -51,6 +57,20 @@ let package = Package( resources: [], swiftSettings: swiftSettings ), + .target( + name: "KanaKanjiConverterModuleWithDefaultDictionary", + dependencies: [ + "KanaKanjiConverterModule" + ], + exclude: [ + "azooKey_dictionary_storage/README.md", + "azooKey_dictionary_storage/LICENSE", + ], + resources: [ + .copy("azooKey_dictionary_storage/Dictionary"), + ], + swiftSettings: swiftSettings + ), .testTarget( name: "SwiftUtilsTests", dependencies: ["SwiftUtils"], @@ -64,6 +84,11 @@ let package = Package( .copy("DictionaryMock") ], swiftSettings: swiftSettings + ), + .testTarget( + name: "KanaKanjiConverterModuleWithDefaultDictionaryTests", + dependencies: ["KanaKanjiConverterModuleWithDefaultDictionary"], + swiftSettings: swiftSettings ) ] ) diff --git a/Sources/KanaKanjiConverterModuleWithDefaultDictionary/KanaKanjiConverterModuleWithDefaultDictionary.swift b/Sources/KanaKanjiConverterModuleWithDefaultDictionary/KanaKanjiConverterModuleWithDefaultDictionary.swift new file mode 100644 index 0000000..d297283 --- /dev/null +++ b/Sources/KanaKanjiConverterModuleWithDefaultDictionary/KanaKanjiConverterModuleWithDefaultDictionary.swift @@ -0,0 +1,50 @@ +@_exported import KanaKanjiConverterModule +import Foundation + +public extension ConvertRequestOptions { + static func withDefaultDictionary( + N_best: Int = 10, + requireJapanesePrediction: Bool, + requireEnglishPrediction: Bool, + keyboardLanguage: KeyboardLanguage, + typographyLetterCandidate: Bool = false, + unicodeCandidate: Bool = true, + englishCandidateInRoman2KanaInput: Bool = false, + fullWidthRomanCandidate: Bool = false, + halfWidthKanaCandidate: Bool = false, + learningType: LearningType, + maxMemoryCount: Int = 65536, + shouldResetMemory: Bool = false, + memoryDirectoryURL: URL, + sharedContainerURL: URL, + textReplacer: TextReplacer = TextReplacer(), + metadata: ConvertRequestOptions.Metadata + ) -> Self { + #if os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + let dictionaryDirectory = Bundle.module.bundleURL.appendingPathComponent("Dictionary", isDirectory: true) + #elseif os(macOS) + let dictionaryDirectory = Bundle.module.resourceURL!.appendingPathComponent("Dictionary", isDirectory: true) + #else + let dictionaryDirectory = Bundle.module.resourceURL!.appendingPathComponent("Dictionary", isDirectory: true) + #endif + return Self( + N_best: N_best, + requireJapanesePrediction: requireJapanesePrediction, + requireEnglishPrediction: requireEnglishPrediction, + keyboardLanguage: keyboardLanguage, + typographyLetterCandidate: typographyLetterCandidate, + unicodeCandidate: unicodeCandidate, + englishCandidateInRoman2KanaInput: englishCandidateInRoman2KanaInput, + fullWidthRomanCandidate: fullWidthRomanCandidate, + halfWidthKanaCandidate: halfWidthKanaCandidate, + learningType: learningType, + maxMemoryCount: maxMemoryCount, + shouldResetMemory: shouldResetMemory, + dictionaryResourceURL: dictionaryDirectory, + memoryDirectoryURL: memoryDirectoryURL, + sharedContainerURL: sharedContainerURL, + textReplacer: textReplacer, + metadata: metadata + ) + } +} diff --git a/Sources/KanaKanjiConverterModuleWithDefaultDictionary/azooKey_dictionary_storage b/Sources/KanaKanjiConverterModuleWithDefaultDictionary/azooKey_dictionary_storage new file mode 160000 index 0000000..43b855b --- /dev/null +++ b/Sources/KanaKanjiConverterModuleWithDefaultDictionary/azooKey_dictionary_storage @@ -0,0 +1 @@ +Subproject commit 43b855b2b6275db3b94fbb51552fc039ee5f156b diff --git a/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift new file mode 100644 index 0000000..9b1c0f7 --- /dev/null +++ b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift @@ -0,0 +1,757 @@ +// +// ConverterTests.swift +// azooKeyTests +// +// Created by ensan on 2023/01/30. +// Copyright © 2023 ensan. All rights reserved. +// + +import Foundation +import KanaKanjiConverterModuleWithDefaultDictionary +import XCTest + +@MainActor final class ConverterTests: XCTestCase { + func sequentialInput(_ composingText: inout ComposingText, sequence: String, inputStyle: KanaKanjiConverterModule.InputStyle) { + for char in sequence { + composingText.insertAtCursorPosition(String(char), inputStyle: inputStyle) + } + } + + func requestOptions() -> ConvertRequestOptions { + .withDefaultDictionary( + N_best: 5, + requireJapanesePrediction: true, + requireEnglishPrediction: false, + keyboardLanguage: .ja_JP, + typographyLetterCandidate: false, + unicodeCandidate: true, + englishCandidateInRoman2KanaInput: true, + fullWidthRomanCandidate: false, + halfWidthKanaCandidate: false, + learningType: .nothing, + maxMemoryCount: 0, + shouldResetMemory: false, + memoryDirectoryURL: URL(fileURLWithPath: ""), + sharedContainerURL: URL(fileURLWithPath: ""), + metadata: .init(appVersionString: "Tests") + ) + } + + func testFullConversion() throws { + do { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition("あずーきーはしんじだいのきーぼーどあぷりです", inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + XCTAssertEqual(results.mainResults.first?.text, "azooKeyは新時代のキーボードアプリです") + } + do { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition("ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた", inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + + } + } + + // 1文字ずつ変換する + // memo: 内部実装としては別のモジュールが呼ばれるのだが、それをテストする方法があまりないかもしれない + func testGradualConversion() throws { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた" + for char in text { + c.insertAtCursorPosition(String(char), inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + if c.input.count == text.count { + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } + } + } + + // 1文字ずつ変換する + // memo: 内部実装としては別のモジュールが呼ばれるのだが、それをテストする方法があまりないかもしれない + func testRoman2KanaGradualConversion() throws { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = "youshoukikaratenisusuieiyakyuushourinjikenpounadosamazamanasupoーtuwokeikennsinagarasodatishougakkouzidaiharosanzerusukinkounitaizaisiteorigoruhuyatenisuwonaratteita" + // 許容される変換結果 + let possibles = [ + "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた", + "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスをならっていた" + ] + for char in text { + c.insertAtCursorPosition(String(char), inputStyle: .roman2kana) + let results = converter.requestCandidates(c, options: requestOptions()) + if c.input.count == text.count { + XCTAssertTrue(possibles.contains(results.mainResults.first!.text)) + } + } + } + + // 2,3文字ずつ変換する + // memo: 内部実装としては別のモジュールが呼ばれるのだが、それをテストする方法があまりないかもしれない + func testSemiGradualConversion() throws { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた" + var leftIndex = text.startIndex + + // ランダムに1~5文字ずつ追加していく + while leftIndex != text.endIndex { + let count = Int.random(in: 1 ... 5) + let rightIndex = text.index(leftIndex, offsetBy: count, limitedBy: text.endIndex) ?? text.endIndex + let prefix = String(text[leftIndex ..< rightIndex]) + c.insertAtCursorPosition(prefix, inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + leftIndex = rightIndex + if rightIndex == text.endIndex { + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } + } + } + + // 1文字ずつ入力するが、時折削除を行う + // memo: 内部実装としてはdeleted_last_nのテストを意図している + func testGradualConversionWithDelete() throws { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = Array("ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた") + let deleteIndices = [1, 4, 8, 10, 15, 18, 20, 21, 23, 25, 26, 28, 29, 33, 34, 37, 39, 40, 42, 44, 45, 49, 51, 54, 58, 60, 62, 64, 67, 69, 70, 75, 80] + for (i, char) in text.enumerated() { + c.insertAtCursorPosition(String(char), inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + if deleteIndices.contains(i) { + let count = i % 3 + 1 + c.deleteBackwardFromCursorPosition(count: count) + _ = converter.requestCandidates(c, options: requestOptions()) + + c.insertAtCursorPosition(String(text[i - count + 1 ... i]), inputStyle: .direct) + _ = converter.requestCandidates(c, options: requestOptions()) + } + if c.input.count == text.count { + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } + } + } + + // 必ず正解すべきテストケース + func testMustCases() throws { + // ダイレクト入力 + do { + let cases: [(input: String, expect: String)] = [ + ("つかっている", "使っている"), + ("しんだどうぶつ", "死んだ動物"), + ("けいさん", "計算"), + ("azooKeyをつかう", "azooKeyを使う"), + ("じどうAIそうじゅう。", "自動AI操縦。") + ] + + // full input + var options = requestOptions() + options.requireJapanesePrediction = false + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + sequentialInput(&c, sequence: input, inputStyle: .direct) + let results = converter.requestCandidates(c, options: options) + XCTAssertEqual(results.mainResults.first?.text, expect) + } + // gradual input + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + for char in input { + c.insertAtCursorPosition(String(char), inputStyle: .direct) + let results = converter.requestCandidates(c, options: options) + if c.input.count == input.count { + XCTAssertEqual(results.mainResults.first?.text, expect) + } + } + } + } + // ローマ字入力 + do { + let cases: [(input: String, expect: String)] = [ + ("tukatteiru", "使っている"), + ("sindadoubutu", "死んだ動物"), + ("keisann", "計算") + ] + + // full input + var options = requestOptions() + options.requireJapanesePrediction = false + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + sequentialInput(&c, sequence: input, inputStyle: .roman2kana) + let results = converter.requestCandidates(c, options: options) + XCTAssertEqual(results.mainResults.first?.text, expect) + } + + // gradual input + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + for char in input { + c.insertAtCursorPosition(String(char), inputStyle: .roman2kana) + let results = converter.requestCandidates(c, options: options) + if c.input.count == input.count { + XCTAssertEqual(results.mainResults.first?.text, expect) + } + } + } + } + } + + // 変換結果が比較的一意なテストケースを無数に持ち、一定の割合を正解することを要求する + // 辞書を更新した結果性能が悪化したら気付ける + func testAccuracy() throws { + let cases: [(input: String, expect: [String])] = [ + ("3がつ8にち", ["3月8日"]), + ("いっていのわりあい", ["一定の割合"]), + ("あいふぉんをこうにゅうする", ["iPhoneを購入する"]), + ("それはくさ", ["それは草"]), + ("おにんぎょうさんみたいだね", ["お人形さんみたいだね"]), + ("にほんごぶんぽうのけいしきりろん", ["日本語文法の形式理論"]), + ("ぷらすちっくをさくげんするひつようがある", ["プラスチックを削減する必要がある"]), + ("きりんさんがすきです", ["キリンさんが好きです"]), + ("しんらばんしょうをすべるかみとなる", ["森羅万象を統べる神となる"]), + ("よねづけんしのしんきょく", ["米津玄師の新曲"]), + ("へいろをけんしゅつするもんだい", ["閉路を検出する問題"]), + ("それなすぎる", ["それなすぎる"]), + ("きたねえんだよやりかたが", ["汚ねえんだよやり方が"]), + ("なにわらってんだよ", ["何笑ってんだよ", "なに笑ってんだよ"]), + ("えもみがふかい", ["エモみが深い"]), + ("とうごてきかなかんじへんかん", ["統語的かな漢字変換"]), + ("あなたとふたりでいきをしていたい", ["あなたとふたりで息をしていたい"]), + ("こんごきをつけます", ["今後気をつけます"]), + ("ごめいわくをおかけしてもうしわけありません", ["ご迷惑をおかけして申し訳ありません"]), + ("どうぞよろしくおねがいいたします", ["どうぞよろしくお願いいたします"]), + ("らいぶへんかんでにゅうりょくがかいてきです", ["ライブ変換で入力が快適です"]), + ("にんちかがくがえがきだすにんげんのすがた", ["認知科学が描き出す人間の姿"]), + ("せいしゃいんになりました", ["正社員になりました"]), + ("しけんにでないえいたんご", ["試験に出ない英単語"]), + ("あかるくげんきなせいかつ", ["明るく元気な生活"]), + ("はるがきたのでかふんがつらい", ["春が来たので花粉が辛い"]), + ("しょうぼうたいがひっしにかじをしょうかした", ["消防隊が必死に火事を消火した"]), + ("たけとりものがたりはにほんのこてんぶんがくです", ["竹取物語は日本の古典文学です"]), + ("よとうもやとうもでぃすればちゅうりつ", ["与党も野党もディスれば中立"]), + ("だいすきなえしさん", ["大好きな絵師さん"]), + ("ぱいそんでかかれたそーすこーど", ["Pythonで書かれたソースコード"]), + ("SwiftでつくったApp", ["Swiftで作ったApp"]), + ("かんじょうなんてむだなもん", ["感情なんて無駄なもん"]), + ("ひびをすごす", ["日々を過ごす"]), + ("あたらしいほんをかった", ["新しい本を買った"]), + ("かれのはなしはおもしろい", ["彼の話は面白い"]), + ("ろーかるでうごかす", ["ローカルで動かす"]), + ("よのなかにひつようなのはてすうりょうぜろのでんしけっさい", ["世の中に必要なのは手数料ゼロの電子決済"]), + ("こんしゅうはとてもそーしゃる", ["今週はとてもソーシャル"]), + ("でかすぎるそーすこーど", ["デカすぎるソースコード"]), + ("らちがあかないんだよね", ["埒が明かないんだよね"]), + ("まいなんばーかーどでじゅうみんひょうだせてべんり", ["マイナンバーカードで住民票出せて便利"]), + ("でじたるかなんですか", ["デジタル化なんですか"]), + ("じぶんのひとつしたのせだいがゆうしゅうすぎる", ["自分の一つ下の世代が優秀すぎる"]), + ("みんなしごととごらくとべんきょうをぜんぶやってる", ["みんな仕事と娯楽と勉強を全部やってる"]), + ("ばいようにくたべてみたいね", ["培養肉食べてみたいね"]), + ("おどらされははらすめんと", ["踊らされはハラスメント"]), + ("じんじょうならびょういんにいくれべるのいたみ", ["尋常なら病院に行くレベルの痛み"]), + ("ろぐいんぼーなすてきなしくみがきらい", ["ログインボーナス的な仕組みが嫌い"]), + ("かいにいくのはおまえね", ["買いに行くのはお前ね"]) + ] + + var score: Double = 0 + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + + if expect.contains(results.mainResults[0].text) { + score += 1 + } else if results.mainResults.count > 1 && expect.contains(results.mainResults[1].text) { + score += 0.5 + } else { + print("\(#function) Failure: input \(input), expect \(expect.joined(separator: " | ")), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + } + } + let accuracy = score / Double(cases.count) + print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") + XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < acuracy + } + + // 変換結果が比較的一意なテストケースを無数に持ち、一定の割合を正解することを要求する + // 辞書を更新した結果性能が悪化したら気付ける + // 口語表現を中心にテストする + func testVerbalAccuracy() throws { + let cases: [(input: String, expect: [String])] = [ + ("うわああああ、まじか", ["うわああああ、マジか", "うわああああ、まじか"]), + ("は?", ["は?"]), + ("おまえなんなん", ["お前なんなん"]), + ("めっちゃくさ", ["めっちゃ草"]), + ("はやってんだなぁやっぱり", ["流行ってんだなぁやっぱり"]), + ("そっちかぁ", ["そっちかぁ"]), + ("かみすぎます…!", ["神すぎます…!"]), + ("うおー、りかいした", ["うおー、理解した"]), + ("あ、なるほど", ["あ、なるほど"]), + ("あらま", ["あらま"]), + ("さすがやな…", ["流石やな…"]), + ("のれないんでしょうね。", ["乗れないんでしょうね。"]), + ("おつかれさまですわら", ["お疲れ様です笑", "おつかれさまです笑"]), + ("よううれたのぉわらわら", ["よう売れたのぉ笑笑"]), + ("わーそれはもう", ["わーそれはもう"]), + ("よねんまえやで??", ["4年前やで??", "四年前やで??"]), + ("おうしょうもいいなぁ", ["王将もいいなぁ", "王将も良いなぁ"]), + ("それなすぎる", ["それなすぎる"]), + ("じじつなんでしゃーないです", ["事実なんでしゃーないです"]), + ("がんばりまーーーす!", ["がんばりまーーーす!", "頑張りまーーーす!"]), + ("うるさいよな", ["うるさいよな"]), + ("ほんとどゆことわらわら", ["ほんとどゆこと笑笑"]) + ] + + var score: Double = 0 + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + + if expect.contains(results.mainResults[0].text) { + score += 1 + } else if results.mainResults.count > 1 && expect.contains(results.mainResults[1].text) { + score += 0.5 + } else { + print("\(#function) Failure: input \(input), expect \(expect.joined(separator: " | ")), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + } + } + let accuracy = score / Double(cases.count) + print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") + XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < acuracy + } + + /// MIDベースの文節単位計算でどれだけ同音異義語の判断が向上しているか確認する。 + func testMeaningBasedConversionAccuracy() throws { + let cases: [(input: String, expect: String)] = [ + ("しょうぼう、しょうか、ほのお", "消防、消火、炎"), + ("いえき、しょうか、こうそ", "胃液、消化、酵素"), + + ("さいばん、こうそ、さいこうさい", "裁判、控訴、最高裁"), + ("すいみん、こうそ、けんこう", "睡眠、酵素、健康"), + + ("かたち、こうし、もよう", "形、格子、模様"), + ("そりゅうし、こうし、げんし", "素粒子、光子、原子"), + ("せんせい、こうし、じゅぎょう", "先生、講師、授業"), + ("けんり、こうし、ぎむ", "権利、行使、義務"), + + ("じこ、しぼう、てんごく", "事故、死亡、天国"), + ("とくほ、しぼう、ねんしょう", "トクホ、脂肪、燃焼"), + ("おんしゃ、しぼう、だいがく", "御社、志望、大学"), + + ("しょくぶつ、しゅし、かふん", "植物、種子、花粉"), + ("ぎろん、しゅし、ろんてん", "議論、趣旨、論点"), + ("しんたい、しゅし、てさき", "身体、手指、手先"), + + ("とくしゃ、おんしゃ、しけい", "特赦、恩赦、死刑"), + ("かんじ、おんしゃ、ぶっきょう", "漢字、音写、仏教"), + ("しゅうでん、きしゃ、ていしゃ", "終電、汽車、停車"), + ("はっぴょう、きしゃ、しつもん", "発表、記者、質問"), + + ("がくぶ、しゅうし、はかせ", "学部、修士、博士"), + ("にゅうきん、しゅうし、かくにん", "入金、収支、確認"), + ("じかん、しゅうし、ふそく", "時間、終始、不足"), + + ("ないかく、しじ、ていめい", "内閣、支持、低迷"), + ("じょうし、しじ、ぶか", "上司、指示、部下"), + + ("てんこう、きしょう、じょうほう", "天候、気象、情報"), + ("かれ、きしょう、りょうこう", "彼、気性、良好"), + ("れあめたる、きしょう、じゅうよう", "レアメタル、希少、重要"), + ("あさ、きしょう、しっぱい", "朝、起床、失敗"), + + ("かみ、へんざい、ばんぶつ", "神、遍在、万物"), + ("とみ、へんざい、けいざい", "富、偏在、経済"), + + ("おうよう、きそ、はってん", "応用、基礎、発展"), + ("たいほ、きそ、さいばん", "逮捕、起訴、裁判"), + + ("じこ、ちめい、しぼう", "事故、致命、死亡"), + ("ちず、ちめい、ちり", "地図、地名、地理"), + + ("なんべい、ちり、りょこう", "南米、チリ、旅行"), + ("よごれ、ちり、そうじ", "汚れ、塵、掃除"), + ("ちがく、ちり、べんきょう", "地学、地理、勉強"), + + ("ごおん、ほうこう、ばくふ", "御恩、奉公、幕府"), + ("なんせい、ほうこう、いどう", "南西、方向、移動"), + ("こうすい、ほうこう、におい", "香水、芳香、匂い"), + ("けもの、ほうこう、おたけび", "獣、咆哮、雄叫び"), + + ("つみ、りょうしん、かしゃく", "罪、良心、呵責"), + ("ちち、りょうしん、はは", "父、両親、母"), + + ("せいじ、さんかく、みんしゅう", "政治、参画、民衆"), + ("すうがく、さんかく、しかく", "数学、三角、四角"), + + ("さんかく、しかく、ろっかく", "三角、四角、六角"), + ("じゅけん、しかく、べんきょう", "受験、資格、勉強"), + ("ちょうかく、しかく、きゅうかく", "聴覚、視覚、嗅覚"), + ("あんさつ、しかく、すぱい", "暗殺、刺客、スパイ"), + ("どうろ、しかく、ちゅうい", "道路、死角、注意"), + + ("せいじ、かくしん、かくめい", "政治、革新、革命"), + ("しゅちょう、かくしん、ぎろん", "主張、核心、議論"), + ("せいこう、かくしん、おうえん", "成功、確信、応援"), + + ("せいじ、せいとう、せんきょ", "政治、政党、選挙"), + ("せいぎ、せいとう、だとう", "正義、正当、妥当"), + ("おうけ、せいとう、しょうめい", "王家、正統、証明"), + ("てすと、せいとう、さいてん", "テスト、正答、採点"), + + ("くーでたー、せんきょ、ていこう", "クーデター、占拠、抵抗"), + ("かいさん、せんきょ、かいし", "解散、選挙、開始"), + + ("まつり、さいてん、えんにち", "祭り、祭典、縁日"), + ("てすと、さいてん、まるつけ", "テスト、採点、丸つけ"), + + ("やきゅう、しゅうきゅう、てにす", "野球、蹴球、テニス"), + ("かいしゃ、しゅうきゅう、ふつか", "会社、週休、二日"), + + ("もじ、かんじ、ぞくじ", "文字、漢字、俗字"), + ("きぶん、かんじ、きもち", "気分、感じ、気持ち"), + ("しゅさい、かんじ、のみかい", "主催、幹事、飲み会"), + + ("ぎろん、よち、ざんぞん", "議論、余地、残存"), + ("よげん、よち、みらい", "予言、予知、未来"), + + ("もしゃ、せいぶつ、すけっち", "模写、静物、スケッチ"), + ("どうぶつ、せいぶつ、しよくぶつ", "動物、生物、植物"), + + ("かんのうてき、せいてき、えろ", "官能的、性的、エロ"), + ("どうてき、せいてき、すたてぃっく", "動的、静的、スタティック"), + ("せいじか、せいてき、さくりゃく", "政治家、政敵、策略"), + + ("えくせる、ちかん、けつごう", "Excel、置換、結合"), + ("でんしゃ、ちかん、たいほ", "電車、痴漢、逮捕"), + + ("ふぁんたじー、ようせい、どらごん", "ファンタジー、妖精、ドラゴン"), + ("ころな、ようせい、いんせい", "コロナ、陽性、陰性"), + ("じしゅく、ようせい、むし", "自粛、要請、無視"), + ("いじん、ようせい、わかさ", "偉人、夭逝、若さ"), + + ("いじめ、むし、ほうち", "いじめ、無視、放置"), + ("こんちゅう、むし、ようちゅう", "昆虫、虫、幼虫"), + + ("けんぼう、かいせい、ろんぎ", "憲法、改正、論議"), + ("みょうじ、かいせい、かいめい", "苗字、改姓、改名"), + ("ほんじつ、かいせい、てんき", "本日、快晴、天気"), + ("ぶれーき、かいせい、えんじん", "ブレーキ、回生、エンジン"), + + ("なまえ、かいめい、てつづき", "名前、改名、手続き"), + ("けんきゅう、かいめい、ろんぶん", "研究、解明、論文"), + + ("ごみ、ほうき、きんし", "ゴミ、放棄、禁止"), + ("べんごし、ほうき、ほうりつ", "弁護士、法規、法律"), + ("まじょ、ほうき、まほう", "魔女、箒、魔法"), + ("みんしゅう、ほうき、かくめい", "民衆、蜂起、革命"), + + ("こうじ、しこう、ごねん", "工事、施工、5年"), + ("しんぽう、しこう、しがつ", "新法、施行、4月"), + ("てつがく、しこう、ぎろん", "哲学、思考、議論"), + ("かくりつ、しこう、かいすう", "確率、試行、回数"), + ("あじわい、しこう、わいん", "味わい、嗜好、ワイン"), + + ("たいほ、こうりゅう、さいばん", "逮捕、勾留、裁判"), + ("でんげん、こうりゅう、ちょくりゅう", "電源、交流、直流"), + + ("いでんし、ぶんか、きのう", "遺伝子、分化、機能"), + ("かがく、ぶんか、ぶんげい", "科学、文化、文芸"), + + ("かがく、ゆうき、むき", "化学、有機、無機"), + ("いし、ゆうき、しんねん", "意思、勇気、信念"), + + ("かわべ、いし、いわ", "川辺、石、岩"), + ("しんねん、いし、しそう", "信念、意思、思想"), + ("びょういん、いし、しんさつ", "病院、医師、診察"), + + ("しかい、しんこう、こうえん", "司会、進行、講演"), + ("せんそう、しんこう、しんりゃく", "戦争、侵攻、侵略"), + ("しゅうきょう、しんこう、しんねん", "宗教、信仰、信念"), + ("きんねん、しんこう、しゅうきょう", "近年、新興、宗教"), + + ("びょういん、しかい、はいしゃ", "病院、歯科医、歯医者"), + ("もや、しかい、あっか", "モヤ、視界、悪化"), + ("ばんぐみ、しかい、げいにん", "番組、司会、芸人"), + + ("こども、こうえん、おにごっこ", "子供、公園、鬼ごっこ"), + ("せいじか、こうえん、しちょう", "政治家、講演、視聴"), + ("ちけっと、こうえん、よやく", "チケット、公演、予約"), + + ("くるま、はいしゃ、すくらっぷ", "車、廃車、スクラップ"), + ("むしば、はいしゃ、ちりょう", "虫歯、歯医者、治療"), + ("こんてすと、はいしゃ、ふっかつ", "コンテスト、敗者、復活"), + ("むりょう、はいしゃ、たくしー", "無料、配車、タクシー"), + + ("じんせい、しょうがい、ろうねん", "人生、生涯、老年"), + ("ちょうかく、しょうがい、ほじょ", "聴覚、障害、補助"), + + ("ぐんたい、ぶたい、ぜんめつ", "軍隊、部隊、全滅"), + ("あいどる、ぶたい、おうえん", "アイドル、舞台、応援"), + + ("けいざい、かぶ、げらく", "経済、株、下落"), + ("やさい、かぶ、りょうり", "野菜、カブ、料理"), + ("ぺーじ、かぶ、がぞう", "ページ、下部、画像"), + + ("きまつ、かだい、ていしゅつ", "期末、課題、提出"), + ("のうりょく、かだい、ひょうか", "能力、過大、評価"), + + ("しんらばんしょうをすべるかみ", "森羅万象を統べる神"), + ("こおりをすべるすけーと", "氷を滑るスケート"), + ("おわらいをすべるげいにん", "お笑いをスベる芸人"), + + ("ざっしにのるないよう", "雑誌に載る内容"), + ("くるまにのるひと", "車に乗る人"), + + ("つなみ、てんさい、わざわい", "津波、天災、災い"), + ("さいのう、てんさい、のうりょく", "才能、天才、能力"), + ("がぞう、てんさい、きょか", "画像、転載、許可"), + + ("ひょうしき、きんし、かんばん", "標識、禁止、看板"), + ("こんたくと、きんし、ろうがん", "コンタクト、近視、老眼"), + ("せいぶつ、きんし、ばくてりあ", "生物、菌糸、バクテリア"), + + ("しょり、こうそく、はんてい", "処理、高速、判定"), + ("ぶつり、こうそく、げんかい", "物理、光速、限界"), + ("しんたい、こうそく、たいほ", "身体、拘束、逮捕"), + ("がっこう、こうそく、るーる", "学校、校則、ルール"), + + ("しへい、こうか、じゅうえん", "紙幣、硬貨、10円"), + ("ねだん、こうか、かいとり", "値段、高価、買取"), + ("くすり、こうか、けんしょう", "薬、効果、検証"), + ("でんちゅう、こうか、かせん", "電柱、高架、架線"), + ("がっこう、こうか、がっしょう", "学校、校歌、合唱"), + ("ぱらしゅーと、こうか、らっか", "パラシュート、降下、落下"), + + ("がぞう、かこう、へんしゅう", "画像、加工、編集"), + ("じょうしょう、かこう、けんしょう", "上昇、下降、減少"), + ("かせん、かこう、かわべ", "河川、河口、川辺"), + ("かざん、かこう、ふんか", "火山、火口、噴火"), + + ("いっとうしょう、けんしょう、おうぼ", "一等賞、懸賞、応募"), + ("かせつ、けんしょう、じっし", "仮説、検証、実施"), + ("けんぽう、けんしょう、じょうやく", "憲法、憲章、条約"), + + ("じんこう、げんしょう、りゆう", "人口、減少、理由"), + ("かがく、げんしょう、けんきゅう", "科学、現象、研究"), + + ("ないふ、きょうき、さつがい", "ナイフ、凶器、殺害"), + ("せいしん、きょうき、はっきょう", "精神、狂気、発狂"), + + ("しゅうきょう、きょうぎ、きょうそ", "宗教、教義、教祖"), + ("たいおう、きょうぎ、けんとう", "対応、協議、検討"), + ("すぽーつ、きょうぎ、しょうぶ", "スポーツ、競技、勝負"), + ("じしょ、きょうぎ、いみ", "辞書、狭義、意味"), + + ("じんじゃ、じしゃ、ぶっきよう", "神社、寺社、仏教"), + ("へいしゃ、じしゃ、せいひん", "弊社、自社、製品"), + + ("こうぎょう、きかく、とういつ", "工業、規格、統一"), + ("いべんと、きかく、かいさい", "イベント、企画、開催"), + + ("めがさめたあさ", "目が覚めた朝"), + ("ねつがさめたりょうり", "熱が冷めた料理"), + + ("どうぶつがないたこえ。", "動物が鳴いた声。"), + ("かれがないたこえ。", "彼が泣いた声。"), + + ("りょうりがあついのでさます", "料理が熱いので冷ます"), + ("だいこんがあついのでうすくきる", "大根が厚いので薄く切る"), + ("へやがあついのですずしくする", "部屋が暑いので涼しくする"), + + ("みらい、こだい、げんだい", "未来、古代、現代"), + ("せんでん、こだい、こうこく", "宣伝、誇大、広告"), + + ("かじょう、せいさん、しゅうりょう", "過剰、生産、終了"), + ("さんげき、せいさん、じけん", "惨劇、凄惨、事件"), + ("けいひ、せいさん、れしーと", "経費、生産、レシート"), + + ("しんぷ、せいしょく、きょうかい", "神父、聖職、教会"), + ("こうび、せいしょく、しゅっさん", "交尾、生殖、出産"), + + ("やさいをきるほうちょう", "野菜を切る包丁"), + ("きものをきるしゅみ", "着物を着る趣味"), + + ("さーびす、たいかい、てつづき", "サービス、退会、手続き"), + ("はなび、たいかい、ゆかた", "花火、大会、浴衣"), + + ("しゅみ、はいかい、はいく", "趣味、俳諧、俳句"), + ("ろうじん、はいかい、にんちしょう", "老人、徘徊、認知症"), + + ("おやににたかお", "親に似た顔"), + ("じっくりにたにく", "じっくり煮た肉"), + + ("ちりょう、なんこう、しゅじゅつ", "治療、難航、手術"), + ("ぬりぐすり、なんこう、ききめ", "塗り薬、軟膏、効き目"), + + ("ようえき、ようかい、ようしつ", "溶液、溶解、溶質"), + ("おばけ、ようかい、ゆうれい", "お化け、妖怪、幽霊"), + + ("りょうち、りょうかい、りょうど", "領地、領海、領土"), + ("おーけー、りょうかい、しょうち", "OK、了解、承知"), + + ("がっこう、こうしょう、ばっじ", "学校、校章、バッジ"), + ("かいぎ、こうしょう、しっぱい", "会議、交渉、失敗"), + + ("ようちえん、ようじ、よういく", "幼稚園、幼児、養育"), + ("きんきゅう、ようじ、きたく", "緊急、用事、帰宅"), + + ("おやぶん、こぶん", "親分、子分"), + ("かんぶん、こぶん", "漢文、古文") + ] + + var score: Double = 0 + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + var options = requestOptions() + options.requireJapanesePrediction = false + let results = converter.requestCandidates(c, options: options) + + if results.mainResults[0].text == expect { + score += 1 + } else if results.mainResults.count > 1 && results.mainResults[1].text == expect { + score += 0.5 + } else { + print("\(#function) Failure: input \(input), expect \(expect), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + } + } + let accuracy = score / Double(cases.count) + print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") + XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < accuracy + } + + func testMozcEvaluationData() async throws { + // ダウンロードするURL + let urlString = "https://raw.githubusercontent.com/google/mozc/master/src/data/dictionary_oss/evaluation.tsv" + let url = URL(string: urlString)! + // URLを元にURLオブジェクトを生成 + let (data, _) = try await URLSession.shared.data(from: url) + let content = String(data: data, encoding: .utf8)! + + var mozcScore: Double = 0 + var azooKeyScore: Double = 0 + var cases = 0 + for line in content.split(separator: "\n") { + if line.hasPrefix("#") { + continue + } + let items = line.split(separator: "\t", omittingEmptySubsequences: false) + if items.count != 6 { + continue + } + // 必要な情報を取り出す + let mozcStatus = items[0] == "OK:" + let input = String(items[1]) + let mozcOutput = String(items[2]) + let commandString = items[3] + let command: MozcCommand + if commandString == "Conversion Match" { + command = .conversionMatch + } else if commandString == "Conversion Not Match" { + command = .conversionNotMatch + } else if commandString == "Suggestion Not Expected" { + command = .suggestionNotExpected + } else if commandString.hasPrefix("Conversion Expected") { + if commandString == "Conversion Expected" { + command = .conversionExpected(within: 1) + } else { + let countString = commandString.split(separator: " ").last! + command = .conversionExpected(within: Int(countString)!) + } + } else { + fatalError("Unknown command \(commandString)") + } + + if command == .suggestionNotExpected { + // azooKeyでは扱えないため + continue + } + + if mozcStatus { + mozcScore += 1 + } + + let argument = items[4] + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + var options = requestOptions() + options.requireJapanesePrediction = false + let results = converter.requestCandidates(c, options: options).mainResults + cases += 1 + let azooKeyStatus = mozcEvaluation(command: command, argument: argument, results: results) + if azooKeyStatus { + azooKeyScore += 1 + if !mozcStatus { + print("\(#function) Success over Mozc: \(commandString) \(argument) for input \(input) \(results.prefix(command.requiredCount).map(\.text)), mozcResult: \(mozcOutput)") + } + } else { + if mozcStatus { + print("\(#function) Failure over Mozc: \(commandString) \(argument) for input \(input) \(results.prefix(command.requiredCount).map(\.text)), mozcResult: \(mozcOutput)") + } else { + print("\(#function) Failure: \(commandString) \(argument) for input \(input) \(results.prefix(command.requiredCount).map(\.text)), mozcResult: \(mozcOutput)") + } + } + } + print("\(#function) Result: Mozc Score: \(mozcScore), azooKeyScore \(azooKeyScore), count \(cases)") + XCTAssertTrue(mozcScore > 0) + XCTAssertTrue(azooKeyScore > 0) + XCTExpectFailure("azooKey is not as accurate as Mozc currently in this mertics, due to some reason") { + XCTAssertTrue(mozcScore < azooKeyScore) + } + } + + enum MozcCommand: Equatable { + /// 変換に`arg`が現れる + case conversionMatch + /// 変換に`arg`が現れない + case conversionNotMatch + /// n番目までに`arg`が登場 + case conversionExpected(within: Int) + /// サジェストに`arg`が登場しない + case suggestionNotExpected + + var requiredCount: Int { + switch self { + case .conversionMatch, .conversionNotMatch, .suggestionNotExpected: + return 1 + case .conversionExpected(within: let count): + return count + } + } + } + + private func mozcEvaluation(command: MozcCommand, argument: some StringProtocol, results: [Candidate]) -> Bool { + guard let first = results.first else { + return false + } + switch command { + case .conversionMatch: + if first.text.contains(argument) { + return true + } + case .conversionNotMatch: + if !first.text.contains(argument) { + return true + } + case .suggestionNotExpected: + fatalError("mozcEvaluation does not support command \(command)") + case .conversionExpected(within: let count): + if results.prefix(count).contains(where: {$0.text == argument}) { + return true + } + } + return false + } +} diff --git a/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/DicdataStoreTests/DicdataStoreTests.swift b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/DicdataStoreTests/DicdataStoreTests.swift new file mode 100644 index 0000000..2111e17 --- /dev/null +++ b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/DicdataStoreTests/DicdataStoreTests.swift @@ -0,0 +1,124 @@ +// +// DicdataStoreTests.swift +// azooKeyTests +// +// Created by ensan on 2023/02/09. +// Copyright © 2023 ensan. All rights reserved. +// + +import KanaKanjiConverterModuleWithDefaultDictionary +import XCTest + +final class DicdataStoreTests: XCTestCase { + func sequentialInput(_ composingText: inout ComposingText, sequence: String, inputStyle: KanaKanjiConverterModule.InputStyle) { + for char in sequence { + composingText.insertAtCursorPosition(String(char), inputStyle: inputStyle) + } + } + + func requestOptions() -> ConvertRequestOptions { + .withDefaultDictionary( + N_best: 5, + requireJapanesePrediction: true, + requireEnglishPrediction: false, + keyboardLanguage: .ja_JP, + typographyLetterCandidate: false, + unicodeCandidate: true, + englishCandidateInRoman2KanaInput: true, + fullWidthRomanCandidate: false, + halfWidthKanaCandidate: false, + learningType: .nothing, + maxMemoryCount: 0, + shouldResetMemory: false, + memoryDirectoryURL: URL(fileURLWithPath: ""), + sharedContainerURL: URL(fileURLWithPath: ""), + metadata: .init(appVersionString: "Tests") + ) + } + + /// 絶対に変換できるべき候補をここに記述する + /// - 主に「変換できない」と報告のあった候補を追加する + func testMustWords() throws { + let dicdataStore = DicdataStore(convertRequestOptions: requestOptions()) + let mustWords = [ + ("アサッテ", "明後日"), + ("オトトシ", "一昨年"), + ("ダイヒョウ", "代表"), + ("テキナ", "的な"), + ("ヤマダ", "山田"), + ("アイロ", "隘路"), + ("フツカ", "二日"), + ("フツカ", "2日"), + ("ガデンインスイ", "我田引水"), + ("フトウフクツ", "不撓不屈"), + ("ナンタイ", "軟体"), + ("ナンジ", "何時"), + ("ナド", "等") + ] + for (key, word) in mustWords { + var c = ComposingText() + c.insertAtCursorPosition(key, inputStyle: .direct) + let result = dicdataStore.getLOUDSData(inputData: c, from: 0, to: c.input.endIndex - 1) + // 冗長な書き方だが、こうすることで「どの項目でエラーが発生したのか」がはっきりするため、こう書いている。 + XCTAssertEqual(result.first(where: {$0.data.word == word})?.data.word, word) + } + } + + /// 入っていてはおかしい候補をここに記述する + /// - 主に以前混入していたが取り除いた語を記述する + func testMustNotWords() throws { + let dicdataStore = DicdataStore(convertRequestOptions: requestOptions()) + let mustWords = [ + ("タイ", "体."), + ("アサッテ", "明日"), + ("チョ", "ちょwww"), + ("シンコウホウホウ", "進行方向"), + ("a", "あ"), // direct入力の場合「a」で「あ」をサジェストしてはいけない + ("\\n", "\n") + ] + for (key, word) in mustWords { + var c = ComposingText() + c.insertAtCursorPosition(key, inputStyle: .direct) + let result = dicdataStore.getLOUDSData(inputData: c, from: 0, to: c.input.endIndex - 1) + XCTAssertNil(result.first(where: {$0.data.word == word && $0.data.ruby == key})) + } + } + + func testGetLOUDSDataInRange() throws { + let dicdataStore = DicdataStore(convertRequestOptions: requestOptions()) + do { + var c = ComposingText() + c.insertAtCursorPosition("ヘンカン", inputStyle: .roman2kana) + let result = dicdataStore.getLOUDSDataInRange(inputData: c, from: 0, toIndexRange: 2..<4) + XCTAssertFalse(result.contains(where: {$0.data.word == "変"})) + XCTAssertTrue(result.contains(where: {$0.data.word == "変化"})) + XCTAssertTrue(result.contains(where: {$0.data.word == "変換"})) + } + do { + var c = ComposingText() + c.insertAtCursorPosition("ヘンカン", inputStyle: .roman2kana) + let result = dicdataStore.getLOUDSDataInRange(inputData: c, from: 0, toIndexRange: 0..<4) + XCTAssertTrue(result.contains(where: {$0.data.word == "変"})) + XCTAssertTrue(result.contains(where: {$0.data.word == "変化"})) + XCTAssertTrue(result.contains(where: {$0.data.word == "変換"})) + } + do { + var c = ComposingText() + c.insertAtCursorPosition("ツカッ", inputStyle: .roman2kana) + let result = dicdataStore.getLOUDSDataInRange(inputData: c, from: 0, toIndexRange: 2..<3) + XCTAssertTrue(result.contains(where: {$0.data.word == "使っ"})) + } + do { + var c = ComposingText() + c.insertAtCursorPosition("ツカッt", inputStyle: .roman2kana) + let result = dicdataStore.getLOUDSDataInRange(inputData: c, from: 0, toIndexRange: 2..<4) + XCTAssertTrue(result.contains(where: {$0.data.word == "使っ"})) + } + do { + var c = ComposingText() + sequentialInput(&c, sequence: "tukatt", inputStyle: .roman2kana) + let result = dicdataStore.getLOUDSDataInRange(inputData: c, from: 0, toIndexRange: 4..<6) + XCTAssertTrue(result.contains(where: {$0.data.word == "使っ"})) + } + } +} From dbbaec32a04fd8950196a5efdb7379ae5bf4b290 Mon Sep 17 00:00:00 2001 From: Miwa / Ensan Date: Tue, 20 Feb 2024 00:46:43 +0900 Subject: [PATCH 2/5] update workflow --- .github/workflows/swift.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 344e6f8..e26f605 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -18,8 +18,10 @@ jobs: - uses: swift-actions/setup-swift@150267bf6ba01f9d942a4bd55aa2f35ba586767d with: swift-version: "5.9" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + submodules: true - name: Build run: swift build -Xswiftc -strict-concurrency=complete -v - name: Run tests - run: swift test -Xswiftc -strict-concurrency=complete -v + run: swift test -c release -Xswiftc -strict-concurrency=complete -v From 0b61b00076d57e0af45a0135c8c6576a558c4b39 Mon Sep 17 00:00:00 2001 From: Miwa / Ensan Date: Tue, 20 Feb 2024 00:47:51 +0900 Subject: [PATCH 3/5] fix indent --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e26f605..e30f256 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -19,8 +19,8 @@ jobs: with: swift-version: "5.9" - uses: actions/checkout@v4 - with: - submodules: true + with: + submodules: true - name: Build run: swift build -Xswiftc -strict-concurrency=complete -v - name: Run tests From bb42d0156317a8eec76473ea4bd2d67c16d78c6a Mon Sep 17 00:00:00 2001 From: Miwa / Ensan Date: Tue, 20 Feb 2024 00:57:27 +0900 Subject: [PATCH 4/5] make testMozcEvaluationData macOS-only to avoid build failure in Linux --- .../ConverterTests/ConverterTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift index 9b1c0f7..51ea091 100644 --- a/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift +++ b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift @@ -631,6 +631,7 @@ import XCTest XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < accuracy } + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) func testMozcEvaluationData() async throws { // ダウンロードするURL let urlString = "https://raw.githubusercontent.com/google/mozc/master/src/data/dictionary_oss/evaluation.tsv" @@ -711,6 +712,7 @@ import XCTest XCTAssertTrue(mozcScore < azooKeyScore) } } + #endif enum MozcCommand: Equatable { /// 変換に`arg`が現れる From a072be91d44a802b49f2d68a02fef9288505cefe Mon Sep 17 00:00:00 2001 From: Miwa / Ensan Date: Tue, 20 Feb 2024 01:05:13 +0900 Subject: [PATCH 5/5] Avoid @MainActor class --- .../ConverterTests/ConverterTests.swift | 943 +++++++++--------- 1 file changed, 480 insertions(+), 463 deletions(-) diff --git a/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift index 51ea091..78ad2e6 100644 --- a/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift +++ b/Tests/KanaKanjiConverterModuleWithDefaultDictionaryTests/ConverterTests/ConverterTests.swift @@ -10,7 +10,7 @@ import Foundation import KanaKanjiConverterModuleWithDefaultDictionary import XCTest -@MainActor final class ConverterTests: XCTestCase { +final class ConverterTests: XCTestCase { func sequentialInput(_ composingText: inout ComposingText, sequence: String, inputStyle: KanaKanjiConverterModule.InputStyle) { for char in sequence { composingText.insertAtCursorPosition(String(char), inputStyle: inputStyle) @@ -37,168 +37,179 @@ import XCTest ) } - func testFullConversion() throws { - do { - let converter = KanaKanjiConverter() - var c = ComposingText() - c.insertAtCursorPosition("あずーきーはしんじだいのきーぼーどあぷりです", inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) - XCTAssertEqual(results.mainResults.first?.text, "azooKeyは新時代のキーボードアプリです") - } - do { - let converter = KanaKanjiConverter() - var c = ComposingText() - c.insertAtCursorPosition("ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた", inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) - XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") - + func testFullConversion() async throws { + await MainActor.run { + do { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition("あずーきーはしんじだいのきーぼーどあぷりです", inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + XCTAssertEqual(results.mainResults.first?.text, "azooKeyは新時代のキーボードアプリです") + } + do { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition("ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた", inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } } } // 1文字ずつ変換する // memo: 内部実装としては別のモジュールが呼ばれるのだが、それをテストする方法があまりないかもしれない - func testGradualConversion() throws { - let converter = KanaKanjiConverter() - var c = ComposingText() - let text = "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた" - for char in text { - c.insertAtCursorPosition(String(char), inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) - if c.input.count == text.count { - XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + func testGradualConversion() async throws { + await MainActor.run { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた" + for char in text { + c.insertAtCursorPosition(String(char), inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + if c.input.count == text.count { + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } } } } // 1文字ずつ変換する // memo: 内部実装としては別のモジュールが呼ばれるのだが、それをテストする方法があまりないかもしれない - func testRoman2KanaGradualConversion() throws { - let converter = KanaKanjiConverter() - var c = ComposingText() - let text = "youshoukikaratenisusuieiyakyuushourinjikenpounadosamazamanasupoーtuwokeikennsinagarasodatishougakkouzidaiharosanzerusukinkounitaizaisiteorigoruhuyatenisuwonaratteita" - // 許容される変換結果 - let possibles = [ - "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた", - "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスをならっていた" - ] - for char in text { - c.insertAtCursorPosition(String(char), inputStyle: .roman2kana) - let results = converter.requestCandidates(c, options: requestOptions()) - if c.input.count == text.count { - XCTAssertTrue(possibles.contains(results.mainResults.first!.text)) + func testRoman2KanaGradualConversion() async throws { + await MainActor.run { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = "youshoukikaratenisusuieiyakyuushourinjikenpounadosamazamanasupoーtuwokeikennsinagarasodatishougakkouzidaiharosanzerusukinkounitaizaisiteorigoruhuyatenisuwonaratteita" + // 許容される変換結果 + let possibles = [ + "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた", + "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスをならっていた" + ] + for char in text { + c.insertAtCursorPosition(String(char), inputStyle: .roman2kana) + let results = converter.requestCandidates(c, options: requestOptions()) + if c.input.count == text.count { + XCTAssertTrue(possibles.contains(results.mainResults.first!.text)) + } } } } // 2,3文字ずつ変換する // memo: 内部実装としては別のモジュールが呼ばれるのだが、それをテストする方法があまりないかもしれない - func testSemiGradualConversion() throws { - let converter = KanaKanjiConverter() - var c = ComposingText() - let text = "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた" - var leftIndex = text.startIndex - - // ランダムに1~5文字ずつ追加していく - while leftIndex != text.endIndex { - let count = Int.random(in: 1 ... 5) - let rightIndex = text.index(leftIndex, offsetBy: count, limitedBy: text.endIndex) ?? text.endIndex - let prefix = String(text[leftIndex ..< rightIndex]) - c.insertAtCursorPosition(prefix, inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) - leftIndex = rightIndex - if rightIndex == text.endIndex { - XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + func testSemiGradualConversion() async throws { + await MainActor.run { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた" + var leftIndex = text.startIndex + + // ランダムに1~5文字ずつ追加していく + while leftIndex != text.endIndex { + let count = Int.random(in: 1 ... 5) + let rightIndex = text.index(leftIndex, offsetBy: count, limitedBy: text.endIndex) ?? text.endIndex + let prefix = String(text[leftIndex ..< rightIndex]) + c.insertAtCursorPosition(prefix, inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + leftIndex = rightIndex + if rightIndex == text.endIndex { + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } } } } // 1文字ずつ入力するが、時折削除を行う // memo: 内部実装としてはdeleted_last_nのテストを意図している - func testGradualConversionWithDelete() throws { - let converter = KanaKanjiConverter() - var c = ComposingText() - let text = Array("ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた") - let deleteIndices = [1, 4, 8, 10, 15, 18, 20, 21, 23, 25, 26, 28, 29, 33, 34, 37, 39, 40, 42, 44, 45, 49, 51, 54, 58, 60, 62, 64, 67, 69, 70, 75, 80] - for (i, char) in text.enumerated() { - c.insertAtCursorPosition(String(char), inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) - if deleteIndices.contains(i) { - let count = i % 3 + 1 - c.deleteBackwardFromCursorPosition(count: count) - _ = converter.requestCandidates(c, options: requestOptions()) - - c.insertAtCursorPosition(String(text[i - count + 1 ... i]), inputStyle: .direct) - _ = converter.requestCandidates(c, options: requestOptions()) - } - if c.input.count == text.count { - XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + func testGradualConversionWithDelete() async throws { + await MainActor.run { + let converter = KanaKanjiConverter() + var c = ComposingText() + let text = Array("ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた") + let deleteIndices = [1, 4, 8, 10, 15, 18, 20, 21, 23, 25, 26, 28, 29, 33, 34, 37, 39, 40, 42, 44, 45, 49, 51, 54, 58, 60, 62, 64, 67, 69, 70, 75, 80] + for (i, char) in text.enumerated() { + c.insertAtCursorPosition(String(char), inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + if deleteIndices.contains(i) { + let count = i % 3 + 1 + c.deleteBackwardFromCursorPosition(count: count) + _ = converter.requestCandidates(c, options: requestOptions()) + + c.insertAtCursorPosition(String(text[i - count + 1 ... i]), inputStyle: .direct) + _ = converter.requestCandidates(c, options: requestOptions()) + } + if c.input.count == text.count { + XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた") + } } } } // 必ず正解すべきテストケース - func testMustCases() throws { - // ダイレクト入力 - do { - let cases: [(input: String, expect: String)] = [ - ("つかっている", "使っている"), - ("しんだどうぶつ", "死んだ動物"), - ("けいさん", "計算"), - ("azooKeyをつかう", "azooKeyを使う"), - ("じどうAIそうじゅう。", "自動AI操縦。") - ] - - // full input - var options = requestOptions() - options.requireJapanesePrediction = false - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - sequentialInput(&c, sequence: input, inputStyle: .direct) - let results = converter.requestCandidates(c, options: options) - XCTAssertEqual(results.mainResults.first?.text, expect) - } - // gradual input - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - for char in input { - c.insertAtCursorPosition(String(char), inputStyle: .direct) + func testMustCases() async throws { + await MainActor.run { + // ダイレクト入力 + do { + let cases: [(input: String, expect: String)] = [ + ("つかっている", "使っている"), + ("しんだどうぶつ", "死んだ動物"), + ("けいさん", "計算"), + ("azooKeyをつかう", "azooKeyを使う"), + ("じどうAIそうじゅう。", "自動AI操縦。") + ] + + // full input + var options = requestOptions() + options.requireJapanesePrediction = false + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + sequentialInput(&c, sequence: input, inputStyle: .direct) let results = converter.requestCandidates(c, options: options) - if c.input.count == input.count { - XCTAssertEqual(results.mainResults.first?.text, expect) + XCTAssertEqual(results.mainResults.first?.text, expect) + } + // gradual input + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + for char in input { + c.insertAtCursorPosition(String(char), inputStyle: .direct) + let results = converter.requestCandidates(c, options: options) + if c.input.count == input.count { + XCTAssertEqual(results.mainResults.first?.text, expect) + } } } } - } - // ローマ字入力 - do { - let cases: [(input: String, expect: String)] = [ - ("tukatteiru", "使っている"), - ("sindadoubutu", "死んだ動物"), - ("keisann", "計算") - ] - - // full input - var options = requestOptions() - options.requireJapanesePrediction = false - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - sequentialInput(&c, sequence: input, inputStyle: .roman2kana) - let results = converter.requestCandidates(c, options: options) - XCTAssertEqual(results.mainResults.first?.text, expect) - } - - // gradual input - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - for char in input { - c.insertAtCursorPosition(String(char), inputStyle: .roman2kana) + // ローマ字入力 + do { + let cases: [(input: String, expect: String)] = [ + ("tukatteiru", "使っている"), + ("sindadoubutu", "死んだ動物"), + ("keisann", "計算") + ] + + // full input + var options = requestOptions() + options.requireJapanesePrediction = false + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + sequentialInput(&c, sequence: input, inputStyle: .roman2kana) let results = converter.requestCandidates(c, options: options) - if c.input.count == input.count { - XCTAssertEqual(results.mainResults.first?.text, expect) + XCTAssertEqual(results.mainResults.first?.text, expect) + } + + // gradual input + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + for char in input { + c.insertAtCursorPosition(String(char), inputStyle: .roman2kana) + let results = converter.requestCandidates(c, options: options) + if c.input.count == input.count { + XCTAssertEqual(results.mainResults.first?.text, expect) + } } } } @@ -207,428 +218,434 @@ import XCTest // 変換結果が比較的一意なテストケースを無数に持ち、一定の割合を正解することを要求する // 辞書を更新した結果性能が悪化したら気付ける - func testAccuracy() throws { - let cases: [(input: String, expect: [String])] = [ - ("3がつ8にち", ["3月8日"]), - ("いっていのわりあい", ["一定の割合"]), - ("あいふぉんをこうにゅうする", ["iPhoneを購入する"]), - ("それはくさ", ["それは草"]), - ("おにんぎょうさんみたいだね", ["お人形さんみたいだね"]), - ("にほんごぶんぽうのけいしきりろん", ["日本語文法の形式理論"]), - ("ぷらすちっくをさくげんするひつようがある", ["プラスチックを削減する必要がある"]), - ("きりんさんがすきです", ["キリンさんが好きです"]), - ("しんらばんしょうをすべるかみとなる", ["森羅万象を統べる神となる"]), - ("よねづけんしのしんきょく", ["米津玄師の新曲"]), - ("へいろをけんしゅつするもんだい", ["閉路を検出する問題"]), - ("それなすぎる", ["それなすぎる"]), - ("きたねえんだよやりかたが", ["汚ねえんだよやり方が"]), - ("なにわらってんだよ", ["何笑ってんだよ", "なに笑ってんだよ"]), - ("えもみがふかい", ["エモみが深い"]), - ("とうごてきかなかんじへんかん", ["統語的かな漢字変換"]), - ("あなたとふたりでいきをしていたい", ["あなたとふたりで息をしていたい"]), - ("こんごきをつけます", ["今後気をつけます"]), - ("ごめいわくをおかけしてもうしわけありません", ["ご迷惑をおかけして申し訳ありません"]), - ("どうぞよろしくおねがいいたします", ["どうぞよろしくお願いいたします"]), - ("らいぶへんかんでにゅうりょくがかいてきです", ["ライブ変換で入力が快適です"]), - ("にんちかがくがえがきだすにんげんのすがた", ["認知科学が描き出す人間の姿"]), - ("せいしゃいんになりました", ["正社員になりました"]), - ("しけんにでないえいたんご", ["試験に出ない英単語"]), - ("あかるくげんきなせいかつ", ["明るく元気な生活"]), - ("はるがきたのでかふんがつらい", ["春が来たので花粉が辛い"]), - ("しょうぼうたいがひっしにかじをしょうかした", ["消防隊が必死に火事を消火した"]), - ("たけとりものがたりはにほんのこてんぶんがくです", ["竹取物語は日本の古典文学です"]), - ("よとうもやとうもでぃすればちゅうりつ", ["与党も野党もディスれば中立"]), - ("だいすきなえしさん", ["大好きな絵師さん"]), - ("ぱいそんでかかれたそーすこーど", ["Pythonで書かれたソースコード"]), - ("SwiftでつくったApp", ["Swiftで作ったApp"]), - ("かんじょうなんてむだなもん", ["感情なんて無駄なもん"]), - ("ひびをすごす", ["日々を過ごす"]), - ("あたらしいほんをかった", ["新しい本を買った"]), - ("かれのはなしはおもしろい", ["彼の話は面白い"]), - ("ろーかるでうごかす", ["ローカルで動かす"]), - ("よのなかにひつようなのはてすうりょうぜろのでんしけっさい", ["世の中に必要なのは手数料ゼロの電子決済"]), - ("こんしゅうはとてもそーしゃる", ["今週はとてもソーシャル"]), - ("でかすぎるそーすこーど", ["デカすぎるソースコード"]), - ("らちがあかないんだよね", ["埒が明かないんだよね"]), - ("まいなんばーかーどでじゅうみんひょうだせてべんり", ["マイナンバーカードで住民票出せて便利"]), - ("でじたるかなんですか", ["デジタル化なんですか"]), - ("じぶんのひとつしたのせだいがゆうしゅうすぎる", ["自分の一つ下の世代が優秀すぎる"]), - ("みんなしごととごらくとべんきょうをぜんぶやってる", ["みんな仕事と娯楽と勉強を全部やってる"]), - ("ばいようにくたべてみたいね", ["培養肉食べてみたいね"]), - ("おどらされははらすめんと", ["踊らされはハラスメント"]), - ("じんじょうならびょういんにいくれべるのいたみ", ["尋常なら病院に行くレベルの痛み"]), - ("ろぐいんぼーなすてきなしくみがきらい", ["ログインボーナス的な仕組みが嫌い"]), - ("かいにいくのはおまえね", ["買いに行くのはお前ね"]) - ] - - var score: Double = 0 - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - c.insertAtCursorPosition(input, inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) + func testAccuracy() async throws { + await MainActor.run { + let cases: [(input: String, expect: [String])] = [ + ("3がつ8にち", ["3月8日"]), + ("いっていのわりあい", ["一定の割合"]), + ("あいふぉんをこうにゅうする", ["iPhoneを購入する"]), + ("それはくさ", ["それは草"]), + ("おにんぎょうさんみたいだね", ["お人形さんみたいだね"]), + ("にほんごぶんぽうのけいしきりろん", ["日本語文法の形式理論"]), + ("ぷらすちっくをさくげんするひつようがある", ["プラスチックを削減する必要がある"]), + ("きりんさんがすきです", ["キリンさんが好きです"]), + ("しんらばんしょうをすべるかみとなる", ["森羅万象を統べる神となる"]), + ("よねづけんしのしんきょく", ["米津玄師の新曲"]), + ("へいろをけんしゅつするもんだい", ["閉路を検出する問題"]), + ("それなすぎる", ["それなすぎる"]), + ("きたねえんだよやりかたが", ["汚ねえんだよやり方が"]), + ("なにわらってんだよ", ["何笑ってんだよ", "なに笑ってんだよ"]), + ("えもみがふかい", ["エモみが深い"]), + ("とうごてきかなかんじへんかん", ["統語的かな漢字変換"]), + ("あなたとふたりでいきをしていたい", ["あなたとふたりで息をしていたい"]), + ("こんごきをつけます", ["今後気をつけます"]), + ("ごめいわくをおかけしてもうしわけありません", ["ご迷惑をおかけして申し訳ありません"]), + ("どうぞよろしくおねがいいたします", ["どうぞよろしくお願いいたします"]), + ("らいぶへんかんでにゅうりょくがかいてきです", ["ライブ変換で入力が快適です"]), + ("にんちかがくがえがきだすにんげんのすがた", ["認知科学が描き出す人間の姿"]), + ("せいしゃいんになりました", ["正社員になりました"]), + ("しけんにでないえいたんご", ["試験に出ない英単語"]), + ("あかるくげんきなせいかつ", ["明るく元気な生活"]), + ("はるがきたのでかふんがつらい", ["春が来たので花粉が辛い"]), + ("しょうぼうたいがひっしにかじをしょうかした", ["消防隊が必死に火事を消火した"]), + ("たけとりものがたりはにほんのこてんぶんがくです", ["竹取物語は日本の古典文学です"]), + ("よとうもやとうもでぃすればちゅうりつ", ["与党も野党もディスれば中立"]), + ("だいすきなえしさん", ["大好きな絵師さん"]), + ("ぱいそんでかかれたそーすこーど", ["Pythonで書かれたソースコード"]), + ("SwiftでつくったApp", ["Swiftで作ったApp"]), + ("かんじょうなんてむだなもん", ["感情なんて無駄なもん"]), + ("ひびをすごす", ["日々を過ごす"]), + ("あたらしいほんをかった", ["新しい本を買った"]), + ("かれのはなしはおもしろい", ["彼の話は面白い"]), + ("ろーかるでうごかす", ["ローカルで動かす"]), + ("よのなかにひつようなのはてすうりょうぜろのでんしけっさい", ["世の中に必要なのは手数料ゼロの電子決済"]), + ("こんしゅうはとてもそーしゃる", ["今週はとてもソーシャル"]), + ("でかすぎるそーすこーど", ["デカすぎるソースコード"]), + ("らちがあかないんだよね", ["埒が明かないんだよね"]), + ("まいなんばーかーどでじゅうみんひょうだせてべんり", ["マイナンバーカードで住民票出せて便利"]), + ("でじたるかなんですか", ["デジタル化なんですか"]), + ("じぶんのひとつしたのせだいがゆうしゅうすぎる", ["自分の一つ下の世代が優秀すぎる"]), + ("みんなしごととごらくとべんきょうをぜんぶやってる", ["みんな仕事と娯楽と勉強を全部やってる"]), + ("ばいようにくたべてみたいね", ["培養肉食べてみたいね"]), + ("おどらされははらすめんと", ["踊らされはハラスメント"]), + ("じんじょうならびょういんにいくれべるのいたみ", ["尋常なら病院に行くレベルの痛み"]), + ("ろぐいんぼーなすてきなしくみがきらい", ["ログインボーナス的な仕組みが嫌い"]), + ("かいにいくのはおまえね", ["買いに行くのはお前ね"]) + ] - if expect.contains(results.mainResults[0].text) { - score += 1 - } else if results.mainResults.count > 1 && expect.contains(results.mainResults[1].text) { - score += 0.5 - } else { - print("\(#function) Failure: input \(input), expect \(expect.joined(separator: " | ")), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + var score: Double = 0 + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + + if expect.contains(results.mainResults[0].text) { + score += 1 + } else if results.mainResults.count > 1 && expect.contains(results.mainResults[1].text) { + score += 0.5 + } else { + print("\(#function) Failure: input \(input), expect \(expect.joined(separator: " | ")), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + } } + let accuracy = score / Double(cases.count) + print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") + XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < acuracy } - let accuracy = score / Double(cases.count) - print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") - XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < acuracy } // 変換結果が比較的一意なテストケースを無数に持ち、一定の割合を正解することを要求する // 辞書を更新した結果性能が悪化したら気付ける // 口語表現を中心にテストする - func testVerbalAccuracy() throws { - let cases: [(input: String, expect: [String])] = [ - ("うわああああ、まじか", ["うわああああ、マジか", "うわああああ、まじか"]), - ("は?", ["は?"]), - ("おまえなんなん", ["お前なんなん"]), - ("めっちゃくさ", ["めっちゃ草"]), - ("はやってんだなぁやっぱり", ["流行ってんだなぁやっぱり"]), - ("そっちかぁ", ["そっちかぁ"]), - ("かみすぎます…!", ["神すぎます…!"]), - ("うおー、りかいした", ["うおー、理解した"]), - ("あ、なるほど", ["あ、なるほど"]), - ("あらま", ["あらま"]), - ("さすがやな…", ["流石やな…"]), - ("のれないんでしょうね。", ["乗れないんでしょうね。"]), - ("おつかれさまですわら", ["お疲れ様です笑", "おつかれさまです笑"]), - ("よううれたのぉわらわら", ["よう売れたのぉ笑笑"]), - ("わーそれはもう", ["わーそれはもう"]), - ("よねんまえやで??", ["4年前やで??", "四年前やで??"]), - ("おうしょうもいいなぁ", ["王将もいいなぁ", "王将も良いなぁ"]), - ("それなすぎる", ["それなすぎる"]), - ("じじつなんでしゃーないです", ["事実なんでしゃーないです"]), - ("がんばりまーーーす!", ["がんばりまーーーす!", "頑張りまーーーす!"]), - ("うるさいよな", ["うるさいよな"]), - ("ほんとどゆことわらわら", ["ほんとどゆこと笑笑"]) - ] - - var score: Double = 0 - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - c.insertAtCursorPosition(input, inputStyle: .direct) - let results = converter.requestCandidates(c, options: requestOptions()) + func testVerbalAccuracy() async throws { + await MainActor.run { + let cases: [(input: String, expect: [String])] = [ + ("うわああああ、まじか", ["うわああああ、マジか", "うわああああ、まじか"]), + ("は?", ["は?"]), + ("おまえなんなん", ["お前なんなん"]), + ("めっちゃくさ", ["めっちゃ草"]), + ("はやってんだなぁやっぱり", ["流行ってんだなぁやっぱり"]), + ("そっちかぁ", ["そっちかぁ"]), + ("かみすぎます…!", ["神すぎます…!"]), + ("うおー、りかいした", ["うおー、理解した"]), + ("あ、なるほど", ["あ、なるほど"]), + ("あらま", ["あらま"]), + ("さすがやな…", ["流石やな…"]), + ("のれないんでしょうね。", ["乗れないんでしょうね。"]), + ("おつかれさまですわら", ["お疲れ様です笑", "おつかれさまです笑"]), + ("よううれたのぉわらわら", ["よう売れたのぉ笑笑"]), + ("わーそれはもう", ["わーそれはもう"]), + ("よねんまえやで??", ["4年前やで??", "四年前やで??"]), + ("おうしょうもいいなぁ", ["王将もいいなぁ", "王将も良いなぁ"]), + ("それなすぎる", ["それなすぎる"]), + ("じじつなんでしゃーないです", ["事実なんでしゃーないです"]), + ("がんばりまーーーす!", ["がんばりまーーーす!", "頑張りまーーーす!"]), + ("うるさいよな", ["うるさいよな"]), + ("ほんとどゆことわらわら", ["ほんとどゆこと笑笑"]) + ] - if expect.contains(results.mainResults[0].text) { - score += 1 - } else if results.mainResults.count > 1 && expect.contains(results.mainResults[1].text) { - score += 0.5 - } else { - print("\(#function) Failure: input \(input), expect \(expect.joined(separator: " | ")), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + var score: Double = 0 + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + let results = converter.requestCandidates(c, options: requestOptions()) + + if expect.contains(results.mainResults[0].text) { + score += 1 + } else if results.mainResults.count > 1 && expect.contains(results.mainResults[1].text) { + score += 0.5 + } else { + print("\(#function) Failure: input \(input), expect \(expect.joined(separator: " | ")), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + } } + let accuracy = score / Double(cases.count) + print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") + XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < acuracy } - let accuracy = score / Double(cases.count) - print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") - XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < acuracy } /// MIDベースの文節単位計算でどれだけ同音異義語の判断が向上しているか確認する。 - func testMeaningBasedConversionAccuracy() throws { - let cases: [(input: String, expect: String)] = [ - ("しょうぼう、しょうか、ほのお", "消防、消火、炎"), - ("いえき、しょうか、こうそ", "胃液、消化、酵素"), + func testMeaningBasedConversionAccuracy() async throws { + await MainActor.run { + let cases: [(input: String, expect: String)] = [ + ("しょうぼう、しょうか、ほのお", "消防、消火、炎"), + ("いえき、しょうか、こうそ", "胃液、消化、酵素"), - ("さいばん、こうそ、さいこうさい", "裁判、控訴、最高裁"), - ("すいみん、こうそ、けんこう", "睡眠、酵素、健康"), + ("さいばん、こうそ、さいこうさい", "裁判、控訴、最高裁"), + ("すいみん、こうそ、けんこう", "睡眠、酵素、健康"), - ("かたち、こうし、もよう", "形、格子、模様"), - ("そりゅうし、こうし、げんし", "素粒子、光子、原子"), - ("せんせい、こうし、じゅぎょう", "先生、講師、授業"), - ("けんり、こうし、ぎむ", "権利、行使、義務"), + ("かたち、こうし、もよう", "形、格子、模様"), + ("そりゅうし、こうし、げんし", "素粒子、光子、原子"), + ("せんせい、こうし、じゅぎょう", "先生、講師、授業"), + ("けんり、こうし、ぎむ", "権利、行使、義務"), - ("じこ、しぼう、てんごく", "事故、死亡、天国"), - ("とくほ、しぼう、ねんしょう", "トクホ、脂肪、燃焼"), - ("おんしゃ、しぼう、だいがく", "御社、志望、大学"), + ("じこ、しぼう、てんごく", "事故、死亡、天国"), + ("とくほ、しぼう、ねんしょう", "トクホ、脂肪、燃焼"), + ("おんしゃ、しぼう、だいがく", "御社、志望、大学"), - ("しょくぶつ、しゅし、かふん", "植物、種子、花粉"), - ("ぎろん、しゅし、ろんてん", "議論、趣旨、論点"), - ("しんたい、しゅし、てさき", "身体、手指、手先"), + ("しょくぶつ、しゅし、かふん", "植物、種子、花粉"), + ("ぎろん、しゅし、ろんてん", "議論、趣旨、論点"), + ("しんたい、しゅし、てさき", "身体、手指、手先"), - ("とくしゃ、おんしゃ、しけい", "特赦、恩赦、死刑"), - ("かんじ、おんしゃ、ぶっきょう", "漢字、音写、仏教"), - ("しゅうでん、きしゃ、ていしゃ", "終電、汽車、停車"), - ("はっぴょう、きしゃ、しつもん", "発表、記者、質問"), + ("とくしゃ、おんしゃ、しけい", "特赦、恩赦、死刑"), + ("かんじ、おんしゃ、ぶっきょう", "漢字、音写、仏教"), + ("しゅうでん、きしゃ、ていしゃ", "終電、汽車、停車"), + ("はっぴょう、きしゃ、しつもん", "発表、記者、質問"), - ("がくぶ、しゅうし、はかせ", "学部、修士、博士"), - ("にゅうきん、しゅうし、かくにん", "入金、収支、確認"), - ("じかん、しゅうし、ふそく", "時間、終始、不足"), + ("がくぶ、しゅうし、はかせ", "学部、修士、博士"), + ("にゅうきん、しゅうし、かくにん", "入金、収支、確認"), + ("じかん、しゅうし、ふそく", "時間、終始、不足"), - ("ないかく、しじ、ていめい", "内閣、支持、低迷"), - ("じょうし、しじ、ぶか", "上司、指示、部下"), + ("ないかく、しじ、ていめい", "内閣、支持、低迷"), + ("じょうし、しじ、ぶか", "上司、指示、部下"), - ("てんこう、きしょう、じょうほう", "天候、気象、情報"), - ("かれ、きしょう、りょうこう", "彼、気性、良好"), - ("れあめたる、きしょう、じゅうよう", "レアメタル、希少、重要"), - ("あさ、きしょう、しっぱい", "朝、起床、失敗"), + ("てんこう、きしょう、じょうほう", "天候、気象、情報"), + ("かれ、きしょう、りょうこう", "彼、気性、良好"), + ("れあめたる、きしょう、じゅうよう", "レアメタル、希少、重要"), + ("あさ、きしょう、しっぱい", "朝、起床、失敗"), - ("かみ、へんざい、ばんぶつ", "神、遍在、万物"), - ("とみ、へんざい、けいざい", "富、偏在、経済"), + ("かみ、へんざい、ばんぶつ", "神、遍在、万物"), + ("とみ、へんざい、けいざい", "富、偏在、経済"), - ("おうよう、きそ、はってん", "応用、基礎、発展"), - ("たいほ、きそ、さいばん", "逮捕、起訴、裁判"), + ("おうよう、きそ、はってん", "応用、基礎、発展"), + ("たいほ、きそ、さいばん", "逮捕、起訴、裁判"), - ("じこ、ちめい、しぼう", "事故、致命、死亡"), - ("ちず、ちめい、ちり", "地図、地名、地理"), + ("じこ、ちめい、しぼう", "事故、致命、死亡"), + ("ちず、ちめい、ちり", "地図、地名、地理"), - ("なんべい、ちり、りょこう", "南米、チリ、旅行"), - ("よごれ、ちり、そうじ", "汚れ、塵、掃除"), - ("ちがく、ちり、べんきょう", "地学、地理、勉強"), + ("なんべい、ちり、りょこう", "南米、チリ、旅行"), + ("よごれ、ちり、そうじ", "汚れ、塵、掃除"), + ("ちがく、ちり、べんきょう", "地学、地理、勉強"), - ("ごおん、ほうこう、ばくふ", "御恩、奉公、幕府"), - ("なんせい、ほうこう、いどう", "南西、方向、移動"), - ("こうすい、ほうこう、におい", "香水、芳香、匂い"), - ("けもの、ほうこう、おたけび", "獣、咆哮、雄叫び"), + ("ごおん、ほうこう、ばくふ", "御恩、奉公、幕府"), + ("なんせい、ほうこう、いどう", "南西、方向、移動"), + ("こうすい、ほうこう、におい", "香水、芳香、匂い"), + ("けもの、ほうこう、おたけび", "獣、咆哮、雄叫び"), - ("つみ、りょうしん、かしゃく", "罪、良心、呵責"), - ("ちち、りょうしん、はは", "父、両親、母"), + ("つみ、りょうしん、かしゃく", "罪、良心、呵責"), + ("ちち、りょうしん、はは", "父、両親、母"), - ("せいじ、さんかく、みんしゅう", "政治、参画、民衆"), - ("すうがく、さんかく、しかく", "数学、三角、四角"), + ("せいじ、さんかく、みんしゅう", "政治、参画、民衆"), + ("すうがく、さんかく、しかく", "数学、三角、四角"), - ("さんかく、しかく、ろっかく", "三角、四角、六角"), - ("じゅけん、しかく、べんきょう", "受験、資格、勉強"), - ("ちょうかく、しかく、きゅうかく", "聴覚、視覚、嗅覚"), - ("あんさつ、しかく、すぱい", "暗殺、刺客、スパイ"), - ("どうろ、しかく、ちゅうい", "道路、死角、注意"), + ("さんかく、しかく、ろっかく", "三角、四角、六角"), + ("じゅけん、しかく、べんきょう", "受験、資格、勉強"), + ("ちょうかく、しかく、きゅうかく", "聴覚、視覚、嗅覚"), + ("あんさつ、しかく、すぱい", "暗殺、刺客、スパイ"), + ("どうろ、しかく、ちゅうい", "道路、死角、注意"), - ("せいじ、かくしん、かくめい", "政治、革新、革命"), - ("しゅちょう、かくしん、ぎろん", "主張、核心、議論"), - ("せいこう、かくしん、おうえん", "成功、確信、応援"), + ("せいじ、かくしん、かくめい", "政治、革新、革命"), + ("しゅちょう、かくしん、ぎろん", "主張、核心、議論"), + ("せいこう、かくしん、おうえん", "成功、確信、応援"), - ("せいじ、せいとう、せんきょ", "政治、政党、選挙"), - ("せいぎ、せいとう、だとう", "正義、正当、妥当"), - ("おうけ、せいとう、しょうめい", "王家、正統、証明"), - ("てすと、せいとう、さいてん", "テスト、正答、採点"), + ("せいじ、せいとう、せんきょ", "政治、政党、選挙"), + ("せいぎ、せいとう、だとう", "正義、正当、妥当"), + ("おうけ、せいとう、しょうめい", "王家、正統、証明"), + ("てすと、せいとう、さいてん", "テスト、正答、採点"), - ("くーでたー、せんきょ、ていこう", "クーデター、占拠、抵抗"), - ("かいさん、せんきょ、かいし", "解散、選挙、開始"), + ("くーでたー、せんきょ、ていこう", "クーデター、占拠、抵抗"), + ("かいさん、せんきょ、かいし", "解散、選挙、開始"), - ("まつり、さいてん、えんにち", "祭り、祭典、縁日"), - ("てすと、さいてん、まるつけ", "テスト、採点、丸つけ"), + ("まつり、さいてん、えんにち", "祭り、祭典、縁日"), + ("てすと、さいてん、まるつけ", "テスト、採点、丸つけ"), - ("やきゅう、しゅうきゅう、てにす", "野球、蹴球、テニス"), - ("かいしゃ、しゅうきゅう、ふつか", "会社、週休、二日"), + ("やきゅう、しゅうきゅう、てにす", "野球、蹴球、テニス"), + ("かいしゃ、しゅうきゅう、ふつか", "会社、週休、二日"), - ("もじ、かんじ、ぞくじ", "文字、漢字、俗字"), - ("きぶん、かんじ、きもち", "気分、感じ、気持ち"), - ("しゅさい、かんじ、のみかい", "主催、幹事、飲み会"), + ("もじ、かんじ、ぞくじ", "文字、漢字、俗字"), + ("きぶん、かんじ、きもち", "気分、感じ、気持ち"), + ("しゅさい、かんじ、のみかい", "主催、幹事、飲み会"), - ("ぎろん、よち、ざんぞん", "議論、余地、残存"), - ("よげん、よち、みらい", "予言、予知、未来"), + ("ぎろん、よち、ざんぞん", "議論、余地、残存"), + ("よげん、よち、みらい", "予言、予知、未来"), - ("もしゃ、せいぶつ、すけっち", "模写、静物、スケッチ"), - ("どうぶつ、せいぶつ、しよくぶつ", "動物、生物、植物"), + ("もしゃ、せいぶつ、すけっち", "模写、静物、スケッチ"), + ("どうぶつ、せいぶつ、しよくぶつ", "動物、生物、植物"), - ("かんのうてき、せいてき、えろ", "官能的、性的、エロ"), - ("どうてき、せいてき、すたてぃっく", "動的、静的、スタティック"), - ("せいじか、せいてき、さくりゃく", "政治家、政敵、策略"), + ("かんのうてき、せいてき、えろ", "官能的、性的、エロ"), + ("どうてき、せいてき、すたてぃっく", "動的、静的、スタティック"), + ("せいじか、せいてき、さくりゃく", "政治家、政敵、策略"), - ("えくせる、ちかん、けつごう", "Excel、置換、結合"), - ("でんしゃ、ちかん、たいほ", "電車、痴漢、逮捕"), + ("えくせる、ちかん、けつごう", "Excel、置換、結合"), + ("でんしゃ、ちかん、たいほ", "電車、痴漢、逮捕"), - ("ふぁんたじー、ようせい、どらごん", "ファンタジー、妖精、ドラゴン"), - ("ころな、ようせい、いんせい", "コロナ、陽性、陰性"), - ("じしゅく、ようせい、むし", "自粛、要請、無視"), - ("いじん、ようせい、わかさ", "偉人、夭逝、若さ"), + ("ふぁんたじー、ようせい、どらごん", "ファンタジー、妖精、ドラゴン"), + ("ころな、ようせい、いんせい", "コロナ、陽性、陰性"), + ("じしゅく、ようせい、むし", "自粛、要請、無視"), + ("いじん、ようせい、わかさ", "偉人、夭逝、若さ"), - ("いじめ、むし、ほうち", "いじめ、無視、放置"), - ("こんちゅう、むし、ようちゅう", "昆虫、虫、幼虫"), + ("いじめ、むし、ほうち", "いじめ、無視、放置"), + ("こんちゅう、むし、ようちゅう", "昆虫、虫、幼虫"), - ("けんぼう、かいせい、ろんぎ", "憲法、改正、論議"), - ("みょうじ、かいせい、かいめい", "苗字、改姓、改名"), - ("ほんじつ、かいせい、てんき", "本日、快晴、天気"), - ("ぶれーき、かいせい、えんじん", "ブレーキ、回生、エンジン"), + ("けんぼう、かいせい、ろんぎ", "憲法、改正、論議"), + ("みょうじ、かいせい、かいめい", "苗字、改姓、改名"), + ("ほんじつ、かいせい、てんき", "本日、快晴、天気"), + ("ぶれーき、かいせい、えんじん", "ブレーキ、回生、エンジン"), - ("なまえ、かいめい、てつづき", "名前、改名、手続き"), - ("けんきゅう、かいめい、ろんぶん", "研究、解明、論文"), + ("なまえ、かいめい、てつづき", "名前、改名、手続き"), + ("けんきゅう、かいめい、ろんぶん", "研究、解明、論文"), - ("ごみ、ほうき、きんし", "ゴミ、放棄、禁止"), - ("べんごし、ほうき、ほうりつ", "弁護士、法規、法律"), - ("まじょ、ほうき、まほう", "魔女、箒、魔法"), - ("みんしゅう、ほうき、かくめい", "民衆、蜂起、革命"), + ("ごみ、ほうき、きんし", "ゴミ、放棄、禁止"), + ("べんごし、ほうき、ほうりつ", "弁護士、法規、法律"), + ("まじょ、ほうき、まほう", "魔女、箒、魔法"), + ("みんしゅう、ほうき、かくめい", "民衆、蜂起、革命"), - ("こうじ、しこう、ごねん", "工事、施工、5年"), - ("しんぽう、しこう、しがつ", "新法、施行、4月"), - ("てつがく、しこう、ぎろん", "哲学、思考、議論"), - ("かくりつ、しこう、かいすう", "確率、試行、回数"), - ("あじわい、しこう、わいん", "味わい、嗜好、ワイン"), + ("こうじ、しこう、ごねん", "工事、施工、5年"), + ("しんぽう、しこう、しがつ", "新法、施行、4月"), + ("てつがく、しこう、ぎろん", "哲学、思考、議論"), + ("かくりつ、しこう、かいすう", "確率、試行、回数"), + ("あじわい、しこう、わいん", "味わい、嗜好、ワイン"), - ("たいほ、こうりゅう、さいばん", "逮捕、勾留、裁判"), - ("でんげん、こうりゅう、ちょくりゅう", "電源、交流、直流"), + ("たいほ、こうりゅう、さいばん", "逮捕、勾留、裁判"), + ("でんげん、こうりゅう、ちょくりゅう", "電源、交流、直流"), - ("いでんし、ぶんか、きのう", "遺伝子、分化、機能"), - ("かがく、ぶんか、ぶんげい", "科学、文化、文芸"), + ("いでんし、ぶんか、きのう", "遺伝子、分化、機能"), + ("かがく、ぶんか、ぶんげい", "科学、文化、文芸"), - ("かがく、ゆうき、むき", "化学、有機、無機"), - ("いし、ゆうき、しんねん", "意思、勇気、信念"), + ("かがく、ゆうき、むき", "化学、有機、無機"), + ("いし、ゆうき、しんねん", "意思、勇気、信念"), - ("かわべ、いし、いわ", "川辺、石、岩"), - ("しんねん、いし、しそう", "信念、意思、思想"), - ("びょういん、いし、しんさつ", "病院、医師、診察"), + ("かわべ、いし、いわ", "川辺、石、岩"), + ("しんねん、いし、しそう", "信念、意思、思想"), + ("びょういん、いし、しんさつ", "病院、医師、診察"), - ("しかい、しんこう、こうえん", "司会、進行、講演"), - ("せんそう、しんこう、しんりゃく", "戦争、侵攻、侵略"), - ("しゅうきょう、しんこう、しんねん", "宗教、信仰、信念"), - ("きんねん、しんこう、しゅうきょう", "近年、新興、宗教"), + ("しかい、しんこう、こうえん", "司会、進行、講演"), + ("せんそう、しんこう、しんりゃく", "戦争、侵攻、侵略"), + ("しゅうきょう、しんこう、しんねん", "宗教、信仰、信念"), + ("きんねん、しんこう、しゅうきょう", "近年、新興、宗教"), - ("びょういん、しかい、はいしゃ", "病院、歯科医、歯医者"), - ("もや、しかい、あっか", "モヤ、視界、悪化"), - ("ばんぐみ、しかい、げいにん", "番組、司会、芸人"), + ("びょういん、しかい、はいしゃ", "病院、歯科医、歯医者"), + ("もや、しかい、あっか", "モヤ、視界、悪化"), + ("ばんぐみ、しかい、げいにん", "番組、司会、芸人"), - ("こども、こうえん、おにごっこ", "子供、公園、鬼ごっこ"), - ("せいじか、こうえん、しちょう", "政治家、講演、視聴"), - ("ちけっと、こうえん、よやく", "チケット、公演、予約"), + ("こども、こうえん、おにごっこ", "子供、公園、鬼ごっこ"), + ("せいじか、こうえん、しちょう", "政治家、講演、視聴"), + ("ちけっと、こうえん、よやく", "チケット、公演、予約"), - ("くるま、はいしゃ、すくらっぷ", "車、廃車、スクラップ"), - ("むしば、はいしゃ、ちりょう", "虫歯、歯医者、治療"), - ("こんてすと、はいしゃ、ふっかつ", "コンテスト、敗者、復活"), - ("むりょう、はいしゃ、たくしー", "無料、配車、タクシー"), + ("くるま、はいしゃ、すくらっぷ", "車、廃車、スクラップ"), + ("むしば、はいしゃ、ちりょう", "虫歯、歯医者、治療"), + ("こんてすと、はいしゃ、ふっかつ", "コンテスト、敗者、復活"), + ("むりょう、はいしゃ、たくしー", "無料、配車、タクシー"), - ("じんせい、しょうがい、ろうねん", "人生、生涯、老年"), - ("ちょうかく、しょうがい、ほじょ", "聴覚、障害、補助"), + ("じんせい、しょうがい、ろうねん", "人生、生涯、老年"), + ("ちょうかく、しょうがい、ほじょ", "聴覚、障害、補助"), - ("ぐんたい、ぶたい、ぜんめつ", "軍隊、部隊、全滅"), - ("あいどる、ぶたい、おうえん", "アイドル、舞台、応援"), + ("ぐんたい、ぶたい、ぜんめつ", "軍隊、部隊、全滅"), + ("あいどる、ぶたい、おうえん", "アイドル、舞台、応援"), - ("けいざい、かぶ、げらく", "経済、株、下落"), - ("やさい、かぶ、りょうり", "野菜、カブ、料理"), - ("ぺーじ、かぶ、がぞう", "ページ、下部、画像"), + ("けいざい、かぶ、げらく", "経済、株、下落"), + ("やさい、かぶ、りょうり", "野菜、カブ、料理"), + ("ぺーじ、かぶ、がぞう", "ページ、下部、画像"), - ("きまつ、かだい、ていしゅつ", "期末、課題、提出"), - ("のうりょく、かだい、ひょうか", "能力、過大、評価"), + ("きまつ、かだい、ていしゅつ", "期末、課題、提出"), + ("のうりょく、かだい、ひょうか", "能力、過大、評価"), - ("しんらばんしょうをすべるかみ", "森羅万象を統べる神"), - ("こおりをすべるすけーと", "氷を滑るスケート"), - ("おわらいをすべるげいにん", "お笑いをスベる芸人"), + ("しんらばんしょうをすべるかみ", "森羅万象を統べる神"), + ("こおりをすべるすけーと", "氷を滑るスケート"), + ("おわらいをすべるげいにん", "お笑いをスベる芸人"), - ("ざっしにのるないよう", "雑誌に載る内容"), - ("くるまにのるひと", "車に乗る人"), + ("ざっしにのるないよう", "雑誌に載る内容"), + ("くるまにのるひと", "車に乗る人"), - ("つなみ、てんさい、わざわい", "津波、天災、災い"), - ("さいのう、てんさい、のうりょく", "才能、天才、能力"), - ("がぞう、てんさい、きょか", "画像、転載、許可"), + ("つなみ、てんさい、わざわい", "津波、天災、災い"), + ("さいのう、てんさい、のうりょく", "才能、天才、能力"), + ("がぞう、てんさい、きょか", "画像、転載、許可"), - ("ひょうしき、きんし、かんばん", "標識、禁止、看板"), - ("こんたくと、きんし、ろうがん", "コンタクト、近視、老眼"), - ("せいぶつ、きんし、ばくてりあ", "生物、菌糸、バクテリア"), + ("ひょうしき、きんし、かんばん", "標識、禁止、看板"), + ("こんたくと、きんし、ろうがん", "コンタクト、近視、老眼"), + ("せいぶつ、きんし、ばくてりあ", "生物、菌糸、バクテリア"), - ("しょり、こうそく、はんてい", "処理、高速、判定"), - ("ぶつり、こうそく、げんかい", "物理、光速、限界"), - ("しんたい、こうそく、たいほ", "身体、拘束、逮捕"), - ("がっこう、こうそく、るーる", "学校、校則、ルール"), + ("しょり、こうそく、はんてい", "処理、高速、判定"), + ("ぶつり、こうそく、げんかい", "物理、光速、限界"), + ("しんたい、こうそく、たいほ", "身体、拘束、逮捕"), + ("がっこう、こうそく、るーる", "学校、校則、ルール"), - ("しへい、こうか、じゅうえん", "紙幣、硬貨、10円"), - ("ねだん、こうか、かいとり", "値段、高価、買取"), - ("くすり、こうか、けんしょう", "薬、効果、検証"), - ("でんちゅう、こうか、かせん", "電柱、高架、架線"), - ("がっこう、こうか、がっしょう", "学校、校歌、合唱"), - ("ぱらしゅーと、こうか、らっか", "パラシュート、降下、落下"), + ("しへい、こうか、じゅうえん", "紙幣、硬貨、10円"), + ("ねだん、こうか、かいとり", "値段、高価、買取"), + ("くすり、こうか、けんしょう", "薬、効果、検証"), + ("でんちゅう、こうか、かせん", "電柱、高架、架線"), + ("がっこう、こうか、がっしょう", "学校、校歌、合唱"), + ("ぱらしゅーと、こうか、らっか", "パラシュート、降下、落下"), - ("がぞう、かこう、へんしゅう", "画像、加工、編集"), - ("じょうしょう、かこう、けんしょう", "上昇、下降、減少"), - ("かせん、かこう、かわべ", "河川、河口、川辺"), - ("かざん、かこう、ふんか", "火山、火口、噴火"), + ("がぞう、かこう、へんしゅう", "画像、加工、編集"), + ("じょうしょう、かこう、けんしょう", "上昇、下降、減少"), + ("かせん、かこう、かわべ", "河川、河口、川辺"), + ("かざん、かこう、ふんか", "火山、火口、噴火"), - ("いっとうしょう、けんしょう、おうぼ", "一等賞、懸賞、応募"), - ("かせつ、けんしょう、じっし", "仮説、検証、実施"), - ("けんぽう、けんしょう、じょうやく", "憲法、憲章、条約"), + ("いっとうしょう、けんしょう、おうぼ", "一等賞、懸賞、応募"), + ("かせつ、けんしょう、じっし", "仮説、検証、実施"), + ("けんぽう、けんしょう、じょうやく", "憲法、憲章、条約"), - ("じんこう、げんしょう、りゆう", "人口、減少、理由"), - ("かがく、げんしょう、けんきゅう", "科学、現象、研究"), + ("じんこう、げんしょう、りゆう", "人口、減少、理由"), + ("かがく、げんしょう、けんきゅう", "科学、現象、研究"), - ("ないふ、きょうき、さつがい", "ナイフ、凶器、殺害"), - ("せいしん、きょうき、はっきょう", "精神、狂気、発狂"), + ("ないふ、きょうき、さつがい", "ナイフ、凶器、殺害"), + ("せいしん、きょうき、はっきょう", "精神、狂気、発狂"), - ("しゅうきょう、きょうぎ、きょうそ", "宗教、教義、教祖"), - ("たいおう、きょうぎ、けんとう", "対応、協議、検討"), - ("すぽーつ、きょうぎ、しょうぶ", "スポーツ、競技、勝負"), - ("じしょ、きょうぎ、いみ", "辞書、狭義、意味"), + ("しゅうきょう、きょうぎ、きょうそ", "宗教、教義、教祖"), + ("たいおう、きょうぎ、けんとう", "対応、協議、検討"), + ("すぽーつ、きょうぎ、しょうぶ", "スポーツ、競技、勝負"), + ("じしょ、きょうぎ、いみ", "辞書、狭義、意味"), - ("じんじゃ、じしゃ、ぶっきよう", "神社、寺社、仏教"), - ("へいしゃ、じしゃ、せいひん", "弊社、自社、製品"), + ("じんじゃ、じしゃ、ぶっきよう", "神社、寺社、仏教"), + ("へいしゃ、じしゃ、せいひん", "弊社、自社、製品"), - ("こうぎょう、きかく、とういつ", "工業、規格、統一"), - ("いべんと、きかく、かいさい", "イベント、企画、開催"), + ("こうぎょう、きかく、とういつ", "工業、規格、統一"), + ("いべんと、きかく、かいさい", "イベント、企画、開催"), - ("めがさめたあさ", "目が覚めた朝"), - ("ねつがさめたりょうり", "熱が冷めた料理"), + ("めがさめたあさ", "目が覚めた朝"), + ("ねつがさめたりょうり", "熱が冷めた料理"), - ("どうぶつがないたこえ。", "動物が鳴いた声。"), - ("かれがないたこえ。", "彼が泣いた声。"), + ("どうぶつがないたこえ。", "動物が鳴いた声。"), + ("かれがないたこえ。", "彼が泣いた声。"), - ("りょうりがあついのでさます", "料理が熱いので冷ます"), - ("だいこんがあついのでうすくきる", "大根が厚いので薄く切る"), - ("へやがあついのですずしくする", "部屋が暑いので涼しくする"), + ("りょうりがあついのでさます", "料理が熱いので冷ます"), + ("だいこんがあついのでうすくきる", "大根が厚いので薄く切る"), + ("へやがあついのですずしくする", "部屋が暑いので涼しくする"), - ("みらい、こだい、げんだい", "未来、古代、現代"), - ("せんでん、こだい、こうこく", "宣伝、誇大、広告"), + ("みらい、こだい、げんだい", "未来、古代、現代"), + ("せんでん、こだい、こうこく", "宣伝、誇大、広告"), - ("かじょう、せいさん、しゅうりょう", "過剰、生産、終了"), - ("さんげき、せいさん、じけん", "惨劇、凄惨、事件"), - ("けいひ、せいさん、れしーと", "経費、生産、レシート"), + ("かじょう、せいさん、しゅうりょう", "過剰、生産、終了"), + ("さんげき、せいさん、じけん", "惨劇、凄惨、事件"), + ("けいひ、せいさん、れしーと", "経費、生産、レシート"), - ("しんぷ、せいしょく、きょうかい", "神父、聖職、教会"), - ("こうび、せいしょく、しゅっさん", "交尾、生殖、出産"), + ("しんぷ、せいしょく、きょうかい", "神父、聖職、教会"), + ("こうび、せいしょく、しゅっさん", "交尾、生殖、出産"), - ("やさいをきるほうちょう", "野菜を切る包丁"), - ("きものをきるしゅみ", "着物を着る趣味"), + ("やさいをきるほうちょう", "野菜を切る包丁"), + ("きものをきるしゅみ", "着物を着る趣味"), - ("さーびす、たいかい、てつづき", "サービス、退会、手続き"), - ("はなび、たいかい、ゆかた", "花火、大会、浴衣"), + ("さーびす、たいかい、てつづき", "サービス、退会、手続き"), + ("はなび、たいかい、ゆかた", "花火、大会、浴衣"), - ("しゅみ、はいかい、はいく", "趣味、俳諧、俳句"), - ("ろうじん、はいかい、にんちしょう", "老人、徘徊、認知症"), + ("しゅみ、はいかい、はいく", "趣味、俳諧、俳句"), + ("ろうじん、はいかい、にんちしょう", "老人、徘徊、認知症"), - ("おやににたかお", "親に似た顔"), - ("じっくりにたにく", "じっくり煮た肉"), + ("おやににたかお", "親に似た顔"), + ("じっくりにたにく", "じっくり煮た肉"), - ("ちりょう、なんこう、しゅじゅつ", "治療、難航、手術"), - ("ぬりぐすり、なんこう、ききめ", "塗り薬、軟膏、効き目"), + ("ちりょう、なんこう、しゅじゅつ", "治療、難航、手術"), + ("ぬりぐすり、なんこう、ききめ", "塗り薬、軟膏、効き目"), - ("ようえき、ようかい、ようしつ", "溶液、溶解、溶質"), - ("おばけ、ようかい、ゆうれい", "お化け、妖怪、幽霊"), + ("ようえき、ようかい、ようしつ", "溶液、溶解、溶質"), + ("おばけ、ようかい、ゆうれい", "お化け、妖怪、幽霊"), - ("りょうち、りょうかい、りょうど", "領地、領海、領土"), - ("おーけー、りょうかい、しょうち", "OK、了解、承知"), + ("りょうち、りょうかい、りょうど", "領地、領海、領土"), + ("おーけー、りょうかい、しょうち", "OK、了解、承知"), - ("がっこう、こうしょう、ばっじ", "学校、校章、バッジ"), - ("かいぎ、こうしょう、しっぱい", "会議、交渉、失敗"), + ("がっこう、こうしょう、ばっじ", "学校、校章、バッジ"), + ("かいぎ、こうしょう、しっぱい", "会議、交渉、失敗"), - ("ようちえん、ようじ、よういく", "幼稚園、幼児、養育"), - ("きんきゅう、ようじ、きたく", "緊急、用事、帰宅"), + ("ようちえん、ようじ、よういく", "幼稚園、幼児、養育"), + ("きんきゅう、ようじ、きたく", "緊急、用事、帰宅"), - ("おやぶん、こぶん", "親分、子分"), - ("かんぶん、こぶん", "漢文、古文") - ] + ("おやぶん、こぶん", "親分、子分"), + ("かんぶん、こぶん", "漢文、古文") + ] - var score: Double = 0 - for (input, expect) in cases { - let converter = KanaKanjiConverter() - var c = ComposingText() - c.insertAtCursorPosition(input, inputStyle: .direct) - var options = requestOptions() - options.requireJapanesePrediction = false - let results = converter.requestCandidates(c, options: options) + var score: Double = 0 + for (input, expect) in cases { + let converter = KanaKanjiConverter() + var c = ComposingText() + c.insertAtCursorPosition(input, inputStyle: .direct) + var options = requestOptions() + options.requireJapanesePrediction = false + let results = converter.requestCandidates(c, options: options) - if results.mainResults[0].text == expect { - score += 1 - } else if results.mainResults.count > 1 && results.mainResults[1].text == expect { - score += 0.5 - } else { - print("\(#function) Failure: input \(input), expect \(expect), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + if results.mainResults[0].text == expect { + score += 1 + } else if results.mainResults.count > 1 && results.mainResults[1].text == expect { + score += 0.5 + } else { + print("\(#function) Failure: input \(input), expect \(expect), result: \(results.mainResults.map(\.text).prefix(5).joined(separator: ", "))") + } } + let accuracy = score / Double(cases.count) + print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") + XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < accuracy } - let accuracy = score / Double(cases.count) - print("\(#function) Result: accuracy \(accuracy), score \(score), count \(cases.count)") - XCTAssertGreaterThan(accuracy, 0.7) // 0.7 < accuracy } #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) @@ -684,12 +701,12 @@ import XCTest } let argument = items[4] - let converter = KanaKanjiConverter() + let converter = await KanaKanjiConverter() var c = ComposingText() c.insertAtCursorPosition(input, inputStyle: .direct) var options = requestOptions() options.requireJapanesePrediction = false - let results = converter.requestCandidates(c, options: options).mainResults + let results = await converter.requestCandidates(c, options: options).mainResults cases += 1 let azooKeyStatus = mozcEvaluation(command: command, argument: argument, results: results) if azooKeyStatus {