diff --git a/.swiftformat.json b/.swiftformat similarity index 94% rename from .swiftformat.json rename to .swiftformat index 1160d8b..056a59a 100644 --- a/.swiftformat.json +++ b/.swiftformat @@ -33,8 +33,8 @@ redundantRawValues, \ redundantType, \ redundantVoidReturnType, \ semicolons, \ -sortedImports, \ -sortedSwitchCases, \ +sortImports, \ +sortSwitchCases, \ spaceAroundBraces, \ spaceAroundBrackets, \ spaceAroundComments, \ @@ -101,4 +101,9 @@ strongifiedSelf --exclude Pods,**/UNTESTED_TODO,vendor,fastlane -# https://github.com/NoemiRozpara/Google-SwiftFormat-Config \ No newline at end of file +# https://github.com/NoemiRozpara/Google-SwiftFormat-Config + +# vChewing-specific settings: + + --disable preferForLoop + \ No newline at end of file diff --git a/Sources/Megrez/1_Compositor.swift b/Sources/Megrez/1_Compositor.swift index ffc0bdd..ba186de 100644 --- a/Sources/Megrez/1_Compositor.swift +++ b/Sources/Megrez/1_Compositor.swift @@ -3,7 +3,9 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) -public extension Megrez { +// MARK: - Megrez.Compositor + +extension Megrez { /// 一個組字器用來在給定一系列的索引鍵的情況下(藉由一系列的觀測行為)返回一套資料值。 /// /// 用於輸入法的話,給定的索引鍵可以是注音、且返回的資料值都是漢語字詞組合。該組字器 @@ -13,15 +15,50 @@ public extension Megrez { /// 簡單的貝氏推論:因為底層的語言模組只會提供單元圖資料。一旦將所有可以組字的單元圖 /// 作為節點塞到組字器內,就可以用一個簡單的有向無環圖爬軌過程、來利用這些隱性資料值 /// 算出最大相似估算結果。 - struct Compositor { + public struct Compositor { + // MARK: Lifecycle + + /// 初期化一個組字器。 + /// - Parameter langModel: 要對接的語言模組。 + public init(with langModel: LangModelProtocol, separator: String = "-") { + self.langModel = .init(withLM: langModel) + self.separator = separator + } + + /// 以指定組字器生成拷貝。 + /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 + /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 + /// 這在某些情況下會造成意料之外的混亂情況,所以需要引入一個拷貝用的建構子。 + public init(from target: Compositor) { + self.cursor = target.cursor + self.marker = target.marker + self.separator = target.separator + self.walkedNodes = target.walkedNodes.map(\.copy) + self.keys = target.keys + self.spans = target.spans.map(\.hardCopy) + self.langModel = target.langModel + } + + // MARK: Public + /// 就文字輸入方向而言的方向。 public enum TypingDirection { case front, rear } /// 軌格增減行為。 public enum ResizeBehavior { case expand, shrink } - /// 該軌格內可以允許的最大幅位長度。 - public static var maxSpanLength: Int = 10 { didSet { maxSpanLength = max(6, maxSpanLength) } } + /// 多字讀音鍵當中用以分割漢字讀音的記號的預設值,是「-」。 public static var theSeparator: String = "-" + + /// 該軌格內可以允許的最大幅位長度。 + public static var maxSpanLength: Int = 10 { didSet { maxSpanLength = max(6, maxSpanLength) } } + + /// 最近一次爬軌結果。 + public var walkedNodes: [Node] = [] + /// 該組字器已經插入的的索引鍵,以陣列的形式存放。 + public private(set) var keys = [String]() + /// 該組字器的幅位單元陣列。 + public private(set) var spans = [SpanUnit]() + /// 該組字器的敲字游標位置。 public var cursor: Int = 0 { didSet { @@ -39,8 +76,6 @@ public extension Megrez { } } - /// 最近一次爬軌結果。 - public var walkedNodes: [Node] = [] /// 該組字器的長度,組字器內已經插入的單筆索引鍵的數量,也就是內建漢字讀音的數量(唯讀)。 /// - Remark: 理論上而言,spans.count 也是這個數。 /// 但是,為了防止萬一,就用了目前的方法來計算。 @@ -48,42 +83,41 @@ public extension Megrez { /// 組字器是否為空。 public var isEmpty: Bool { spans.isEmpty && keys.isEmpty } - /// 該組字器已經插入的的索引鍵,以陣列的形式存放。 - public private(set) var keys = [String]() - /// 該組字器的幅位單元陣列。 - public private(set) var spans = [SpanUnit]() /// 該組字器所使用的語言模型(被 LangModelRanked 所封裝)。 public var langModel: LangModelRanked { didSet { clear() } } - /// 初期化一個組字器。 - /// - Parameter langModel: 要對接的語言模組。 - public init(with langModel: LangModelProtocol, separator: String = "-") { - self.langModel = .init(withLM: langModel) - self.separator = separator - } - - /// 以指定組字器生成拷貝。 - /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 - /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 - /// 這在某些情況下會造成意料之外的混亂情況,所以需要引入一個拷貝用的建構子。 - public init(from target: Compositor) { - cursor = target.cursor - marker = target.marker - separator = target.separator - walkedNodes = target.walkedNodes.map(\.copy) - keys = target.keys - spans = target.spans.map(\.hardCopy) - langModel = target.langModel - } - /// 該組字器的硬拷貝。 /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 /// 這在某些情況下會造成意料之外的混亂情況,所以需要引入一個拷貝用的建構子。 public var hardCopy: Compositor { .init(from: self) } + /// 生成用以交給 GraphViz 診斷的資料檔案內容,純文字。 + public var dumpDOT: String { + // C# StringBuilder 與 Swift NSMutableString 能提供爆發性的效能。 + var strOutput = "digraph {\ngraph [ rankdir=LR ];\nBOS;\n" + spans.enumerated().forEach { p, span in + (0 ... span.maxLength).forEach { ni in + guard let np = span[ni] else { return } + if p == 0 { strOutput.append("BOS -> \(np.value);\n") } + strOutput.append("\(np.value);\n") + if (p + ni) < spans.count { + let destinationSpan = spans[p + ni] + (0 ... destinationSpan.maxLength).forEach { q in + guard let dn = destinationSpan[q] else { return } + strOutput.append(np.value + " -> " + dn.value + ";\n") + } + } + guard (p + ni) == spans.count else { return } + strOutput.append(np.value + " -> EOS;\n") + } + } + strOutput.append("EOS;\n}\n") + return strOutput.description + } + /// 重置包括游標在內的各項參數,且清空各種由組字器生成的內部資料。 /// /// 將已經被插入的索引鍵陣列與幅位單元陣列(包括其內的節點)全部清空。 @@ -99,8 +133,10 @@ public extension Megrez { /// 在游標位置插入給定的索引鍵。 /// - Parameter key: 要插入的索引鍵。 /// - Returns: 該操作是否成功執行。 - @discardableResult public mutating func insertKey(_ key: String) -> Bool { - guard !key.isEmpty, key != separator, langModel.hasUnigramsFor(keyArray: [key]) else { return false } + @discardableResult + public mutating func insertKey(_ key: String) -> Bool { + guard !key.isEmpty, key != separator, + langModel.hasUnigramsFor(keyArray: [key]) else { return false } keys.insert(key, at: cursor) let gridBackup = spans resizeGrid(at: cursor, do: .expand) @@ -120,7 +156,8 @@ public extension Megrez { /// 如果是朝著與文字輸入方向相反的方向砍的話,游標位置會自動遞減。 /// - Parameter direction: 指定方向(相對於文字輸入方向而言)。 /// - Returns: 該操作是否成功執行。 - @discardableResult public mutating func dropKey(direction: TypingDirection) -> Bool { + @discardableResult + public mutating func dropKey(direction: TypingDirection) -> Bool { let isBackSpace: Bool = direction == .rear ? true : false guard cursor != (isBackSpace ? 0 : keys.count) else { return false } keys.remove(at: cursor - (isBackSpace ? 1 : 0)) @@ -142,9 +179,12 @@ public extension Megrez { /// // 該特性不適用於小麥注音,除非小麥注音重新設計 InputState 且修改 KeyHandler、 /// 將標記游標交給敝引擎來管理。屆時,NSStringUtils 將徹底卸任。 /// - Returns: 該操作是否順利完成。 - @discardableResult public mutating func jumpCursorBySpan(to direction: TypingDirection, isMarker: Bool = false) - -> Bool - { + @discardableResult + public mutating func jumpCursorBySpan( + to direction: TypingDirection, + isMarker: Bool = false + ) + -> Bool { var target = isMarker ? marker : cursor switch direction { case .front: @@ -155,7 +195,10 @@ public extension Megrez { guard let currentRegion = walkedNodes.cursorRegionMap[target] else { return false } let aRegionForward = max(currentRegion - 1, 0) - let currentRegionBorderRear: Int = walkedNodes[0 ..< currentRegion].map(\.spanLength).reduce(0, +) + let currentRegionBorderRear: Int = walkedNodes[0 ..< currentRegion].map(\.spanLength).reduce( + 0, + + + ) switch target { case currentRegionBorderRear: switch direction { @@ -180,30 +223,6 @@ public extension Megrez { } return true } - - /// 生成用以交給 GraphViz 診斷的資料檔案內容,純文字。 - public var dumpDOT: String { - // C# StringBuilder 與 Swift NSMutableString 能提供爆發性的效能。 - var strOutput = "digraph {\ngraph [ rankdir=LR ];\nBOS;\n" - spans.enumerated().forEach { p, span in - (0 ... span.maxLength).forEach { ni in - guard let np = span[ni] else { return } - if p == 0 { strOutput.append("BOS -> \(np.value);\n") } - strOutput.append("\(np.value);\n") - if (p + ni) < spans.count { - let destinationSpan = spans[p + ni] - (0 ... destinationSpan.maxLength).forEach { q in - guard let dn = destinationSpan[q] else { return } - strOutput.append(np.value + " -> " + dn.value + ";\n") - } - } - guard (p + ni) == spans.count else { return } - strOutput.append(np.value + " -> EOS;\n") - } - } - strOutput.append("EOS;\n}\n") - return strOutput.description - } } } @@ -284,9 +303,13 @@ extension Megrez.Compositor { /// - Parameter updateExisting: 是否根據目前的語言模型的資料狀態來對既有節點更新其內部的單元圖陣列資料。 /// 該特性可以用於「在選字窗內屏蔽了某個詞之後,立刻生效」這樣的軟體功能需求的實現。 /// - Returns: 新增或影響了多少個節點。如果返回「0」則表示可能發生了錯誤。 - @discardableResult public mutating func update(updateExisting: Bool = false) -> Int { + @discardableResult + public mutating func update(updateExisting: Bool = false) -> Int { let maxSpanLength = Megrez.Compositor.maxSpanLength - let rangeOfPositions = max(0, cursor - maxSpanLength) ..< min(cursor + maxSpanLength, keys.count) + let rangeOfPositions = max(0, cursor - maxSpanLength) ..< min( + cursor + maxSpanLength, + keys.count + ) var nodesChanged = 0 rangeOfPositions.forEach { position in let rangeOfLengths = 1 ... min(maxSpanLength, rangeOfPositions.upperBound - position) diff --git a/Sources/Megrez/2_Walker.swift b/Sources/Megrez/2_Walker.swift index a79edac..072322e 100644 --- a/Sources/Megrez/2_Walker.swift +++ b/Sources/Megrez/2_Walker.swift @@ -3,7 +3,7 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) -public extension Megrez.Compositor { +extension Megrez.Compositor { /// 爬軌函式,會更新當前組字器的 walkedNodes。 /// /// 找到軌格陣圖內權重最大的路徑。該路徑代表了可被觀測到的最可能的隱藏事件鏈。 @@ -17,7 +17,8 @@ public extension Megrez.Compositor { /// 郭家寶(ByVoid)的《[基於統計語言模型的拼音輸入法](https://byvoid.com/zht/blog/slm_based_pinyin_ime/) 》; /// 再後來則是 2022 年中時期劉燈的 Gramambular 2 組字引擎。 /// - Returns: 爬軌結果+該過程是否順利執行。 - @discardableResult mutating func walk() -> [Megrez.Node] { + @discardableResult + public mutating func walk() -> [Megrez.Node] { defer { Self.reinitVertexNetwork() } walkedNodes.removeAll() sortAndRelax() diff --git a/Sources/Megrez/3_KeyValuePaired.swift b/Sources/Megrez/3_KeyValuePaired.swift index e1cd68a..e5fcaab 100644 --- a/Sources/Megrez/3_KeyValuePaired.swift +++ b/Sources/Megrez/3_KeyValuePaired.swift @@ -3,25 +3,12 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) -public extension Megrez { +// MARK: - Megrez.KeyValuePaired + +extension Megrez { /// 鍵值配對,乃索引鍵陣列與讀音的配對單元。 - struct KeyValuePaired: Equatable, CustomStringConvertible, Hashable, Comparable { - /// 索引鍵陣列。一般情況下用來放置讀音等可以用來作為索引的內容。 - public let keyArray: [String] - /// 資料值,通常是詞語或單個字。 - public let value: String - /// 權重。 - public let score: Double - /// 將當前鍵值列印成一個字串。 - public var description: String { "(\(keyArray.description),\(value),\(score))" } - /// 判斷當前鍵值配對是否合規。如果鍵與值有任一為空,則結果為 false。 - public var isValid: Bool { !keyArray.joined().isEmpty && !value.isEmpty } - /// 將當前鍵值列印成一個字串,但如果該鍵值配對為空的話則僅列印「()」。 - public var toNGramKey: String { !isValid ? "()" : "(\(joinedKey()),\(value))" } - /// 通用陣列表達形式。 - public var keyValueTuplet: (keyArray: [String], value: String) { (keyArray, value) } - /// 通用陣列表達形式。 - public var triplet: (keyArray: [String], value: String, score: Double) { (keyArray, value, score) } + public struct KeyValuePaired: Equatable, CustomStringConvertible, Hashable, Comparable { + // MARK: Lifecycle /// 初期化一組鍵值配對。 /// - Parameters: @@ -37,19 +24,19 @@ public extension Megrez { /// 初期化一組鍵值配對。 /// - Parameter tripletExpression: 傳入的通用陣列表達形式。 public init(_ tripletExpression: (keyArray: [String], value: String, score: Double)) { - keyArray = tripletExpression.keyArray.isEmpty ? ["N/A"] : tripletExpression.keyArray + self.keyArray = tripletExpression.keyArray.isEmpty ? ["N/A"] : tripletExpression.keyArray let theValue = tripletExpression.value.isEmpty ? "N/A" : tripletExpression.value - value = theValue - score = tripletExpression.score + self.value = theValue + self.score = tripletExpression.score } /// 初期化一組鍵值配對。 /// - Parameter tuplet: 傳入的通用陣列表達形式。 public init(_ tupletExpression: (keyArray: [String], value: String)) { - keyArray = tupletExpression.keyArray.isEmpty ? ["N/A"] : tupletExpression.keyArray + self.keyArray = tupletExpression.keyArray.isEmpty ? ["N/A"] : tupletExpression.keyArray let theValue = tupletExpression.value.isEmpty ? "N/A" : tupletExpression.value - value = theValue - score = 0 + self.value = theValue + self.score = 0 } /// 初期化一組鍵值配對。 @@ -58,27 +45,36 @@ public extension Megrez { /// - value: 資料值。 /// - score: 權重(雙精度小數)。 public init(key: String = "N/A", value: String = "N/A", score: Double = 0) { - keyArray = key.isEmpty ? ["N/A"] : key.sliced(by: Megrez.Compositor.theSeparator) + self.keyArray = key.isEmpty ? ["N/A"] : key.sliced(by: Megrez.Compositor.theSeparator) self.value = value.isEmpty ? "N/A" : value self.score = score } - /// 做為預設雜湊函式。 - /// - Parameter hasher: 目前物件的雜湊碼。 - public func hash(into hasher: inout Hasher) { - hasher.combine(keyArray) - hasher.combine(value) - hasher.combine(score) + // MARK: Public + + /// 索引鍵陣列。一般情況下用來放置讀音等可以用來作為索引的內容。 + public let keyArray: [String] + /// 資料值,通常是詞語或單個字。 + public let value: String + /// 權重。 + public let score: Double + /// 通用陣列表達形式。 + public var keyValueTuplet: (keyArray: [String], value: String) { (keyArray, value) } + /// 通用陣列表達形式。 + public var triplet: (keyArray: [String], value: String, score: Double) { + (keyArray, value, score) } + /// 將當前鍵值列印成一個字串。 + public var description: String { "(\(keyArray.description),\(value),\(score))" } + /// 判斷當前鍵值配對是否合規。如果鍵與值有任一為空,則結果為 false。 + public var isValid: Bool { !keyArray.joined().isEmpty && !value.isEmpty } + /// 將當前鍵值列印成一個字串,但如果該鍵值配對為空的話則僅列印「()」。 + public var toNGramKey: String { !isValid ? "()" : "(\(joinedKey()),\(value))" } public var hardCopy: KeyValuePaired { .init(keyArray: keyArray, value: value, score: score) } - public func joinedKey(by separator: String = Megrez.Compositor.theSeparator) -> String { - keyArray.joined(separator: separator) - } - public static func == (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool { lhs.score == rhs.score && lhs.keyArray == rhs.keyArray && lhs.value == rhs.value } @@ -102,24 +98,37 @@ public extension Megrez { (lhs.keyArray.count >= rhs.keyArray.count) || (lhs.keyArray.count == rhs.keyArray.count && lhs.value >= rhs.value) } + + /// 做為預設雜湊函式。 + /// - Parameter hasher: 目前物件的雜湊碼。 + public func hash(into hasher: inout Hasher) { + hasher.combine(keyArray) + hasher.combine(value) + hasher.combine(score) + } + + public func joinedKey(by separator: String = Megrez.Compositor.theSeparator) -> String { + keyArray.joined(separator: separator) + } } } -public extension Megrez.Compositor { +extension Megrez.Compositor { /// 規定候選字陣列內容的獲取範圍類型: /// - all: 不只包含其它兩類結果,還允許游標穿插候選字。 /// - beginAt: 僅獲取從當前游標位置開始的節點內的候選字。 /// - endAt 僅獲取在當前游標位置結束的節點內的候選字。 - enum CandidateFetchFilter { case all, beginAt, endAt } + public enum CandidateFetchFilter { case all, beginAt, endAt } /// 返回在當前位置的所有候選字詞(以詞音配對的形式)。如果組字器內有幅位、且游標 /// 位於組字器的(文字輸入順序的)最前方(也就是游標位置的數值是最大合規數值)的 /// 話,那麼這裡會對 location 的位置自動減去 1、以免去在呼叫該函式後再處理的麻煩。 /// - Parameter location: 游標位置,必須是顯示的游標位置、不得做任何事先糾偏處理。 /// - Returns: 候選字音配對陣列。 - func fetchCandidates( + public func fetchCandidates( at givenLocation: Int? = nil, filter givenFilter: CandidateFetchFilter = .all - ) -> [Megrez.KeyValuePaired] { + ) + -> [Megrez.KeyValuePaired] { var result = [Megrez.KeyValuePaired]() guard !keys.isEmpty else { return result } var location = max(min(givenLocation ?? cursor, keys.count), 0) @@ -161,12 +170,18 @@ public extension Megrez.Compositor { /// - location: 游標位置。 /// - overrideType: 指定覆寫行為。 /// - Returns: 該操作是否成功執行。 - @discardableResult func overrideCandidate( - _ candidate: Megrez.KeyValuePaired, at location: Int, overrideType: Megrez.Node.OverrideType = .withHighScore + @discardableResult + public func overrideCandidate( + _ candidate: Megrez.KeyValuePaired, at location: Int, + overrideType: Megrez.Node.OverrideType = .withHighScore ) - -> Bool - { - overrideCandidateAgainst(keyArray: candidate.keyArray, at: location, value: candidate.value, type: overrideType) + -> Bool { + overrideCandidateAgainst( + keyArray: candidate.keyArray, + at: location, + value: candidate.value, + type: overrideType + ) } /// 使用給定的候選字詞字串,將給定位置的節點的候選字詞改為與之一致的候選字詞。 @@ -177,10 +192,12 @@ public extension Megrez.Compositor { /// - location: 游標位置。 /// - overrideType: 指定覆寫行為。 /// - Returns: 該操作是否成功執行。 - @discardableResult func overrideCandidateLiteral( + @discardableResult + public func overrideCandidateLiteral( _ candidate: String, at location: Int, overrideType: Megrez.Node.OverrideType = .withHighScore - ) -> Bool { + ) + -> Bool { overrideCandidateAgainst(keyArray: nil, at: location, value: candidate, type: overrideType) } @@ -193,11 +210,18 @@ public extension Megrez.Compositor { /// - value: 資料值。 /// - type: 指定覆寫行為。 /// - Returns: 該操作是否成功執行。 - internal func overrideCandidateAgainst(keyArray: [String]?, at location: Int, value: String, type: Megrez.Node.OverrideType) - -> Bool - { + internal func overrideCandidateAgainst( + keyArray: [String]?, + at location: Int, + value: String, + type: Megrez.Node.OverrideType + ) + -> Bool { let location = max(min(location, keys.count), 0) // 防呆 - var arrOverlappedNodes: [(location: Int, node: Megrez.Node)] = fetchOverlappingNodes(at: min(keys.count - 1, location)) + var arrOverlappedNodes: [(location: Int, node: Megrez.Node)] = fetchOverlappingNodes(at: min( + keys.count - 1, + location + )) var overridden: (location: Int, node: Megrez.Node)? for anchor in arrOverlappedNodes { if keyArray != nil, anchor.node.keyArray != keyArray { continue } @@ -208,22 +232,24 @@ public extension Megrez.Compositor { guard let overridden = overridden else { return false } // 啥也不覆寫。 - (overridden.location ..< min(spans.count, overridden.location + overridden.node.spanLength)).forEach { i in - /// 咱們還得弱化所有在相同的幅位座標的節點的複寫權重。舉例說之前爬軌的結果是「A BC」 - /// 且 A 與 BC 都是被覆寫的結果,然後使用者現在在與 A 相同的幅位座標位置 - /// 選了「DEF」,那麼 BC 的覆寫狀態就有必要重設(但 A 不用重設)。 - arrOverlappedNodes = fetchOverlappingNodes(at: i) - arrOverlappedNodes.forEach { anchor in - if anchor.node == overridden.node { return } - let anchorNodeKeyJoined = anchor.node.joinedKey(by: "\t") - let overriddenNodeKeyJoined = overridden.node.joinedKey(by: "\t") - if !overriddenNodeKeyJoined.has(string: anchorNodeKeyJoined) || !overridden.node.value.has(string: anchor.node.value) { - anchor.node.reset() - return + (overridden.location ..< min(spans.count, overridden.location + overridden.node.spanLength)) + .forEach { i in + /// 咱們還得弱化所有在相同的幅位座標的節點的複寫權重。舉例說之前爬軌的結果是「A BC」 + /// 且 A 與 BC 都是被覆寫的結果,然後使用者現在在與 A 相同的幅位座標位置 + /// 選了「DEF」,那麼 BC 的覆寫狀態就有必要重設(但 A 不用重設)。 + arrOverlappedNodes = fetchOverlappingNodes(at: i) + arrOverlappedNodes.forEach { anchor in + if anchor.node == overridden.node { return } + let anchorNodeKeyJoined = anchor.node.joinedKey(by: "\t") + let overriddenNodeKeyJoined = overridden.node.joinedKey(by: "\t") + if !overriddenNodeKeyJoined.has(string: anchorNodeKeyJoined) || !overridden.node.value + .has(string: anchor.node.value) { + anchor.node.reset() + return + } + anchor.node.overridingScore /= 4 } - anchor.node.overridingScore /= 4 } - } return true } } diff --git a/Sources/Megrez/4_SpanUnit.swift b/Sources/Megrez/4_SpanUnit.swift index 757780d..1f63627 100644 --- a/Sources/Megrez/4_SpanUnit.swift +++ b/Sources/Megrez/4_SpanUnit.swift @@ -3,17 +3,19 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) -public extension Megrez { +// MARK: - Megrez.SpanUnit + +extension Megrez { /// 幅位乃指一組共享起點的節點。其實是個辭典:[幅位長度: 節點]。 - typealias SpanUnit = [Int: Node] + public typealias SpanUnit = [Int: Node] } -public extension Megrez.SpanUnit { +extension Megrez.SpanUnit { /// 幅位乃指一組共享起點的節點。其實是個辭典:[幅位長度: 節點]。 /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 /// 這在某些情況下會造成意料之外的混亂情況,所以需要引入一個拷貝用的建構子。 - init(SpanUnit target: Megrez.SpanUnit) { + public init(SpanUnit target: Megrez.SpanUnit) { self.init() target.forEach { theKey, theValue in self[theKey] = theValue.copy @@ -21,13 +23,13 @@ public extension Megrez.SpanUnit { } /// 該幅位的硬拷貝。 - var hardCopy: Megrez.SpanUnit { .init(SpanUnit: self) } + public var hardCopy: Megrez.SpanUnit { .init(SpanUnit: self) } // MARK: - Dynamic Variables /// 該幅位單元內的所有節點當中持有最長幅位的節點長度。 /// 該變數受該幅位的自身操作函式而被動更新。 - var maxLength: Int { keys.max() ?? 0 } + public var maxLength: Int { keys.max() ?? 0 } /// (該變數為捷徑,代傳 Megrez.Compositor.maxSpanLength。) private var maxSpanLength: Int { Megrez.Compositor.maxSpanLength } @@ -40,7 +42,8 @@ public extension Megrez.SpanUnit { /// - Remark: 這個函式用來防呆。一般情況下用不到。 /// - Parameter node: 要塞入的節點。 /// - Returns: 該操作是否成功執行。 - @discardableResult mutating func addNode(node: Megrez.Node) -> Bool { + @discardableResult + public mutating func addNode(node: Megrez.Node) -> Bool { guard allowedLengths.contains(node.spanLength) else { return false } self[node.spanLength] = node return true @@ -50,7 +53,8 @@ public extension Megrez.SpanUnit { /// - Remark: 這個函式用來防呆。一般情況下用不到。 /// - Parameter length: 給定的幅位長度。 /// - Returns: 該操作是否成功執行。 - @discardableResult mutating func dropNodesOfOrBeyond(length: Int) -> Bool { + @discardableResult + public mutating func dropNodesOfOrBeyond(length: Int) -> Bool { guard allowedLengths.contains(length) else { return false } let length = Swift.min(length, maxSpanLength) (length ... maxSpanLength).forEach { self[$0] = nil } diff --git a/Sources/Megrez/5_Node.swift b/Sources/Megrez/5_Node.swift index 6f008a3..05fba9d 100644 --- a/Sources/Megrez/5_Node.swift +++ b/Sources/Megrez/5_Node.swift @@ -3,7 +3,9 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) -public extension Megrez { +// MARK: - Megrez.Node + +extension Megrez { /// 字詞節點。 /// /// 一個節點由這些內容組成:幅位長度、索引鍵、以及一組單元圖。幅位長度就是指這個 @@ -11,7 +13,42 @@ public extension Megrez { /// 的詞,組字器會將多個讀音索引鍵合併為一個讀音索引鍵、據此向語言模組請求對應的 /// 單元圖結果陣列。舉例說,如果一個詞有兩個漢字組成的話,那麼讀音也是有兩個、其 /// 索引鍵也是由兩個讀音組成的,那麼這個節點的幅位長度就是 2。 - class Node: Equatable, Hashable { + public class Node: Equatable, Hashable { + // MARK: Lifecycle + + /// 生成一個字詞節點。 + /// + /// 一個節點由這些內容組成:幅位長度、索引鍵、以及一組單元圖。幅位長度就是指這個 + /// 節點在組字器內橫跨了多少個字長。組字器負責構築自身的節點。對於由多個漢字組成 + /// 的詞,組字器會將多個讀音索引鍵合併為一個讀音索引鍵、據此向語言模組請求對應的 + /// 單元圖結果陣列。舉例說,如果一個詞有兩個漢字組成的話,那麼讀音也是有兩個、其 + /// 索引鍵也是由兩個讀音組成的,那麼這個節點的幅位長度就是 2。 + /// - Parameters: + /// - keyArray: 給定索引鍵陣列,不得為空。 + /// - spanLength: 給定幅位長度,一般情況下與給定索引鍵陣列內的索引鍵數量一致。 + /// - unigrams: 給定單元圖陣列,不得為空。 + public init(keyArray: [String] = [], spanLength: Int = 0, unigrams: [Megrez.Unigram] = []) { + self.keyArray = keyArray + self.spanLength = max(spanLength, 0) + self.unigrams = unigrams + self.currentOverrideType = .withNoOverrides + } + + /// 以指定字詞節點生成拷貝。 + /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 + /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 + /// 這在某些情況下會造成意料之外的混亂情況,所以需要引入一個拷貝用的建構子。 + public init(node: Node) { + self.overridingScore = node.overridingScore + self.keyArray = node.keyArray + self.spanLength = node.spanLength + self.unigrams = node.unigrams + self.currentOverrideType = node.currentOverrideType + self.currentUnigramIndex = node.currentUnigramIndex + } + + // MARK: Public + /// 三種不同的針對一個節點的覆寫行為。 /// - withNoOverrides: 無覆寫行為。 /// - withTopUnigramScore: 使用指定的單元圖資料值來覆寫該節點,但卻使用 @@ -44,6 +81,7 @@ public extension Megrez { public private(set) var unigrams: [Megrez.Unigram] /// 該節點目前的覆寫狀態種類。 public private(set) var currentOverrideType: Node.OverrideType + /// 當前該節點所指向的(單元圖陣列內的)單元圖索引位置。 public private(set) var currentUnigramIndex: Int = 0 { didSet { currentUnigramIndex = max(min(unigrams.count - 1, currentUnigramIndex), 0) } @@ -52,54 +90,6 @@ public extension Megrez { /// 該節點當前狀態所展示的鍵值配對。 public var currentPair: Megrez.KeyValuePaired { .init(keyArray: keyArray, value: value) } - /// 做為預設雜湊函式。 - /// - Parameter hasher: 目前物件的雜湊碼。 - public func hash(into hasher: inout Hasher) { - hasher.combine(overridingScore) - hasher.combine(keyArray) - hasher.combine(spanLength) - hasher.combine(unigrams) - hasher.combine(currentOverrideType) - hasher.combine(currentUnigramIndex) - } - - public static func == (lhs: Node, rhs: Node) -> Bool { - lhs.overridingScore == rhs.overridingScore && lhs.spanLength == rhs.spanLength - && lhs.keyArray == rhs.keyArray && lhs.currentUnigramIndex == rhs.currentUnigramIndex - && lhs.unigrams == rhs.unigrams && lhs.currentOverrideType == rhs.currentOverrideType - } - - /// 生成一個字詞節點。 - /// - /// 一個節點由這些內容組成:幅位長度、索引鍵、以及一組單元圖。幅位長度就是指這個 - /// 節點在組字器內橫跨了多少個字長。組字器負責構築自身的節點。對於由多個漢字組成 - /// 的詞,組字器會將多個讀音索引鍵合併為一個讀音索引鍵、據此向語言模組請求對應的 - /// 單元圖結果陣列。舉例說,如果一個詞有兩個漢字組成的話,那麼讀音也是有兩個、其 - /// 索引鍵也是由兩個讀音組成的,那麼這個節點的幅位長度就是 2。 - /// - Parameters: - /// - keyArray: 給定索引鍵陣列,不得為空。 - /// - spanLength: 給定幅位長度,一般情況下與給定索引鍵陣列內的索引鍵數量一致。 - /// - unigrams: 給定單元圖陣列,不得為空。 - public init(keyArray: [String] = [], spanLength: Int = 0, unigrams: [Megrez.Unigram] = []) { - self.keyArray = keyArray - self.spanLength = max(spanLength, 0) - self.unigrams = unigrams - currentOverrideType = .withNoOverrides - } - - /// 以指定字詞節點生成拷貝。 - /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 - /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 - /// 這在某些情況下會造成意料之外的混亂情況,所以需要引入一個拷貝用的建構子。 - public init(node: Node) { - overridingScore = node.overridingScore - keyArray = node.keyArray - spanLength = node.spanLength - unigrams = node.unigrams - currentOverrideType = node.currentOverrideType - currentUnigramIndex = node.currentUnigramIndex - } - /// 生成自身的拷貝。 /// - Remark: 因為 Node 不是 Struct,所以會在 Compositor 被拷貝的時候無法被真實複製。 /// 這樣一來,Compositor 複製品當中的 Node 的變化會被反應到原先的 Compositor 身上。 @@ -129,6 +119,23 @@ public extension Megrez { } } + public static func == (lhs: Node, rhs: Node) -> Bool { + lhs.overridingScore == rhs.overridingScore && lhs.spanLength == rhs.spanLength + && lhs.keyArray == rhs.keyArray && lhs.currentUnigramIndex == rhs.currentUnigramIndex + && lhs.unigrams == rhs.unigrams && lhs.currentOverrideType == rhs.currentOverrideType + } + + /// 做為預設雜湊函式。 + /// - Parameter hasher: 目前物件的雜湊碼。 + public func hash(into hasher: inout Hasher) { + hasher.combine(overridingScore) + hasher.combine(keyArray) + hasher.combine(spanLength) + hasher.combine(unigrams) + hasher.combine(currentOverrideType) + hasher.combine(currentUnigramIndex) + } + /// 重設該節點的覆寫狀態、及其內部的單元圖索引位置指向。 public func reset() { currentUnigramIndex = 0 @@ -172,6 +179,8 @@ public extension Megrez { return false } + // MARK: Internal + // MARK: - Vertex Extensions. // 注意:這一段的任何參數都不參與 Hash。 @@ -209,17 +218,17 @@ public extension Megrez { // MARK: - Array Extensions. -public extension Array where Element == Megrez.Node { +extension Array where Element == Megrez.Node { /// 從一個節點陣列當中取出目前的選字字串陣列。 - var values: [String] { map(\.value) } + public var values: [String] { map(\.value) } /// 從一個節點陣列當中取出目前的索引鍵陣列。 - func joinedKeys(by separator: String = Megrez.Compositor.theSeparator) -> [String] { + public func joinedKeys(by separator: String = Megrez.Compositor.theSeparator) -> [String] { map { $0.keyArray.lazy.joined(separator: separator) } } /// 從一個節點陣列當中取出目前的索引鍵陣列。 - var keyArrays: [[String]] { map(\.keyArray) } + public var keyArrays: [[String]] { map(\.keyArray) } /// 返回一連串的節點起點。結果為 (Result A, Result B) 辭典陣列。 /// Result A 以索引查座標,Result B 以座標查索引。 @@ -241,14 +250,14 @@ public extension Array where Element == Megrez.Node { } /// 返回一個辭典,以座標查索引。允許以游標位置查詢其屬於第幾個幅位座標(從 0 開始算)。 - var cursorRegionMap: [Int: Int] { nodeBorderPointDictPair.cursorRegionMap } + public var cursorRegionMap: [Int: Int] { nodeBorderPointDictPair.cursorRegionMap } /// 總讀音單元數量。在絕大多數情況下,可視為總幅位長度。 - var totalKeyCount: Int { map(\.keyArray.count).reduce(0, +) } + public var totalKeyCount: Int { map(\.keyArray.count).reduce(0, +) } /// 根據給定的游標,返回其前後最近的邊界點。 /// - Parameter cursor: 給定的游標。 - func contextRange(ofGivenCursor cursor: Int) -> Range { + public func contextRange(ofGivenCursor cursor: Int) -> Range { guard !isEmpty else { return 0 ..< 0 } let lastSpanningLength = reversed()[0].keyArray.count var nilReturn = (totalKeyCount - lastSpanningLength) ..< totalKeyCount @@ -257,8 +266,10 @@ public extension Array where Element == Megrez.Node { nilReturn = cursor ..< cursor // 下文按道理來講不應該會出現 nilReturn。 guard let rearNodeID = nodeBorderPointDictPair.cursorRegionMap[cursor] else { return nilReturn } - guard let rearIndex = nodeBorderPointDictPair.regionCursorMap[rearNodeID] else { return nilReturn } - guard let frontIndex = nodeBorderPointDictPair.regionCursorMap[rearNodeID + 1] else { return nilReturn } + guard let rearIndex = nodeBorderPointDictPair.regionCursorMap[rearNodeID] + else { return nilReturn } + guard let frontIndex = nodeBorderPointDictPair.regionCursorMap[rearNodeID + 1] + else { return nilReturn } return rearIndex ..< frontIndex } @@ -267,7 +278,7 @@ public extension Array where Element == Megrez.Node { /// - cursor: 給定游標位置。 /// - outCursorPastNode: 找出的節點的前端位置。 /// - Returns: 查找結果。 - func findNode(at cursor: Int, target outCursorPastNode: inout Int) -> Megrez.Node? { + public func findNode(at cursor: Int, target outCursorPastNode: inout Int) -> Megrez.Node? { guard !isEmpty else { return nil } let cursor = Swift.max(0, Swift.min(cursor, totalKeyCount - 1)) // 防呆 let range = contextRange(ofGivenCursor: cursor) @@ -279,13 +290,13 @@ public extension Array where Element == Megrez.Node { /// 在陣列內以給定游標位置找出對應的節點。 /// - Parameter cursor: 給定游標位置。 /// - Returns: 查找結果。 - func findNode(at cursor: Int) -> Megrez.Node? { + public func findNode(at cursor: Int) -> Megrez.Node? { var useless = 0 return findNode(at: cursor, target: &useless) } /// 提供一組逐字的字音配對陣列(不使用 Megrez 的 KeyValuePaired 類型),但字音不匹配的節點除外。 - var smashedPairs: [(key: String, value: String)] { + public var smashedPairs: [(key: String, value: String)] { var arrData = [(key: String, value: String)]() forEach { node in if node.isReadingMismatched, !node.keyArray.joined().isEmpty { diff --git a/Sources/Megrez/6_LangModel.swift b/Sources/Megrez/6_LangModel.swift index 42c78ba..3fcb2ee 100644 --- a/Sources/Megrez/6_LangModel.swift +++ b/Sources/Megrez/6_LangModel.swift @@ -3,6 +3,8 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) +// MARK: - LangModelProtocol + /// 語言模組協定。 public protocol LangModelProtocol { /// 給定索引鍵陣列,讓語言模型找給一組單元圖陣列。 @@ -11,16 +13,21 @@ public protocol LangModelProtocol { func hasUnigramsFor(keyArray: [String]) -> Bool } -public extension Megrez.Compositor { +// MARK: - Megrez.Compositor.LangModelRanked + +extension Megrez.Compositor { /// 一個套殼語言模型,用來始終返回經過排序的單元圖。 - class LangModelRanked: LangModelProtocol { - private let langModel: LangModelProtocol + public class LangModelRanked: LangModelProtocol { + // MARK: Lifecycle + /// 一個套殼語言模型,用來始終返回經過排序的單元圖。 /// - Parameter withLM: 用來對接的語言模型。 public init(withLM: LangModelProtocol) { - langModel = withLM + self.langModel = withLM } + // MARK: Public + /// 給定索引鍵,讓語言模型找給一組經過穩定排序的單元圖陣列。 /// - Parameter key: 給定的索引鍵字串。 /// - Returns: 對應的經過穩定排序的單元圖陣列。 @@ -34,6 +41,10 @@ public extension Megrez.Compositor { public func hasUnigramsFor(keyArray: [String]) -> Bool { langModel.hasUnigramsFor(keyArray: keyArray) } + + // MARK: Private + + private let langModel: LangModelProtocol } } @@ -41,16 +52,15 @@ public extension Megrez.Compositor { // Reference: https://stackoverflow.com/a/50545761/4162914 -private extension Sequence { +extension Sequence { /// Return a stable-sorted collection. /// /// - Parameter areInIncreasingOrder: Return nil when two element are equal. /// - Returns: The sorted collection. - func stableSorted( + fileprivate func stableSorted( by areInIncreasingOrder: (Element, Element) throws -> Bool ) - rethrows -> [Element] - { + rethrows -> [Element] { try enumerated() .sorted { a, b -> Bool in try areInIncreasingOrder(a.element, b.element) diff --git a/Sources/Megrez/7_Unigram.swift b/Sources/Megrez/7_Unigram.swift index 29306e4..f31085d 100644 --- a/Sources/Megrez/7_Unigram.swift +++ b/Sources/Megrez/7_Unigram.swift @@ -3,17 +3,12 @@ // ==================== // This code is released under the MIT license (SPDX-License-Identifier: MIT) -public extension Megrez { +// MARK: - Megrez.Unigram + +extension Megrez { /// 單元圖。 - struct Unigram: Equatable, CustomStringConvertible, Hashable { - /// 資料值,通常是詞語或單個字。 - public var value: String - /// 權重。 - public var score: Double - /// 將當前單元圖列印成一個字串。 - public var description: String { - "(" + value.description + "," + String(score) + ")" - } + public struct Unigram: Equatable, CustomStringConvertible, Hashable { + // MARK: Lifecycle /// 初期化一筆「單元圖」。一筆單元圖由一筆資料值與一筆權重數值組成。 /// - Parameters: @@ -24,11 +19,16 @@ public extension Megrez { self.score = score } - /// 做為預設雜湊函式。 - /// - Parameter hasher: 目前物件的雜湊碼。 - public func hash(into hasher: inout Hasher) { - hasher.combine(value) - hasher.combine(score) + // MARK: Public + + /// 資料值,通常是詞語或單個字。 + public var value: String + /// 權重。 + public var score: Double + + /// 將當前單元圖列印成一個字串。 + public var description: String { + "(" + value.description + "," + String(score) + ")" } public static func == (lhs: Unigram, rhs: Unigram) -> Bool { @@ -38,14 +38,21 @@ public extension Megrez { public static func < (lhs: Unigram, rhs: Unigram) -> Bool { lhs.value < rhs.value || (lhs.value == rhs.value && lhs.score < rhs.score) } + + /// 做為預設雜湊函式。 + /// - Parameter hasher: 目前物件的雜湊碼。 + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + hasher.combine(score) + } } } // MARK: - Array Extensions. -public extension Array where Element == Megrez.Unigram { +extension Array where Element == Megrez.Unigram { /// 給定過濾清單,讓單元圖陣列自我過濾。 - mutating func consolidate(filter theFilter: Set = .init()) { + public mutating func consolidate(filter theFilter: Set = .init()) { var inserted: [String: Double] = [:] var insertedArray: [Megrez.Unigram] = [] filter { !theFilter.contains($0.value) }.forEach { neta in diff --git a/Tests/MegrezTests/LMDataForTests.swift b/Tests/MegrezTests/LMDataForTests.swift index bfe858c..7a3600c 100644 --- a/Tests/MegrezTests/LMDataForTests.swift +++ b/Tests/MegrezTests/LMDataForTests.swift @@ -5,10 +5,11 @@ import Megrez -// MARK: - 用以測試的語言模型(簡單範本型) +// MARK: - SimpleLM class SimpleLM: LangModelProtocol { - var mutDatabase: [String: [Megrez.Unigram]] = [:] + // MARK: Lifecycle + init(input: String, swapKeyValue: Bool = false) { let sstream = input.components(separatedBy: "\n") sstream.forEach { line in @@ -24,6 +25,10 @@ class SimpleLM: LangModelProtocol { } } + // MARK: Internal + + var mutDatabase: [String: [Megrez.Unigram]] = [:] + func unigramsFor(keyArray: [String]) -> [Megrez.Unigram] { if let f = mutDatabase[keyArray.joined()] { return f @@ -47,6 +52,8 @@ class SimpleLM: LangModelProtocol { } } +// MARK: - MockLM + class MockLM: LangModelProtocol { func unigramsFor(keyArray: [String]) -> [Megrez.Unigram] { [Megrez.Unigram(value: keyArray.joined(), score: -1)] diff --git a/Tests/MegrezTests/MegrezImplForTests.swift b/Tests/MegrezTests/MegrezImplForTests.swift index 4cba0c8..b7625b4 100644 --- a/Tests/MegrezTests/MegrezImplForTests.swift +++ b/Tests/MegrezTests/MegrezImplForTests.swift @@ -7,7 +7,7 @@ import Megrez // MARK: - Megrez Extensions for Test Purposes Only. -public extension Megrez.Compositor { +extension Megrez.Compositor { /// 返回在當前位置的所有候選字詞(以詞音配對的形式)。如果組字器內有幅位、且游標 /// 位於組字器的(文字輸入順序的)最前方(也就是游標位置的數值是最大合規數值)的 /// 話,那麼這裡會用到 location - 1、以免去在呼叫該函式後再處理的麻煩。 @@ -15,7 +15,11 @@ public extension Megrez.Compositor { /// 現僅用於單元測試、以確認其繼任者是否有給出所有該給出的正常結果。 /// - Parameter location: 游標位置。 /// - Returns: 候選字音配對陣列。 - func fetchCandidatesDeprecated(at location: Int, filter: CandidateFetchFilter = .all) -> [Megrez.KeyValuePaired] { + public func fetchCandidatesDeprecated( + at location: Int, + filter: CandidateFetchFilter = .all + ) + -> [Megrez.KeyValuePaired] { var result = [Megrez.KeyValuePaired]() guard !keys.isEmpty else { return result } let location = max(min(location, keys.count - 1), 0) // 防呆 diff --git a/Tests/MegrezTests/MegrezTests.swift b/Tests/MegrezTests/MegrezTests.swift index bcd351f..3dbe31b 100644 --- a/Tests/MegrezTests/MegrezTests.swift +++ b/Tests/MegrezTests/MegrezTests.swift @@ -16,7 +16,8 @@ final class MegrezTests: XCTestCase { keyArray: ["gao1"], spanLength: 1, unigrams: langModel.unigramsFor(keyArray: ["gao1"]) ) let n3 = Megrez.Node( - keyArray: ["gao1ke1ji4"], spanLength: 3, unigrams: langModel.unigramsFor(keyArray: ["gao1ke1ji4"]) + keyArray: ["gao1ke1ji4"], spanLength: 3, + unigrams: langModel.unigramsFor(keyArray: ["gao1ke1ji4"]) ) XCTAssertEqual(span.maxLength, 0) span.addNode(node: n1) @@ -55,7 +56,11 @@ final class MegrezTests: XCTestCase { func hasUnigramsFor(keyArray: [String]) -> Bool { keyArray.joined() == "foo" } func unigramsFor(keyArray: [String]) -> [Megrez.Unigram] { keyArray.joined() == "foo" - ? [.init(value: "middle", score: -5), .init(value: "highest", score: -2), .init(value: "lowest", score: -10)] + ? [ + .init(value: "middle", score: -5), + .init(value: "highest", score: -2), + .init(value: "lowest", score: -10) + ] : .init() } } @@ -151,7 +156,10 @@ final class MegrezTests: XCTestCase { XCTAssertEqual(compositor.spans[0].maxLength, 3) XCTAssertEqual(compositor.spans[0][1]?.keyArray.joined(separator: compositor.separator), "a") XCTAssertEqual(compositor.spans[0][2]?.keyArray.joined(separator: compositor.separator), "a;b") - XCTAssertEqual(compositor.spans[0][3]?.keyArray.joined(separator: compositor.separator), "a;b;c") + XCTAssertEqual( + compositor.spans[0][3]?.keyArray.joined(separator: compositor.separator), + "a;b;c" + ) XCTAssertEqual(compositor.spans[1].maxLength, 2) XCTAssertEqual(compositor.spans[1][1]?.keyArray.joined(separator: compositor.separator), "b") XCTAssertEqual(compositor.spans[1][2]?.keyArray.joined(separator: compositor.separator), "b;c") @@ -247,12 +255,21 @@ final class MegrezTests: XCTestCase { XCTAssertEqual(compositor.spans[0].maxLength, 4) XCTAssertEqual(compositor.spans[0][1]?.keyArray.joined(separator: compositor.separator), "a") XCTAssertEqual(compositor.spans[0][2]?.keyArray.joined(separator: compositor.separator), "a;X") - XCTAssertEqual(compositor.spans[0][3]?.keyArray.joined(separator: compositor.separator), "a;X;b") - XCTAssertEqual(compositor.spans[0][4]?.keyArray.joined(separator: compositor.separator), "a;X;b;c") + XCTAssertEqual( + compositor.spans[0][3]?.keyArray.joined(separator: compositor.separator), + "a;X;b" + ) + XCTAssertEqual( + compositor.spans[0][4]?.keyArray.joined(separator: compositor.separator), + "a;X;b;c" + ) XCTAssertEqual(compositor.spans[1].maxLength, 3) XCTAssertEqual(compositor.spans[1][1]?.keyArray.joined(separator: compositor.separator), "X") XCTAssertEqual(compositor.spans[1][2]?.keyArray.joined(separator: compositor.separator), "X;b") - XCTAssertEqual(compositor.spans[1][3]?.keyArray.joined(separator: compositor.separator), "X;b;c") + XCTAssertEqual( + compositor.spans[1][3]?.keyArray.joined(separator: compositor.separator), + "X;b;c" + ) XCTAssertEqual(compositor.spans[2].maxLength, 2) XCTAssertEqual(compositor.spans[2][1]?.keyArray.joined(separator: compositor.separator), "b") XCTAssertEqual(compositor.spans[2][2]?.keyArray.joined(separator: compositor.separator), "b;c") @@ -282,17 +299,50 @@ final class MegrezTests: XCTestCase { XCTAssertEqual(compositor.cursor, 6) XCTAssertEqual(compositor.length, 13) XCTAssertEqual(compositor.spans.count, 13) - XCTAssertEqual(compositor.spans[0][6]?.keyArray.joined(separator: compositor.separator), "abcdef") - XCTAssertEqual(compositor.spans[1][6]?.keyArray.joined(separator: compositor.separator), "bcdefh") - XCTAssertEqual(compositor.spans[1][5]?.keyArray.joined(separator: compositor.separator), "bcdef") - XCTAssertEqual(compositor.spans[2][6]?.keyArray.joined(separator: compositor.separator), "cdefhi") - XCTAssertEqual(compositor.spans[2][5]?.keyArray.joined(separator: compositor.separator), "cdefh") - XCTAssertEqual(compositor.spans[3][6]?.keyArray.joined(separator: compositor.separator), "defhij") - XCTAssertEqual(compositor.spans[4][6]?.keyArray.joined(separator: compositor.separator), "efhijk") - XCTAssertEqual(compositor.spans[5][6]?.keyArray.joined(separator: compositor.separator), "fhijkl") - XCTAssertEqual(compositor.spans[6][6]?.keyArray.joined(separator: compositor.separator), "hijklm") - XCTAssertEqual(compositor.spans[7][6]?.keyArray.joined(separator: compositor.separator), "ijklmn") - XCTAssertEqual(compositor.spans[8][5]?.keyArray.joined(separator: compositor.separator), "jklmn") + XCTAssertEqual( + compositor.spans[0][6]?.keyArray.joined(separator: compositor.separator), + "abcdef" + ) + XCTAssertEqual( + compositor.spans[1][6]?.keyArray.joined(separator: compositor.separator), + "bcdefh" + ) + XCTAssertEqual( + compositor.spans[1][5]?.keyArray.joined(separator: compositor.separator), + "bcdef" + ) + XCTAssertEqual( + compositor.spans[2][6]?.keyArray.joined(separator: compositor.separator), + "cdefhi" + ) + XCTAssertEqual( + compositor.spans[2][5]?.keyArray.joined(separator: compositor.separator), + "cdefh" + ) + XCTAssertEqual( + compositor.spans[3][6]?.keyArray.joined(separator: compositor.separator), + "defhij" + ) + XCTAssertEqual( + compositor.spans[4][6]?.keyArray.joined(separator: compositor.separator), + "efhijk" + ) + XCTAssertEqual( + compositor.spans[5][6]?.keyArray.joined(separator: compositor.separator), + "fhijkl" + ) + XCTAssertEqual( + compositor.spans[6][6]?.keyArray.joined(separator: compositor.separator), + "hijklm" + ) + XCTAssertEqual( + compositor.spans[7][6]?.keyArray.joined(separator: compositor.separator), + "ijklmn" + ) + XCTAssertEqual( + compositor.spans[8][5]?.keyArray.joined(separator: compositor.separator), + "jklmn" + ) } func test12_Compositor_LongGridInsertion() throws { @@ -317,19 +367,52 @@ final class MegrezTests: XCTestCase { XCTAssertEqual(compositor.cursor, 8) XCTAssertEqual(compositor.length, 15) XCTAssertEqual(compositor.spans.count, 15) - XCTAssertEqual(compositor.spans[0][6]?.keyArray.joined(separator: compositor.separator), "abcdef") - XCTAssertEqual(compositor.spans[1][6]?.keyArray.joined(separator: compositor.separator), "bcdefg") - XCTAssertEqual(compositor.spans[2][6]?.keyArray.joined(separator: compositor.separator), "cdefgX") - XCTAssertEqual(compositor.spans[3][6]?.keyArray.joined(separator: compositor.separator), "defgXh") - XCTAssertEqual(compositor.spans[3][5]?.keyArray.joined(separator: compositor.separator), "defgX") - XCTAssertEqual(compositor.spans[4][6]?.keyArray.joined(separator: compositor.separator), "efgXhi") - XCTAssertEqual(compositor.spans[4][5]?.keyArray.joined(separator: compositor.separator), "efgXh") + XCTAssertEqual( + compositor.spans[0][6]?.keyArray.joined(separator: compositor.separator), + "abcdef" + ) + XCTAssertEqual( + compositor.spans[1][6]?.keyArray.joined(separator: compositor.separator), + "bcdefg" + ) + XCTAssertEqual( + compositor.spans[2][6]?.keyArray.joined(separator: compositor.separator), + "cdefgX" + ) + XCTAssertEqual( + compositor.spans[3][6]?.keyArray.joined(separator: compositor.separator), + "defgXh" + ) + XCTAssertEqual( + compositor.spans[3][5]?.keyArray.joined(separator: compositor.separator), + "defgX" + ) + XCTAssertEqual( + compositor.spans[4][6]?.keyArray.joined(separator: compositor.separator), + "efgXhi" + ) + XCTAssertEqual( + compositor.spans[4][5]?.keyArray.joined(separator: compositor.separator), + "efgXh" + ) XCTAssertEqual(compositor.spans[4][4]?.keyArray.joined(separator: compositor.separator), "efgX") XCTAssertEqual(compositor.spans[4][3]?.keyArray.joined(separator: compositor.separator), "efg") - XCTAssertEqual(compositor.spans[5][6]?.keyArray.joined(separator: compositor.separator), "fgXhij") - XCTAssertEqual(compositor.spans[6][6]?.keyArray.joined(separator: compositor.separator), "gXhijk") - XCTAssertEqual(compositor.spans[7][6]?.keyArray.joined(separator: compositor.separator), "Xhijkl") - XCTAssertEqual(compositor.spans[8][6]?.keyArray.joined(separator: compositor.separator), "hijklm") + XCTAssertEqual( + compositor.spans[5][6]?.keyArray.joined(separator: compositor.separator), + "fgXhij" + ) + XCTAssertEqual( + compositor.spans[6][6]?.keyArray.joined(separator: compositor.separator), + "gXhijk" + ) + XCTAssertEqual( + compositor.spans[7][6]?.keyArray.joined(separator: compositor.separator), + "Xhijkl" + ) + XCTAssertEqual( + compositor.spans[8][6]?.keyArray.joined(separator: compositor.separator), + "hijklm" + ) } func test13_Compositor_StressBench() throws { @@ -478,7 +561,8 @@ final class MegrezTests: XCTestCase { func test18_Compositor_OverrideReset() throws { var compositor = Megrez.Compositor( - with: SimpleLM(input: strSampleData + "zhong1jiang3 終講 -11.0\n" + "jiang3jin1 槳襟 -11.0\n")) + with: SimpleLM(input: strSampleData + "zhong1jiang3 終講 -11.0\n" + "jiang3jin1 槳襟 -11.0\n") + ) compositor.separator = "" compositor.insertKey("nian2") compositor.insertKey("zhong1") @@ -517,7 +601,10 @@ final class MegrezTests: XCTestCase { result = compositor.walk() XCTAssertEqual(result.values, ["高熱", "🔥", "焰", "危險"]) - XCTAssertTrue(compositor.overrideCandidate(.init(keyArray: ["huo3", "yan4"], value: "🔥"), at: location)) + XCTAssertTrue(compositor.overrideCandidate( + .init(keyArray: ["huo3", "yan4"], value: "🔥"), + at: location + )) result = compositor.walk() XCTAssertEqual(result.values, ["高熱", "🔥", "危險"]) } @@ -593,10 +680,26 @@ final class MegrezTests: XCTestCase { var stack2A = [String]() var stack2B = [String]() for i in 0 ... compositor.keys.count { - stack1A.append(compositor.fetchCandidates(at: i, filter: .beginAt).map(\.value).joined(separator: "-")) - stack1B.append(compositor.fetchCandidates(at: i, filter: .endAt).map(\.value).joined(separator: "-")) - stack2A.append(compositor.fetchCandidatesDeprecated(at: i, filter: .beginAt).map(\.value).joined(separator: "-")) - stack2B.append(compositor.fetchCandidatesDeprecated(at: i, filter: .endAt).map(\.value).joined(separator: "-")) + stack1A + .append( + compositor.fetchCandidates(at: i, filter: .beginAt).map(\.value) + .joined(separator: "-") + ) + stack1B + .append( + compositor.fetchCandidates(at: i, filter: .endAt).map(\.value) + .joined(separator: "-") + ) + stack2A + .append( + compositor.fetchCandidatesDeprecated(at: i, filter: .beginAt).map(\.value) + .joined(separator: "-") + ) + stack2B + .append( + compositor.fetchCandidatesDeprecated(at: i, filter: .endAt).map(\.value) + .joined(separator: "-") + ) } stack1B.removeFirst() stack2B.removeLast()