Skip to content

Commit

Permalink
Add DumpDOT support and fix unit tests. (#25)
Browse files Browse the repository at this point in the history
* Refactoring & Add documentation.

* DumpDOT support.

* Fix data conversion loss in UnitTests and add DumpDOT tests.
  • Loading branch information
ShikiSuen authored May 19, 2022
1 parent c105514 commit 22dfbf0
Show file tree
Hide file tree
Showing 12 changed files with 571 additions and 299 deletions.
118 changes: 113 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,117 @@

Megrez Engine is a module made for processing lingual data of an input method. This repository is part of Operation Longinus of The vChewing Project.

欲知使用方法,請洽該倉庫內的 MegrezTests.swift 檔案當中的示例、也可研讀上文提到的威注音輸入法的倉庫內的源碼。
## 使用說明

- Swiftified by (c) 2022 and onwards The vChewing Project (MIT-NTL License).
- Swift programmer: Shiki Suen
- C++ migration review: Hiraku Wong
- Rebranded from (c) Lukhnos Liu's C++ library "Gramambular" (MIT License).
### §1. 初期化

在你的 ctlInputMethod (InputMethodController) 或者 KeyHandler 內初期化一份 Megrez.BlockReadingBuilder 分節讀音槽副本(這裡將該副本命名為「`_builder`」)。由於 Megrez.BlockReadingBuilder 的型別是 Class 型別,所以其副本可以用 let 來宣告。

以 KeyHandler 為例:
```swift
class KeyHandler: NSObject {
// 先設定好變數
let _builder: Megrez.BlockReadingBuilder = .init()
...
}
```

以 ctlInputMethod 為例:
```swift
@objc(ctlInputMethod) // 根據 info.plist 內的情況來確定型別的命名
class ctlInputMethod: IMKInputController {
// 先設定好變數
let _builder: Megrez.BlockReadingBuilder = .init()
...
}
```

由於 Swift 會在某個大副本(KeyHandler 或者 ctlInputMethod 副本)被銷毀的時候自動銷毀其中的全部副本,所以 Megrez.BlockReadingBuilder 的副本初期化沒必要寫在 init() 當中。但你很可能會想在 init() 時指定 Tekkon.Composer 所對接的語言模組型別、以及其可以允許的最大詞長。

這裡就需要在 init() 時使用參數:
```swift
/// 分節讀音槽。
/// - Parameters:
/// - lm: 語言模型。可以是任何基於 Megrez.LanguageModel 的衍生型別。
/// - length: 指定該分節讀音曹內可以允許的最大詞長,預設為 10 字。
/// - separator: 多字讀音鍵當中用以分割漢字讀音的記號,預設為空。
let _builder: Megrez.BlockReadingBuilder = .init(lm: lmTest, length: 13, separator: "-")
```

### §2. 使用範例

請結合 MegrezTests.swift 檔案來學習。這裡只是給個概述。

#### // 1. 準備用作語言模型的專用型別

首先,Megrez 內建的 LanguageModel 型別是遠遠不夠用的,只能說是個類似於 protocol 一樣的存在。你需要自己單獨寫一個新的衍生型別:

```swift
class ExampleLM: Megrez.LanguageModel {
...
override func unigramsFor(key: String) -> [Megrez.Unigram] {
...
}
...
}
```

這個型別需要下述兩個函數能夠針對給定的鍵回饋對應的資料值、或其存無狀態:
- unigramsFor(key: String) -> [Megrez.Unigram]
- hasUnigramsFor(key: String) -> Bool

MegrezTests.swift 檔案內的 SimpleLM 可以作為範例。

如果需要更實戰的範例的話,可以洽威注音專案的倉庫內的 LMInstantiator.swift。

#### // 2. 怎樣與 builder 互動:

這裡只講幾個常用函數:

- 游標位置 `builder.cursorIndex` 是可以賦值與取值的動態變數,且會在賦值內容為超出位置範圍的數值時自動修正。初期值為 0。
- `builder.insertReadingAtCursor(reading: "gao1")` 可以在當前的游標位置插入讀音「gao1」。
- `builder.deleteReadingToTheFrontOfCursor()` 的作用是:朝著往文字輸入方向、砍掉一個與游標相鄰的讀音。反之,`deleteReadingAtTheRearOfCursor` 則朝著與文字輸入方向相反的方向、砍掉一個與游標相鄰的讀音。
- 在威注音的術語體系當中,「文字輸入方向」為向前(Front)、與此相反的方向為向後(Rear)。
- `builder.grid.fixNodeSelectedCandidate(location: ?, value: "??")` 用來根據輸入法選中的候選字詞、據此更新當前游標位置選中的候選字詞節點當中的候選字詞。

輸入完內容之後,可以聲明一個用來接收結果的變數:

```swift
/// 對已給定的軌格按照給定的位置與條件進行正向爬軌。
///
/// 其實就是將反向爬軌的結果顛倒順序再給出來而已,省得使用者自己再顛倒一遍。
/// - Parameters:
/// - at: 開始爬軌的位置。
/// - score: 給定累計權重,非必填參數。預設值為 0。
/// - nodesLimit: 限定最多只爬多少個節點。
/// - balanced: 啟用平衡權重,在節點權重的基礎上根據節點幅位長度來加權。
var walked = _builder.walk(at: builder.grid.width, score: 0.0, nodesLimit: 3, balanced: true)
```

MegrezTests.swift 是輸入了很多內容之後再 walk 的。實際上一款輸入法會在你每次插入讀音或刪除讀音的時候都重新 walk。那些處於候選字詞鎖定狀態的節點不會再受到之後的 walk 的行為的影響,但除此之外的節點會因為每次 walk 而可能各自的候選字詞會出現自動變化。如果給了 nodesLimit 一個非零的數值的話,則 walk 的範圍外的節點不會受到影響。

walk 之後的取值的方法及利用方法可以有很多種。這裡有其中的一個:

```swift
var composed: [String] = []
for phrase in walked {
if let node = phrase.node {
composed.append(node.currentKeyValue.value)
}
}
print(composed)
```

上述 print 結果就是 _builder 目前的組句,是這種陣列格式(以吳宗憲的詩句為例):
```swift
["八月", "中秋", "山林", "", "風吹", "大地", "草枝", ""]
```

自己看 MegrezTests.swift 慢慢研究吧。

## 著作權 (Credits)

- Swiftified and further development by (c) 2022 and onwards The vChewing Project (MIT-NTL License).
- Swift programmer: Shiki Suen
- C++ migration review: Hiraku Wong
- Was initially rebranded from (c) Lukhnos Liu's C++ library "Gramambular" (MIT License).
173 changes: 146 additions & 27 deletions Sources/Megrez/1_BlockReadingBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,64 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

extension Megrez {
/// 分節讀音槽。
public class BlockReadingBuilder {
var mutMaximumBuildSpanLength = 10
var mutCursorIndex: Int = 0
var mutReadings: [String] = []
var mutGrid: Grid = .init()
var mutLM: LanguageModel
var mutJoinSeparator: String = ""

public init(lm: LanguageModel, length: Int = 10) {
/// 該分節讀音曹內可以允許的最大詞長。
private var mutMaximumBuildSpanLength = 10
/// 該分節讀音槽的游標位置。
private var mutCursorIndex: Int = 0
/// 該分節讀音槽的讀音陣列。
private var mutReadings: [String] = []
/// 該分節讀音槽的軌格。
private var mutGrid: Grid = .init()
/// 該分節讀音槽所使用的語言模型。
private var mutLM: LanguageModel

/// 公開:多字讀音鍵當中用以分割漢字讀音的記號,預設為空。
public var joinSeparator: String = ""
/// 公開:該分節讀音槽的游標位置。
public var cursorIndex: Int {
get { mutCursorIndex }
set { mutCursorIndex = min(newValue, mutReadings.count) }
}

/// 公開:該分節讀音槽的軌格(唯讀)。
public var grid: Grid { mutGrid }
/// 公開:該分節讀音槽的長度,也就是內建漢字讀音的數量(唯讀)。
public var length: Int { mutReadings.count }
/// 公開:該分節讀音槽的讀音陣列(唯讀)。
public var readings: [String] { mutReadings }

/// 分節讀音槽。
/// - Parameters:
/// - lm: 語言模型。可以是任何基於 Megrez.LanguageModel 的衍生型別。
/// - length: 指定該分節讀音曹內可以允許的最大詞長,預設為 10 字。
/// - separator: 多字讀音鍵當中用以分割漢字讀音的記號,預設為空。
public init(lm: LanguageModel, length: Int = 10, separator: String = "") {
mutLM = lm
mutMaximumBuildSpanLength = length
joinSeparator = separator
}

/// 分節讀音槽自我清空專用函數。
public func clear() {
mutCursorIndex = 0
mutReadings.removeAll()
mutGrid.clear()
}

public func length() -> Int { mutReadings.count }

public func cursorIndex() -> Int { mutCursorIndex }

public func setCursorIndex(newIndex: Int) {
mutCursorIndex = min(newIndex, mutReadings.count)
}

/// 在游標位置插入給定的讀音。
/// - Parameters:
/// - reading: 要插入的讀音。
public func insertReadingAtCursor(reading: String) {
mutReadings.insert(reading, at: mutCursorIndex)
mutGrid.expandGridByOneAt(location: mutCursorIndex)
build()
mutCursorIndex += 1
}

public func readings() -> [String] { mutReadings }

/// 朝著與文字輸入方向相反的方向、砍掉一個與游標相鄰的讀音。
/// 在威注音的術語體系當中,「與文字輸入方向相反的方向」為向後(Rear)。
@discardableResult public func deleteReadingAtTheRearOfCursor() -> Bool {
if mutCursorIndex == 0 {
return false
Expand All @@ -72,6 +94,8 @@ extension Megrez {
return true
}

/// 朝著往文字輸入方向、砍掉一個與游標相鄰的讀音。
/// 在威注音的術語體系當中,「文字輸入方向」為向前(Front)。
@discardableResult public func deleteReadingToTheFrontOfCursor() -> Bool {
if mutCursorIndex == mutReadings.count {
return false
Expand All @@ -83,8 +107,12 @@ extension Megrez {
return true
}

/// 移除該分節讀音槽的第一個讀音單元。
///
/// 用於輸入法組字區長度上限處理:
/// 將該位置要溢出的敲字內容遞交之後、再執行這個函數。
@discardableResult public func removeHeadReadings(count: Int) -> Bool {
if count > length() {
if count > length {
return false
}

Expand All @@ -100,17 +128,108 @@ extension Megrez {
return true
}

public func setJoinSeparator(separator: String) {
mutJoinSeparator = separator
// MARK: - Walker

/// 對已給定的軌格按照給定的位置與條件進行正向爬軌。
///
/// 其實就是將反向爬軌的結果顛倒順序再給出來而已,省得使用者自己再顛倒一遍。
/// - Parameters:
/// - at: 開始爬軌的位置。
/// - score: 給定累計權重,非必填參數。預設值為 0。
/// - nodesLimit: 限定最多只爬多少個節點。
/// - balanced: 啟用平衡權重,在節點權重的基礎上根據節點幅位長度來加權。
public func walk(
at location: Int,
score accumulatedScore: Double = 0.0,
nodesLimit: Int = 0,
balanced: Bool = false
) -> [NodeAnchor] {
Array(
reverseWalk(
at: location, score: accumulatedScore,
nodesLimit: nodesLimit, balanced: balanced
).reversed())
}

public func joinSeparator() -> String { mutJoinSeparator }
/// 對已給定的軌格按照給定的位置與條件進行反向爬軌。
/// - Parameters:
/// - at: 開始爬軌的位置。
/// - score: 給定累計權重,非必填參數。預設值為 0。
/// - nodesLimit: 限定最多只爬多少個節點。
/// - balanced: 啟用平衡權重,在節點權重的基礎上根據節點幅位長度來加權。
public func reverseWalk(
at location: Int,
score accumulatedScore: Double = 0.0,
nodesLimit: Int = 0,
balanced: Bool = false
) -> [NodeAnchor] {
if location == 0 || location > mutGrid.width {
return [] as [NodeAnchor]
}

var paths: [[NodeAnchor]] = []
var nodes: [NodeAnchor] = mutGrid.nodesEndingAt(location: location)

if balanced {
nodes.sort {
$0.balancedScore > $1.balancedScore
}
}

for (i, n) in nodes.enumerated() {
// 只檢查前 X 個 NodeAnchor 是否有 node。
// 這裡有 abs 是為了防止有白癡填負數。
if abs(nodesLimit) > 0, i == abs(nodesLimit) - 1 {
break
}

public func grid() -> Grid { mutGrid }
var n = n
guard let nNode = n.node else {
continue
}

n.accumulatedScore = accumulatedScore + nNode.score

// 利用幅位長度來決定權重。
// 這樣一來,例:「再見」比「在」與「見」的權重更高。
if balanced {
let weightedScore: Double = (Double(n.spanningLength) - 1) * 2
n.accumulatedScore += weightedScore
}

var path: [NodeAnchor] = reverseWalk(
at: location - n.spanningLength,
score: n.accumulatedScore
)

path.insert(n, at: 0)

paths.append(path)

// 始終使用固定的候選字詞
if balanced, nNode.score >= 0 {
break
}
}

if !paths.isEmpty {
if var result = paths.first {
for value in paths {
if let vLast = value.last, let rLast = result.last {
if vLast.accumulatedScore > rLast.accumulatedScore {
result = value
}
}
}
return result
}
}
return [] as [NodeAnchor]
}

public func build() {
// if (mutLM == nil) { return } // 這個出不了 nil,所以註釋掉。
// MARK: - Private functions

private func build() {
let itrBegin: Int =
(mutCursorIndex < mutMaximumBuildSpanLength) ? 0 : mutCursorIndex - mutMaximumBuildSpanLength
let itrEnd: Int = min(mutCursorIndex + mutMaximumBuildSpanLength, mutReadings.count)
Expand All @@ -121,7 +240,7 @@ extension Megrez {
break
}
let strSlice = mutReadings[p..<(p + q)]
let combinedReading: String = join(slice: strSlice, separator: mutJoinSeparator)
let combinedReading: String = join(slice: strSlice, separator: joinSeparator)

if !mutGrid.hasMatchedNode(location: p, spanningLength: q, key: combinedReading) {
let unigrams: [Unigram] = mutLM.unigramsFor(key: combinedReading)
Expand All @@ -134,7 +253,7 @@ extension Megrez {
}
}

public func join(slice strSlice: ArraySlice<String>, separator: String) -> String {
private func join(slice strSlice: ArraySlice<String>, separator: String) -> String {
var arrResult: [String] = []
for value in strSlice {
arrResult.append(value)
Expand Down
Loading

0 comments on commit 22dfbf0

Please sign in to comment.