Skip to content

Commit

Permalink
Class inheritence & Foundation independence.
Browse files Browse the repository at this point in the history
* Merge Vertex into Node, etc.

* Remove dependency of Foundation.
  • Loading branch information
ShikiSuen committed May 27, 2023
1 parent f6a2c2b commit 3477c1f
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 254 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Megrez Engine is a module made for processing lingual data of an input method. T

### §1. 初期化

在你的 IMKInputController 或者 InputHandler 內初期化一份 Megrez.Compositor 組字器副本(這裡將該副本命名為「`compositor`」)。由於 Megrez.Compositor 的型別是 Struct 型別(為了讓 Compositor 可以 deep copy),所以其副本可以用 var 來宣告。
在你的 IMKInputController 或者 InputHandler 內初期化一份 Megrez.Compositor 組字器副本(這裡將該副本命名為「`compositor`」)。由於 Megrez.Compositor 的型別是 Class 型別,所以其副本可以用 let 來宣告。

> 如果要製作副本的話,可以用「`.hardCopy`」。
以 InputHandler 為例:
```swift
Expand Down
20 changes: 9 additions & 11 deletions Sources/Megrez/1_Compositor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)

import Foundation

public extension Megrez {
/// 一個組字器用來在給定一系列的索引鍵的情況下(藉由一系列的觀測行為)返回一套資料值。
///
Expand All @@ -15,7 +13,7 @@ public extension Megrez {
/// 簡單的貝氏推論:因為底層的語言模組只會提供單元圖資料。一旦將所有可以組字的單元圖
/// 作為節點塞到組字器內,就可以用一個簡單的有向無環圖爬軌過程、來利用這些隱性資料值
/// 算出最大相似估算結果。
struct Compositor {
class Compositor {
/// 就文字輸入方向而言的方向。
public enum TypingDirection { case front, rear }
/// 軌格增減行為。
Expand Down Expand Up @@ -90,7 +88,7 @@ public extension Megrez {
///
/// 將已經被插入的索引鍵陣列與幅位單元陣列(包括其內的節點)全部清空。
/// 最近一次的爬軌結果陣列也會被清空。游標跳轉換算表也會被清空。
public mutating func clear() {
public func clear() {
cursor = 0
marker = 0
keys.removeAll()
Expand All @@ -101,7 +99,7 @@ public extension Megrez {
/// 在游標位置插入給定的索引鍵。
/// - Parameter key: 要插入的索引鍵。
/// - Returns: 該操作是否成功執行。
@discardableResult public mutating func insertKey(_ key: String) -> Bool {
@discardableResult public 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
Expand All @@ -122,7 +120,7 @@ public extension Megrez {
/// 如果是朝著與文字輸入方向相反的方向砍的話,游標位置會自動遞減。
/// - Parameter direction: 指定方向(相對於文字輸入方向而言)。
/// - Returns: 該操作是否成功執行。
@discardableResult public mutating func dropKey(direction: TypingDirection) -> Bool {
@discardableResult public 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))
Expand All @@ -144,7 +142,7 @@ public extension Megrez {
/// // 該特性不適用於小麥注音,除非小麥注音重新設計 InputState 且修改 KeyHandler、
/// 將標記游標交給敝引擎來管理。屆時,NSStringUtils 將徹底卸任。
/// - Returns: 該操作是否順利完成。
@discardableResult public mutating func jumpCursorBySpan(to direction: TypingDirection, isMarker: Bool = false)
@discardableResult public func jumpCursorBySpan(to direction: TypingDirection, isMarker: Bool = false)
-> Bool
{
var target = isMarker ? marker : cursor
Expand Down Expand Up @@ -186,7 +184,7 @@ public extension Megrez {
/// 生成用以交給 GraphViz 診斷的資料檔案內容,純文字。
public var dumpDOT: String {
// C# StringBuilder 與 Swift NSMutableString 能提供爆發性的效能。
let strOutput: NSMutableString = .init(string: "digraph {\ngraph [ rankdir=LR ];\nBOS;\n")
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 }
Expand Down Expand Up @@ -216,7 +214,7 @@ extension Megrez.Compositor {
/// - Parameters:
/// - location: 給定的幅位座標。
/// - action: 指定是擴張還是縮減一個幅位。
private mutating func resizeGrid(at location: Int, do action: ResizeBehavior) {
private func resizeGrid(at location: Int, do action: ResizeBehavior) {
let location = max(min(location, spans.count), 0) // 防呆
switch action {
case .expand:
Expand Down Expand Up @@ -260,7 +258,7 @@ extension Megrez.Compositor {
/// (XXXXXXX? <-被砍爛的節點
/// ```
/// - Parameter location: 給定的幅位座標。
mutating func dropWreckedNodes(at location: Int) {
func dropWreckedNodes(at location: Int) {
let location = max(min(location, spans.count), 0) // 防呆
guard !spans.isEmpty else { return }
let affectedLength = Megrez.Compositor.maxSpanLength - 1
Expand All @@ -286,7 +284,7 @@ extension Megrez.Compositor {
/// - Parameter updateExisting: 是否根據目前的語言模型的資料狀態來對既有節點更新其內部的單元圖陣列資料。
/// 該特性可以用於「在選字窗內屏蔽了某個詞之後,立刻生效」這樣的軟體功能需求的實現。
/// - Returns: 新增或影響了多少個節點。如果返回「0」則表示可能發生了錯誤。
@discardableResult public mutating func update(updateExisting: Bool = false) -> Int {
@discardableResult public func update(updateExisting: Bool = false) -> Int {
let maxSpanLength = Megrez.Compositor.maxSpanLength
let rangeOfPositions = max(0, cursor - maxSpanLength) ..< min(cursor + maxSpanLength, keys.count)
var nodesChanged = 0
Expand Down
151 changes: 96 additions & 55 deletions Sources/Megrez/2_Walker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,72 +13,113 @@ public extension Megrez.Compositor {
/// 對於 `G = (V, E)`,該算法的運行次數為 `O(|V|+|E|)`,其中 `G` 是一個有向無環圖。
/// 這意味著,即使軌格很大,也可以用很少的算力就可以爬軌。
/// - Returns: 爬軌結果+該過程是否順利執行。
@discardableResult mutating func walk() -> (walkedNodes: [Megrez.Node], succeeded: Bool) {
var result = [Megrez.Node]()
defer { walkedNodes = result }
guard !spans.isEmpty else { return (result, true) }

var vertexSpans: [[Int: Vertex]] = spans.map(\.asVertexSpan)

let terminal = Vertex(node: .init(keyArray: ["_TERMINAL_"]))
var root = Vertex(node: .init(keyArray: ["_ROOT_"]))
root.distance = 0
@discardableResult func walk() -> [Megrez.Node] {
defer { reinitVertexNetwork() }
sortAndRelax()
guard !spans.isEmpty else { return [] }
var iterated: Megrez.Node? = Megrez.Node.trailingNode
walkedNodes.removeAll()
while let itPrev = iterated?.prev {
// 此處必須得是 Copy,讓組字器外部對此的操作影響不到組字器內部的節點。
walkedNodes.insert(itPrev.copy, at: 0)
iterated = itPrev
}
iterated?.destroyVertex()
iterated = nil
walkedNodes.removeFirst()
return walkedNodes
}

vertexSpans.enumerated().forEach { location, vertexSpan in
vertexSpan.values.forEach { vertex in
let nextVertexPosition = location + vertex.node.spanLength
if nextVertexPosition == vertexSpans.count {
vertex.edges.append(terminal)
/// 先進行位相幾何排序、再卸勁。
internal func sortAndRelax() {
reinitVertexNetwork()
guard !spans.isEmpty else { return }
Megrez.Node.leadingNode.distance = 0
spans.enumerated().forEach { location, theSpan in
theSpan.values.forEach { theNode in
let nextVertexPosition = location + theNode.spanLength
if nextVertexPosition == spans.count {
theNode.edges.append(.trailingNode)
return
}
vertexSpans[nextVertexPosition].values.forEach { vertex.edges.append($0) }
spans[nextVertexPosition].values.forEach { theNode.edges.append($0) }
}
}

root.edges.append(contentsOf: vertexSpans[0].values)

topologicalSort(root: &root).reversed().forEach { neta in
neta.edges.indices.forEach { neta.relax(target: &neta.edges[$0]) }
}

var iterated = terminal
var walked = [Megrez.Node]()
var totalLengthOfKeys = 0

while let itPrev = iterated.prev {
walked.append(itPrev.node)
iterated = itPrev
totalLengthOfKeys += iterated.node.spanLength
Megrez.Node.leadingNode.edges.append(contentsOf: spans[0].values)
Self.topologicalSort().reversed().forEach { neta in
neta.edges.indices.forEach { Self.relax(u: neta, v: &neta.edges[$0]) }
}
}

// 清理內容,否則會有記憶體洩漏
vertexSpans.removeAll()
iterated.destroy()
root.destroy()
terminal.destroy()
/// 摧毀所有與共用起始虛擬節點有牽涉的節點自身的 Vertex 特性資料
internal func reinitVertexNetwork() {
Megrez.Node.leadingNode.destroyVertex()
Megrez.Node.trailingNode.destroyVertex()
}

guard totalLengthOfKeys == keys.count else {
print("!!! ERROR A")
return (result, false)
/// 對持有單個根頂點的有向無環圖進行位相幾何排序(topological
/// sort)、且將排序結果以頂點陣列的形式給出。
///
/// 這裡使用我們自己的堆棧和狀態定義實現了一個非遞迴版本,
/// 這樣我們就不會受到當前線程的堆棧大小的限制。以下是等價的原始算法。
/// ```
/// func topologicalSort(node: Node) {
/// node.edges.forEach { nodeNode in
/// if !nodeNode.topologicallySorted {
/// dfs(nodeNode, result)
/// nodeNode.topologicallySorted = true
/// }
/// result.append(nodeNode)
/// }
/// }
/// ```
/// 至於其遞迴版本,則類似於 Cormen 在 2001 年的著作「Introduction to Algorithms」當中的樣子。
/// - Returns: 排序結果(頂點陣列)。
private static func topologicalSort() -> [Megrez.Node] {
class State {
var iterIndex: Int
let node: Megrez.Node
init(node: Megrez.Node, iterIndex: Int = 0) {
self.node = node
self.iterIndex = iterIndex
}
}
guard walked.count >= 2 else {
print("!!! ERROR B")
return (result, false)
var result = [Megrez.Node]()
var stack = [State]()
stack.append(.init(node: .leadingNode))
while !stack.isEmpty {
let state = stack[stack.count - 1]
let theNode = state.node
if state.iterIndex < state.node.edges.count {
let newNode = state.node.edges[state.iterIndex]
state.iterIndex += 1
if !newNode.topologicallySorted {
stack.append(.init(node: newNode))
continue
}
}
theNode.topologicallySorted = true
result.append(theNode)
stack.removeLast()
}
walked = walked.reversed()
walked.removeFirst()
result = walked
return (result, true)
return result
}
}

extension Megrez.SpanUnit {
/// 將當前幅位單元由節點辭典轉為頂點辭典。
var asVertexSpan: [Int: Megrez.Compositor.Vertex] {
var result = [Int: Megrez.Compositor.Vertex]()
forEach { theKey, theValue in
result[theKey] = .init(node: theValue)
}
return result
/// 卸勁函式。
///
/// 「卸勁 (relax)」一詞出自 Cormen 在 2001 年的著作「Introduction to Algorithms」的 585 頁。
/// - Remark: 自己就是參照頂點 (u),會在必要時成為 target (v) 的前述頂點。
/// - Parameters:
/// - u: 基準頂點。
/// - v: 要影響的頂點。
private static func relax(u: Megrez.Node, v: inout Megrez.Node) {
// 從 u 到 w 的距離,也就是 v 的權重。
let w: Double = v.score
// 這裡計算最大權重:
// 如果 v 目前的距離值小於「u 的距離值+w(w 是 u 到 w 的距離,也就是 v 的權重)」,
// 我們就更新 v 的距離及其前述頂點。
guard v.distance < u.distance + w else { return }
v.distance = u.distance + w
v.prev = u
}
}
52 changes: 31 additions & 21 deletions Sources/Megrez/3_KeyValuePaired.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,72 @@
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)

import Foundation

public extension Megrez {
/// 鍵值配對,乃索引鍵陣列與讀音的配對單元。
struct KeyValuePaired: Equatable, Hashable, Comparable, CustomStringConvertible {
class KeyValuePaired: Unigram, Comparable {
/// 索引鍵陣列。一般情況下用來放置讀音等可以用來作為索引的內容。
public var keyArray: [String]
/// 資料值。
public var value: String
public var keyArray: [String] = []
/// 將當前鍵值列印成一個字串。
public var description: String { "(" + keyArray.description + "," + value + ")" }
override 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 toNGramKey: String { !isValid ? "()" : "(\(joinedKey()),\(value))" }
/// 通用陣列表達形式。
public var keyValueTuplet: (keyArray: [String], value: String) { (keyArray, value) }
/// 通用陣列表達形式。
public var tupletExpression: (keyArray: [String], value: String) { (keyArray, value) }
public var triplet: (keyArray: [String], value: String, score: Double) { (keyArray, value, score) }

/// 初期化一組鍵值配對。
/// - Parameters:
/// - keyArray: 索引鍵陣列。一般情況下用來放置讀音等可以用來作為索引的內容。
/// - value: 資料值。
public init(keyArray: [String], value: String = "N/A") {
/// - score: 權重(雙精度小數)。
public init(keyArray: [String], value: String = "N/A", score: Double = 0) {
super.init(value: value.isEmpty ? "N/A" : value, score: score)
self.keyArray = keyArray.isEmpty ? ["N/A"] : keyArray
self.value = value.isEmpty ? "N/A" : value
}

/// 初期化一組鍵值配對。
/// - Parameter tupletExpression: 傳入的通用陣列表達形式。
/// - Parameter tripletExpression: 傳入的通用陣列表達形式。
public init(_ tripletExpression: (keyArray: [String], value: String, score: Double)) {
let theValue = tripletExpression.value.isEmpty ? "N/A" : tripletExpression.value
super.init(value: theValue, score: tripletExpression.score)
keyArray = tripletExpression.keyArray.isEmpty ? ["N/A"] : tripletExpression.keyArray
}

/// 初期化一組鍵值配對。
/// - Parameter tuplet: 傳入的通用陣列表達形式。
public init(_ tupletExpression: (keyArray: [String], value: String)) {
let theValue = tupletExpression.value.isEmpty ? "N/A" : tupletExpression.value
super.init(value: theValue, score: 0)
keyArray = tupletExpression.keyArray.isEmpty ? ["N/A"] : tupletExpression.keyArray
value = tupletExpression.value.isEmpty ? "N/A" : tupletExpression.value
}

/// 初期化一組鍵值配對。
/// - Parameters:
/// - key: 索引鍵。一般情況下用來放置讀音等可以用來作為索引的內容。
/// - value: 資料值。
public init(key: String = "N/A", value: String = "N/A") {
keyArray = key.isEmpty ? ["N/A"] : key.components(separatedBy: Megrez.Compositor.theSeparator)
self.value = value.isEmpty ? "N/A" : value
/// - score: 權重(雙精度小數)。
public init(key: String = "N/A", value: String = "N/A", score: Double = 0) {
super.init(value: value.isEmpty ? "N/A" : value, score: score)
keyArray = key.isEmpty ? ["N/A"] : key.sliced(by: Megrez.Compositor.theSeparator)
}

/// 做為預設雜湊函式。
/// - Parameter hasher: 目前物件的雜湊碼。
public func hash(into hasher: inout Hasher) {
override 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 static func == (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool {
lhs.keyArray == rhs.keyArray && lhs.value == rhs.value
lhs.score == rhs.score && lhs.keyArray == rhs.keyArray && lhs.value == rhs.value
}

public static func < (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool {
Expand Down Expand Up @@ -193,9 +203,9 @@ public extension Megrez.Compositor {
arrOverlappedNodes = fetchOverlappingNodes(at: i)
arrOverlappedNodes.forEach { anchor in
if anchor.node == overridden.node { return }
if !overridden.node.joinedKey(by: "\t").contains(anchor.node.joinedKey(by: "\t"))
|| !overridden.node.value.contains(anchor.node.value)
{
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
}
Expand Down
Loading

0 comments on commit 3477c1f

Please sign in to comment.