Skip to content

Commit 1594ec7

Browse files
committed
Sync changes from Swift Tekkon v1.7.0. (#43)
1 parent 97ea5aa commit 1594ec7

13 files changed

Lines changed: 2536 additions & 7793 deletions

CHANGELOG_v1.7.0.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# TekkonNT v1.7.0 變更日誌
2+
3+
此版本移植了 Swift Tekkon v1.6.0 → v1.7.0 的所有變更。
4+
5+
## 重大變更(可能造成不相容)
6+
7+
### 公開 API 改以 `Rune` 為核心型別
8+
9+
- `Phonabet.AllowedConsonants` / `AllowedSemivowels` / `AllowedVowels` / `AllowedIntonations` / `AllowedPhonabets` 改為 `IReadOnlyList<Rune>`(原為 `String[]`
10+
- `Phonabet``Rune` 儲存值;新增 `Phonabet(Rune input)` 建構子
11+
- 多數內部處理與對外方法改以 `Rune` 參數(如 `ReceiveKey(Rune input)``ReceiveKeyFromPhonabet(Rune phonabet)`
12+
13+
### 屬性改為唯讀(對外)
14+
15+
`Composer` 多數屬性改為 `{ get; internal set; }`(對外唯讀,對內可寫):
16+
- `Consonant`(聲母)
17+
- `Semivowel`(介母)
18+
- `Vowel`(韻母)
19+
- `Intonation`(聲調)
20+
- `RomajiBuffer`(拼音組音區)
21+
- `Parser`(注音排列種類)
22+
23+
需改用對應方法變更狀態,無法直接從外部賦值。
24+
25+
### 動態注音排列處理方法改為私有
26+
27+
以下方法改為 `private`
28+
- `HandleETen26`
29+
- `HandleHsu`
30+
- `HandleStarlight`
31+
- `HandleDachen26`
32+
- `HandleAlvinLiu`
33+
34+
## 新功能與能力
35+
36+
### MandarinParser 擴充方法
37+
38+
新增以下靜態集合與方法:
39+
- `AllCases` - 列出所有可用的鍵盤排列
40+
- `AllPinyinCases` - 列出所有拼音排列
41+
- `AllDynamicZhuyinCases` - 列出所有動態注音排列
42+
- `AllStaticZhuyinCases` - 列出所有靜態注音排列
43+
- `IsPinyin()` - 判定指定排列是否為拼音模式
44+
- `IsDynamic()` - 判定指定排列是否為動態注音排列
45+
- `MapZhuyinPinyin()` - 取得與拼音排列對應的注音查表
46+
- `NameTag()` - 提供排列的識別名稱
47+
48+
### PinyinTrie 改進
49+
50+
- 修正 `DeductChoppedPinyinToZhuyin()` 的處理問題
51+
- 改進節點識別符的處理機制
52+
53+
## 測試結構重構
54+
55+
### 移除的測試檔案
56+
- `TekkonTests_Advanced.cs`(462KB 的硬編碼測試)
57+
58+
### 新增/重組的測試檔案
59+
- **`TekkonTests_Arrangements.cs`** - 資料驅動的鍵盤排列測試(2 個測試)
60+
- 靜態大千排列測試
61+
- 動態排列測試(自動測試所有 5 種動態排列)
62+
- **`TekkonTests_Basic.cs`** - 核心功能測試(8 個測試)
63+
- 包含 MandarinParser、Phonabet、組音、PinyinTrie 測試
64+
- **`TekkonTests_Pinyin.cs`** - 拼音專屬測試(6 個測試)
65+
- 涵蓋所有 6 種拼音變體
66+
67+
總計 16 個測試,全數通過 ✅
68+
69+
新的資料驅動方法將測試程式碼從 462KB 降至 32KB,同時維持完整的測試覆蓋率。
70+
71+
## 重構與維護性提升
72+
73+
### 型別系統改進
74+
- 核心型別由 `String``Rune`,降低字串切割與比較成本,避免多字元誤用
75+
- 屬性封裝確保內部狀態一致性
76+
77+
### 關鍵修復
78+
由於 C# 的 `struct` 特性,在屬性上呼叫方法會建立副本,因此將所有 `property.Clear()` 呼叫改為 `property = new Phonabet()` 賦值,確保正確修改原始值(共修復 15+ 處)。
79+
80+
## 建置與環境
81+
82+
- 目標框架從 .NET 6.0 升級至 .NET 8.0(.NET 6.0 已於 2024 年 11 月 12 日終止支援)
83+
- 其他平台需求不變
84+
85+
## 授權
86+
87+
授權維持 LGPL v3.0 or later。
88+
89+
## 升級指引摘要
90+
91+
### API 變更
92+
- 若外部程式碼使用到公開常數陣列,需改以 `IReadOnlyList<Rune>` 處理
93+
- 直接寫入 `Composer` 的欄位需改用對應的輸入路徑(`ReceiveKey` / `ReceiveSequence` 等)
94+
- 需要使用 `Rune` 型別處理單一注音符號,而非 `String`
95+
96+
### 相容性
97+
- 保留以 `String` 為參數的建構子與方法,以維持向後相容
98+
- 舊有 API 仍可使用,但建議遷移至新的 `Rune` 為基礎的 API
99+
100+
### 測試
101+
- 測試結構已完全重構為資料驅動模式
102+
- 若有自訂測試,建議參考新的測試檔案結構
103+
104+
## 已知問題
105+
106+
無。
107+
108+
## 貢獻者
109+
110+
- @copilot - 主要移植工作與測試重構
111+
- @ShikiSuen - 程式碼審查與後續修正
112+
113+
---
114+
115+
完整變更請參閱:https://github.com/vChewing/Tekkon/releases/tag/v1.7.0(Swift 版本)

Tekkon.Tests/Tekkon.Tests.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<PropertyGroup>
44
<Nullable>disable</Nullable>
55
<IsPackable>false</IsPackable>
6-
<ReleaseVersion>1.6.0</ReleaseVersion>
7-
<LangVersion>6</LangVersion>
8-
<TargetFrameworks>net46;net6.0</TargetFrameworks>
6+
<ReleaseVersion>1.7.0</ReleaseVersion>
7+
<LangVersion>10</LangVersion>
8+
<TargetFramework>net6.0</TargetFramework>
99
</PropertyGroup>
1010

1111
<ItemGroup>

Tekkon.Tests/TekkonTests_Advanced.cs

Lines changed: 0 additions & 7525 deletions
This file was deleted.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// (c) 2022 and onwards The vChewing Project (LGPL v3.0 License or later).
2+
// ====================
3+
// This code is released under the SPDX-License-Identifier: `LGPL-3.0-or-later`.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
9+
using NUnit.Framework;
10+
11+
namespace Tekkon.Tests {
12+
/// <summary>
13+
/// 資料驅動測試案例的輔助類別。
14+
/// </summary>
15+
public class SubTestCase {
16+
public MandarinParser Parser { get; }
17+
public string Typing { get; }
18+
public string Expected { get; }
19+
20+
public SubTestCase(MandarinParser parser, string typing, string expected) {
21+
// 略過以反引號標記的測試案例
22+
if (typing.StartsWith("`")) {
23+
Parser = parser;
24+
Typing = "";
25+
Expected = "";
26+
return;
27+
}
28+
Parser = parser;
29+
Typing = typing.Replace("_", " ");
30+
Expected = expected.Replace("_", " ");
31+
}
32+
33+
public bool Verify() {
34+
if (string.IsNullOrEmpty(Typing)) return true;
35+
36+
var composer = new Composer(arrange: Parser);
37+
string strResult = composer.CnvSequence(Typing);
38+
if (strResult == Expected) return true;
39+
40+
string parserTag = Parser.NameTag();
41+
string strError = $"MISMATCH ({parserTag}): \"{Typing}\" -> \"{strResult}\" != \"{Expected}\"";
42+
Console.WriteLine(strError);
43+
return false;
44+
}
45+
}
46+
47+
/// <summary>
48+
/// 靜態鍵盤排列測試(例如:大千排列)。
49+
/// </summary>
50+
public class TekkonTestsKeyboardArrangementsStatic {
51+
private void CheckEq(ref int counter, ref Composer composer, string strGivenSeq, string strExpected) {
52+
string strResult = composer.CnvSequence(strGivenSeq);
53+
if (strResult == strExpected) return;
54+
55+
string parserTag = composer.Parser.NameTag();
56+
string strError = $"MISMATCH ({parserTag}): \"{strGivenSeq}\" -> \"{strResult}\" != \"{strExpected}\"";
57+
Console.WriteLine(strError);
58+
counter++;
59+
}
60+
61+
[Test]
62+
public void TestQwertyDachenKeys() {
63+
// 測試大千傳統排列(QWERTY)
64+
var c = new Composer(arrange: MandarinParser.OfDachen);
65+
int counter = 0;
66+
CheckEq(ref counter, ref c, " ", " ");
67+
CheckEq(ref counter, ref c, "18 ", "ㄅㄚ ");
68+
CheckEq(ref counter, ref c, "m,4", "ㄩㄝˋ");
69+
CheckEq(ref counter, ref c, "5j/ ", "ㄓㄨㄥ ");
70+
CheckEq(ref counter, ref c, "fu.", "ㄑㄧㄡ");
71+
CheckEq(ref counter, ref c, "g0 ", "ㄕㄢ ");
72+
CheckEq(ref counter, ref c, "xup6", "ㄌㄧㄣˊ");
73+
CheckEq(ref counter, ref c, "xu;6", "ㄌㄧㄤˊ");
74+
CheckEq(ref counter, ref c, "z/", "ㄈㄥ");
75+
CheckEq(ref counter, ref c, "tjo ", "ㄔㄨㄟ ");
76+
CheckEq(ref counter, ref c, "284", "ㄉㄚˋ");
77+
CheckEq(ref counter, ref c, "2u4", "ㄉㄧˋ");
78+
CheckEq(ref counter, ref c, "hl3", "ㄘㄠˇ");
79+
CheckEq(ref counter, ref c, "5 ", "ㄓ ");
80+
CheckEq(ref counter, ref c, "193", "ㄅㄞˇ");
81+
Assert.AreEqual(0, counter);
82+
}
83+
}
84+
85+
/// <summary>
86+
/// 動態鍵盤排列測試(大千26鍵、倚天26鍵、許氏鍵盤、星光鍵盤、劉氏鍵盤)。
87+
/// </summary>
88+
public class TekkonTestsKeyboardArrangementsDynamic {
89+
[Test]
90+
public void TestDynamicKeyLayouts() {
91+
// 取得所有動態排列
92+
var dynamicParsers = MandarinParserExtensions.AllDynamicZhuyinCases.ToList();
93+
94+
foreach (var (parser, idxRaw) in dynamicParsers.Select((p, i) => (p, i))) {
95+
var cases = new List<SubTestCase>();
96+
Console.WriteLine($" -> [Tekkon] 準備動態鍵盤處理測試...");
97+
98+
// 解析測試資料
99+
var lines = TekkonTestData.DynamicLayoutTable.Split('\n', StringSplitOptions.RemoveEmptyEntries);
100+
bool isTitleLine = true;
101+
102+
foreach (var line in lines) {
103+
if (isTitleLine) {
104+
isTitleLine = false;
105+
continue;
106+
}
107+
108+
var cells = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
109+
if (cells.Length < 2) continue;
110+
111+
string expected = cells[0];
112+
int idx = idxRaw + 1;
113+
if (idx >= cells.Length) continue;
114+
115+
string typing = cells[idx];
116+
var testCase = new SubTestCase(parser, typing, expected);
117+
cases.Add(testCase);
118+
}
119+
120+
var startTime = DateTime.Now;
121+
Console.WriteLine($" -> [Tekkon][({parser.NameTag()})] 開始動態鍵盤處理測試...");
122+
123+
int failures = cases.Select(testCase => testCase.Verify() ? 0 : 1).Sum();
124+
125+
Assert.AreEqual(0, failures,
126+
$"[失敗] {parser.NameTag()} 處理失敗,共 {failures} 個錯誤結果。");
127+
128+
var elapsed = DateTime.Now - startTime;
129+
Console.WriteLine($" -> [Tekkon][({parser.NameTag()})] 測試完成,耗時 {elapsed.TotalSeconds:F4} 秒。");
130+
}
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)