Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor some handlings and unit tests. #80

Merged
merged 1 commit into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 14 additions & 28 deletions Sources/Tekkon/Tekkon_Phonabets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.

infix operator <~: AssignmentPrecedence

public extension Tekkon {
// MARK: - Dynamic Constants and Basic Enums

/// 定義注音符號的種類
enum PhoneType: Int {
enum PhoneType: Int, Codable, Hashable {
case null = 0 // 假
case consonant = 1 // 聲
case semivowel = 2 // 介
Expand All @@ -19,7 +21,7 @@ public extension Tekkon {
}

/// 定義注音排列的類型
enum MandarinParser: Int {
enum MandarinParser: Int, Codable, Hashable {
case ofDachen = 0
case ofDachen26 = 1
case ofETen = 2
Expand Down Expand Up @@ -85,7 +87,7 @@ public extension Tekkon {
/// 如果遇到被設為多個字符、或者字符不對的情況的話,value 會被清空、PhoneType 會變成 null。
/// 賦值時最好直接重新 init 且一直用 let 來初期化 Phonabet。
/// 其實 value 對外只讀,對內的話另有 valueStorage 代為存儲內容。這樣比較安全一些。
@frozen struct Phonabet: Equatable, Hashable, ExpressibleByStringLiteral {
@frozen struct Phonabet: Equatable, Codable, Hashable {
public var type: PhoneType = .null
private var valueStorage = ""
public var value: String { valueStorage }
Expand Down Expand Up @@ -117,6 +119,15 @@ public extension Tekkon {
ensureType()
}

public mutating func setValue(_ newValue: String) {
valueStorage = newValue
ensureType()
}

public static func <~ (_ lhs: inout Tekkon.Phonabet, _ newValue: String) {
lhs.setValue(newValue)
}

/// 用來自動更新自身的屬性值的函式。
public mutating func ensureType() {
if Tekkon.allowedConsonants.contains(value) {
Expand All @@ -132,30 +143,5 @@ public extension Tekkon {
valueStorage = ""
}
}

// MARK: - Misc Definitions

/// 這些內容用來滿足 "Equatable, Hashable, ExpressibleByStringLiteral" 需求。

public static func == (lhs: Phonabet, rhs: Phonabet) -> Bool {
lhs.value == rhs.value
}

public func hash(into hasher: inout Hasher) {
hasher.combine(value)
hasher.combine(type)
}

public init(stringLiteral value: String) {
self.init(value)
}

public init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}

public init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}
}
}
129 changes: 48 additions & 81 deletions Sources/Tekkon/Tekkon_SyllableComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ public extension Tekkon {
///
/// 因為是 String Literal,所以初期化時可以藉由 @input 參數指定初期已經傳入的按鍵訊號。
/// 還可以在初期化時藉由 @arrange 參數來指定注音排列(預設為「.ofDachen」大千佈局)。
@frozen struct Composer: Equatable, Hashable, ExpressibleByStringLiteral {
@frozen struct Composer: Equatable, Codable, Hashable, ExpressibleByStringLiteral {
/// 聲母。
public var consonant: Phonabet = ""
public var consonant: Phonabet = .init()

/// 介母。
public var semivowel: Phonabet = ""
public var semivowel: Phonabet = .init()

/// 韻母。
public var vowel: Phonabet = ""
public var vowel: Phonabet = .init()

/// 聲調。
public var intonation: Phonabet = ""
public var intonation: Phonabet = .init()

/// 拼音組音區。
public var romajiBuffer: String = ""
public var romajiBuffer: String = .init()

/// 注音排列種類。預設情況下是大千排列(Windows / macOS 預設注音排列)。
public var parser: MandarinParser = .ofDachen
Expand Down Expand Up @@ -204,10 +204,8 @@ public extension Tekkon {
receiveKey(fromPhonabet: translate(key: input))
return
}
if mapArayuruPinyinIntonation.keys.contains(input) {
if let theTone = mapArayuruPinyinIntonation[input] {
intonation = Phonabet(theTone)
}
if let theTone = mapArayuruPinyinIntonation[input] {
intonation = Phonabet(theTone)
} else {
// 為了防止 romajiBuffer 越敲越長帶來算力負擔,這裡讓它在要溢出時自動丟掉最早輸入的音頭。
let maxCount: Int = (parser == .ofWadeGilesPinyin) ? 7 : 6
Expand Down Expand Up @@ -241,36 +239,32 @@ public extension Tekkon {
if phonabetCombinationCorrectionEnabled {
switch phonabet {
case "ㄧ", "ㄩ":
if vowel.value == "ㄜ" { vowel = "ㄝ" }
if vowel.value == "ㄜ" { vowel <~ "ㄝ" }
case "ㄜ":
if "ㄨ".doesHave(semivowel.value) { semivowel = "ㄩ" }
if "ㄧㄩ".doesHave(semivowel.value) { thePhone = "ㄝ" }
if "ㄨ".doesHave(semivowel.value) { semivowel <~ "ㄩ" }
if "ㄧㄩ".doesHave(semivowel.value) { thePhone <~ "ㄝ" }
case "ㄝ":
if "ㄨ".doesHave(semivowel.value) { semivowel = "ㄩ" }
if "ㄨ".doesHave(semivowel.value) { semivowel <~ "ㄩ" }
case "ㄛ", "ㄥ":
if phonabet == "ㄛ", semivowel.value == "ㄩ" { semivowel = "ㄨ" }
if phonabet == "ㄛ", semivowel.value == "ㄩ" { semivowel <~ "ㄨ" }
if "ㄅㄆㄇㄈ".doesHave(consonant.value), semivowel.value == "ㄨ" { semivowel.clear() }
case "ㄟ":
if "ㄋㄌ".doesHave(consonant.value), semivowel.value == "ㄨ" { semivowel.clear() }
case "ㄨ":
if "ㄅㄆㄇㄈ".doesHave(consonant.value), "ㄛㄥ".doesHave(vowel.value) { vowel.clear() }
if "ㄋㄌ".doesHave(consonant.value), "ㄟ".doesHave(vowel.value) { vowel.clear() }
if "ㄜ".doesHave(vowel.value) { vowel = "ㄝ" }
if "ㄝ".doesHave(vowel.value) { thePhone = "ㄩ" }
if "ㄜ".doesHave(vowel.value) { vowel <~ "ㄝ" }
if "ㄝ".doesHave(vowel.value) { thePhone <~ "ㄩ" }
case "ㄅ", "ㄆ", "ㄇ", "ㄈ":
if ["ㄨㄛ", "ㄨㄥ"].contains(semivowel.value + vowel.value) { semivowel.clear() }
default: break
}
if [.vowel, .intonation].contains(thePhone.type), "ㄓㄔㄕㄗㄘㄙ".doesHave(consonant.value) {
switch semivowel.value {
case "ㄧ": semivowel.clear()
case "ㄩ":
switch consonant {
case _ where "ㄓㄗ".doesHave(consonant.value): consonant = "ㄐ"
case _ where "ㄔㄘ".doesHave(consonant.value): consonant = "ㄑ"
case _ where "ㄕㄙ".doesHave(consonant.value): consonant = "ㄒ"
default: break
}
switch (semivowel.value, consonant.value) {
case ("ㄧ", _): semivowel.clear()
case ("ㄩ", "ㄓ"), ("ㄩ", "ㄗ"): consonant <~ "ㄐ"
case ("ㄩ", "ㄔ"), ("ㄩ", "ㄘ"): consonant <~ "ㄑ"
case ("ㄩ", "ㄕ"), ("ㄩ", "ㄙ"): consonant <~ "ㄒ"
default: break
}
}
Expand Down Expand Up @@ -430,38 +424,14 @@ public extension Tekkon {
// 處理特殊情形。
switch incomingPhonabet.type {
case .semivowel:
switch consonant {
case "ㄍ":
switch incomingPhonabet {
// 這裡不處理「ㄍㄧ」到「ㄑㄧ」的轉換,因為只有倚天26需要處理這個。
case "ㄨ": consonant = "ㄍ" // ㄍㄨ
case "ㄩ": consonant = "ㄑ" // ㄑㄩ
default: break
}
case "ㄓ":
switch incomingPhonabet {
case "ㄧ": consonant = "ㄐ" // ㄐㄧ
case "ㄨ": consonant = "ㄓ" // ㄓㄨ
case "ㄩ": consonant = "ㄐ" // ㄐㄩ
default: break
}
case "ㄔ":
switch incomingPhonabet {
case "ㄧ": consonant = "ㄑ" // ㄐㄧ
case "ㄨ": consonant = "ㄔ" // ㄓㄨ
case "ㄩ": consonant = "ㄑ" // ㄐㄩ
default: break
}
case "ㄕ":
switch incomingPhonabet {
case "ㄧ": consonant = "ㄒ" // ㄒㄧ
case "ㄨ": consonant = "ㄕ" // ㄕㄨ
case "ㄩ": consonant = "ㄒ" // ㄒㄩ
default: break
}
// 這裡不處理「ㄍㄧ」到「ㄑㄧ」的轉換,因為只有倚天26需要處理這個。
switch (consonant.value, incomingPhonabet.value) {
case ("ㄓ", "ㄧ"), ("ㄓ", "ㄩ"): consonant <~ "ㄐ"
case ("ㄍ", "ㄩ"), ("ㄔ", "ㄧ"), ("ㄔ", "ㄩ"): consonant <~ "ㄑ"
case ("ㄕ", "ㄧ"), ("ㄕ", "ㄩ"): consonant <~ "ㄒ"
default: break
}
if incomingPhonabet == "ㄨ" {
if incomingPhonabet.value == "ㄨ" {
fixValue("ㄐ", "ㄓ")
fixValue("ㄑ", "ㄔ")
fixValue("ㄒ", "ㄕ")
Expand Down Expand Up @@ -491,8 +461,8 @@ public extension Tekkon {
case "f" where isPronounceable: strReturn = "ˊ"
case "j" where isPronounceable: strReturn = "ˇ"
case "k" where isPronounceable: strReturn = "ˋ"
case "e" where consonant == "ㄍ": consonant = "ㄑ"
case "p" where !consonant.isEmpty || semivowel == "ㄧ": strReturn = "ㄡ"
case "e" where consonant.value == "ㄍ": consonant <~ "ㄑ"
case "p" where !consonant.isEmpty || semivowel.value == "ㄧ": strReturn = "ㄡ"
case "h" where !consonant.isEmpty || !semivowel.isEmpty: strReturn = "ㄦ"
case "l" where !consonant.isEmpty || !semivowel.isEmpty: strReturn = "ㄥ"
case "m" where !consonant.isEmpty || !semivowel.isEmpty: strReturn = "ㄢ"
Expand Down Expand Up @@ -520,7 +490,7 @@ public extension Tekkon {
}

// 後置修正
if value == "ㄍ˙" { consonant = "ㄑ" }
if value == "ㄍ˙" { consonant <~ "ㄑ" }

// 這些按鍵在上文處理過了,就不要再回傳了。
if keysToHandleHere.doesHave(key) { strReturn = "" }
Expand Down Expand Up @@ -585,7 +555,7 @@ public extension Tekkon {
}

// 後置修正
if value == "ㄔ˙" { consonant = "ㄑ" }
if value == "ㄔ˙" { consonant <~ "ㄑ" }

// 這些按鍵在上文處理過了,就不要再回傳了。
if keysToHandleHere.doesHave(key) { strReturn = "" }
Expand Down Expand Up @@ -654,41 +624,38 @@ public extension Tekkon {
case "d" where isPronounceable: strReturn = "ˋ"
case "y" where isPronounceable: strReturn = "˙"
case "b" where !consonant.isEmpty || !semivowel.isEmpty: strReturn = "ㄝ"
case "i" where vowel.isEmpty || vowel == "ㄞ": strReturn = "ㄛ"
case "l" where vowel.isEmpty || vowel == "ㄤ": strReturn = "ㄠ"
case "i" where vowel.isEmpty || vowel.value == "ㄞ": strReturn = "ㄛ"
case "l" where vowel.isEmpty || vowel.value == "ㄤ": strReturn = "ㄠ"
case "n" where !consonant.isEmpty || !semivowel.isEmpty:
if value == "ㄙ" { consonant.clear() }
strReturn = "ㄥ"
case "o" where vowel.isEmpty || vowel == "ㄢ": strReturn = "ㄟ"
case "p" where vowel.isEmpty || vowel == "ㄦ": strReturn = "ㄣ"
case "q" where consonant.isEmpty || consonant == "ㄅ": strReturn = "ㄆ"
case "t" where consonant.isEmpty || consonant == "ㄓ": strReturn = "ㄔ"
case "w" where consonant.isEmpty || consonant == "ㄉ": strReturn = "ㄊ"
case "o" where vowel.isEmpty || vowel.value == "ㄢ": strReturn = "ㄟ"
case "p" where vowel.isEmpty || vowel.value == "ㄦ": strReturn = "ㄣ"
case "q" where consonant.isEmpty || consonant.value == "ㄅ": strReturn = "ㄆ"
case "t" where consonant.isEmpty || consonant.value == "ㄓ": strReturn = "ㄔ"
case "w" where consonant.isEmpty || consonant.value == "ㄉ": strReturn = "ㄊ"
case "m":
if semivowel == "ㄩ", vowel != "ㄡ" {
switch (semivowel.value, vowel.value) {
case ("ㄩ", _):
semivowel.clear()
strReturn = "ㄡ"
} else if semivowel != "ㄩ", vowel == "ㄡ" {
case (_, "ㄡ"):
vowel.clear()
strReturn = "ㄩ"
} else if !semivowel.isEmpty {
strReturn = "ㄡ"
} else {
strReturn = "ㄐㄑㄒ".doesHave(consonant.value) ? "ㄩ" : "ㄡ"
case ("ㄩ", "ㄡ"), _:
strReturn = (!semivowel.isEmpty || !"ㄐㄑㄒ".doesHave(consonant.value)) ? "ㄡ" : "ㄩ"
}
case "u":
if semivowel == "ㄧ", vowel != "ㄚ" {
semivowel.clear()
strReturn = "ㄚ"
} else if semivowel != "ㄧ", vowel == "ㄚ" {
strReturn = "ㄧ"
} else if semivowel == "ㄧ", vowel == "ㄚ" {
switch (semivowel.value, vowel.value) {
case ("ㄧ", "ㄚ"):
semivowel.clear()
vowel.clear()
} else if !semivowel.isEmpty {
case ("ㄧ", _):
semivowel.clear()
strReturn = "ㄚ"
} else {
case (_, "ㄚ"):
strReturn = "ㄧ"
case (_, _): strReturn = semivowel.isEmpty ? "ㄧ" : "ㄚ"
}
default: break
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/TekkonTests/BasicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ final class TekkonTestsBasic: XCTestCase {
// Testing tool functions
XCTAssertEqual(Tekkon.restoreToneOneInPhona(target: "ㄉㄧㄠ"), "ㄉㄧㄠ1")
XCTAssertEqual(Tekkon.cnvPhonaToTextbookStyle(target: "ㄓㄜ˙"), "˙ㄓㄜ")
XCTAssertEqual(Tekkon.cnvPhonaToHanyuPinyin(targetJoined: "ㄍㄢˋ"), "gan4")
XCTAssertEqual(Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: "起(qi3)居(ju1)"), "起(qǐ)居(jū)")
XCTAssertEqual(Tekkon.cnvHanyuPinyinToPhona(targetJoined: "bian4-le5-tian1"), "ㄅㄧㄢˋ-ㄌㄜ˙-ㄊㄧㄢ")
// 測試這種情形:「如果傳入的字串不包含任何半形英數內容的話,那麼應該直接將傳入的字串原樣返回」。
XCTAssertEqual(Tekkon.cnvHanyuPinyinToPhona(targetJoined: "ㄅㄧㄢˋ-˙ㄌㄜ-ㄊㄧㄢ"), "ㄅㄧㄢˋ-˙ㄌㄜ-ㄊㄧㄢ")
Expand Down
Loading