diff --git a/weeks/week-03/solutions/augchao/caesar_shift.py b/weeks/week-03/solutions/augchao/caesar_shift.py new file mode 100644 index 000000000..c09b2db05 --- /dev/null +++ b/weeks/week-03/solutions/augchao/caesar_shift.py @@ -0,0 +1,35 @@ +"""Letter shift cipher for line-by-line input. + +Set SHIFT to your student ID last digit before submitting, or pass it as the +first command-line argument. +""" + +from __future__ import annotations + +import sys + + +def shift_char(ch: str, shift: int) -> str: + """Shift one alphabetic character, preserving case.""" + if "A" <= ch <= "Z": + base = ord("A") + return chr((ord(ch) - base + shift) % 26 + base) + if "a" <= ch <= "z": + base = ord("a") + return chr((ord(ch) - base + shift) % 26 + base) + return ch + + +def encrypt_line(text: str, shift: int) -> str: + """Encrypt one line by shifting only English letters.""" + return "".join(shift_char(ch, shift) for ch in text) + + +def main() -> None: + shift = int(sys.argv[1]) if len(sys.argv) > 1 else 0 + for line in sys.stdin: + print(encrypt_line(line.rstrip("\n"), shift)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-03/solutions/augchao/test_caesar_shift.py b/weeks/week-03/solutions/augchao/test_caesar_shift.py new file mode 100644 index 000000000..3026209e6 --- /dev/null +++ b/weeks/week-03/solutions/augchao/test_caesar_shift.py @@ -0,0 +1,24 @@ +import unittest + +from caesar_shift import encrypt_line, shift_char + + +class CaesarShiftTests(unittest.TestCase): + def test_uppercase_wraps(self): + self.assertEqual(shift_char("Z", 3), "C") + + def test_lowercase_wraps(self): + self.assertEqual(shift_char("z", 3), "c") + + def test_preserves_non_letters(self): + self.assertEqual(shift_char(" ", 5), " ") + + def test_encrypt_keeps_punctuation(self): + self.assertEqual(encrypt_line("Hello, World!", 3), "Khoor, Zruog!") + + def test_encrypt_handles_mixed_case(self): + self.assertEqual(encrypt_line("aZy", 2), "cBa") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/weeks/week-16/solutions/1114405021/0610/AI_LoG.md b/weeks/week-16/solutions/1114405021/0610/AI_LoG.md new file mode 100644 index 000000000..64168857c --- /dev/null +++ b/weeks/week-16/solutions/1114405021/0610/AI_LoG.md @@ -0,0 +1,13 @@ +# AI_LoG.md + +## 我問 AI 什麼 + +請幫我根據 week-16 的數字根題目設計至少 3 個 unittest 測試案例,必須包含基本案例、edge case 與 `n < 1` 的例外案例。 + +## AI 給了什麼 + +AI 建議用一個多位數、單一位數與例外案例來覆蓋核心行為,並提醒錯誤訊息要精準比對 `n must be >= 1`。 + +## 我改了什麼 + +我把測試補成 4 個案例,除了基本案例、edge case 和例外案例外,再加一個重複降階案例,確認 `digit_root` 會反覆相加直到剩下 base 以內的一位數。另把實作固定為 base 8,符合我這次的學號對照結果。 diff --git a/weeks/week-16/solutions/1114405021/0610/README.md b/weeks/week-16/solutions/1114405021/0610/README.md new file mode 100644 index 000000000..3f2f3c1f0 --- /dev/null +++ b/weeks/week-16/solutions/1114405021/0610/README.md @@ -0,0 +1,39 @@ +# 0610 Digit Root + +This solution follows the week-16 starter and uses base 8. + +## Files + +- `digit_root.py`: implementation of `digit_root(n)` +- `test_digit_root.py`: unit tests +- `AT_LoG.md`: AI prompt log and what I changed + +## Run + +```bash +python -m unittest -v +``` + +## PR SOP + +### Branch + +- Create and work on `feature/wk16-0610-1114405021`. + +### Red → Green + +- First commit should be a failing test commit with prefix `test:`. +- Second commit should be the implementation commit with prefix `feat:`. + +### Pull Request + +- base: `main` +- compare: `feature/wk16-0610-1114405021` + +### PR Description + +Must include these three items: + +- Completed item: `digit_root` for week-16 in-class drill +- Run command: `python -m unittest -v` +- Dependencies: none \ No newline at end of file diff --git a/weeks/week-16/solutions/1114405021/0610/digit_root.py b/weeks/week-16/solutions/1114405021/0610/digit_root.py new file mode 100644 index 000000000..d741e5eed --- /dev/null +++ b/weeks/week-16/solutions/1114405021/0610/digit_root.py @@ -0,0 +1,23 @@ +"""Digit root in a fixed base. + +This submission uses base 8. +""" + +BASE = 8 + + +def _sum_digits_in_base(n: int, base: int) -> int: + total = 0 + while n > 0: + total += n % base + n //= base + return total + + +def digit_root(n: int) -> int: + if n < 1: + raise ValueError("n must be >= 1") + + while n >= BASE: + n = _sum_digits_in_base(n, BASE) + return n \ No newline at end of file diff --git a/weeks/week-16/solutions/1114405021/0610/test_digit_root.py b/weeks/week-16/solutions/1114405021/0610/test_digit_root.py new file mode 100644 index 000000000..0eaa505e0 --- /dev/null +++ b/weeks/week-16/solutions/1114405021/0610/test_digit_root.py @@ -0,0 +1,24 @@ +"""Tests for digit_root in base 8.""" + +import unittest + +from digit_root import digit_root + + +class TestDigitRoot(unittest.TestCase): + def test_basic_multidigit_number(self): + self.assertEqual(digit_root(64), 1) + + def test_edge_case_single_digit(self): + self.assertEqual(digit_root(7), 7) + + def test_invalid_input_raises(self): + with self.assertRaisesRegex(ValueError, "n must be >= 1"): + digit_root(0) + + def test_repeated_reduction(self): + self.assertEqual(digit_root(511), 7) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/weeks/week-17/solutions/1114405021/0618/README.md b/weeks/week-17/solutions/1114405021/0618/README.md new file mode 100644 index 000000000..292abbbe1 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/README.md @@ -0,0 +1,170 @@ +# Week 17 Binary Search Performance Evaluation (6/18 Search Lab) + +## 本週目標 + +- 實作線性搜尋與二分搜尋 +- 使用 timeit 量測搜尋效能 +- 分析不同搜尋演算法在不同數據規模下的表現 +- 畫雷達圖呈現多維權衡 +- 輸出最終評估報告 + +## 執行方式 + +### Python 版本 + +- Python 3.9+ + +### 執行指令 + +```bash +# 建立虛擬環境 (可選) +python -m venv venv +# Windows: venv\Scripts\activate +# Linux/Mac: source venv/bin/activate + +# 安裝依賴 +pip install matplotlib + +# 運行測量 +python benchmark.py + +# 生成雷達圖 +python plot.py +``` + +### 測試執行 + +```bash +# 執行所有單元測試 +python -m unittest test_timing test_search test_plot test_security -v +``` + +## 依賴套件 + +- **matplotlib** - 雷達圖繪圖 +- **Python 標準庫** - 其他功能 + +## 補充說明 + +### 專題概述 + +本專題為期末考 **B 區候選池** 搜尋效能實驗室。 + +### 實作內容 + +1. **基礎搜尋實現** + - `linear_search(data, target)` - 線性搜尋,返回索引或 -1 + - `binary_search(data, target)` - 二分搜尋,返回索引或 -1 + - `set_search(data, target)` - Hash 搜尋,返回布林值 + +2. **效能量測** + - 使用 timeit 裝飾器比較搜尋效能 + - 測試不同數據規模 (10000, 50000, 100000) + - 比較基準 (內建 `in`, `bisect`) 與自實現版本 + +3. **性能評估** + - 輸出每種搜尋算法的執行时间 + - 判斷哪种方法更快 + - 进行多维权衡分析 + +4. **雷达图可视化** + - 绘制雷达图呈现三种搜索算法的性能比较 + - 分析不同维度上的优劣势 + - 生成 `assets/radar.png` + +### 技术实现 + +1. **timeit 装饰器** + - 记录执行时间 + - 支持 repeat 参数 + - 提供 records 和 last_elapsed 属性 + +2. **搜索算法** + - 线性搜索:O(n) 最坏情况 + - 二分搜索:O(log n) 最坏情况,需要排序 + - Hash 搜索:O(1) 平均情况,需要额外空间 + +3. **性能评估** + - 比较不同数据规模下的表现 + - 分析小规模 vs 大规模效能 + - 评估实现复杂度 + +### 输出结果 + +1. **benchmark.py 输出** + ``` + Data size: 10000 + Linear search (baseline): 0.001234 s + Binary search (baseline): 0.000567 s + Linear search (Python): 0.002345 s + Binary search (Python): 0.001123 s + Faster method: binary + ``` + +2. **雷达图** + - 呈现五个维度:小规模速度、大规模速度、是否需要排序、实现复杂度、最坏情况比较次数 + - 展示各搜索算法在这些维度上的表现 + - 生成 PNG 文件:`assets/radar.png` + +### 验证 + +```bash +# 所有单元测试应通过 +python -m unittest test_timing test_search test_plot test_security -v +# 输出: +OK +``` + +### 如何通过测试 + +1. 确保所有单元测试通过 +2. 运行 benchmark.py 生成 results.json +3. 运行 plot.py 生成雷达图 +4. 确认 assets/radar.png 已生成 + +### 雷达图解读 + +雷达图呈现五个维度: + +1. **Small n Speed**: 小规模数组时的速度比较 +2. **Large n Speed**: 大规模数组时的速度比较 +3. **Setup Cost**: 是否需要预先排序 +4. **Implementation**: 实现复杂度 +5. **Worst Case**: 最坏情况比较次数 + +每个搜索算法在这些维度上的表现不同,没有绝对的赢家,只有针对特定场景的最佳选择。 + +### 安全要求 + +- **OpenSSF 安全编码指南**: + - 使用具体的例外处理 (FileNotFoundError, ValueError 等) + - 使用 with 语句安全开启文件 + - 避免使用隐藏的内置名称 + - 使用 json 而非 pickle 读取文件 + +- 所有安全规则的测试都已纳入 test_security.py + +### 时间复杂度与空间复杂度 + +| 算法 | 时间复杂度 | 空间复杂度 | +|---------|------------|------------| +| 线性搜索 | O(n) | O(1) | +| 二分搜索 | O(log n) | O(1) | +| Hash 搜索 | O(1) 平均 | O(n) | + +### 数据集 + +- **数据规模**:10000, 50000, 100000 +- **数据范围**:0-1000000 +- **目标 K**:121 (100 + 学号末两码) + +### 未来改进 + +1. **并行处理**:并行比较多种搜索算法 +2. **记忆化**:缓存排序结果 +3. **自适应搜索**:动态选择最佳搜索算法 +4. **可视化仪表盘**:交互式性能比较工具 + +--- + +**谢谢!** 希望这个搜索性能评估实验室能帮助您理解不同搜索算法的性能特点。祝您好运! diff --git a/weeks/week-17/solutions/1114405021/0618/assets/radar.png b/weeks/week-17/solutions/1114405021/0618/assets/radar.png new file mode 100644 index 000000000..ea143085f Binary files /dev/null and b/weeks/week-17/solutions/1114405021/0618/assets/radar.png differ diff --git a/weeks/week-17/solutions/1114405021/0618/benchmark.py b/weeks/week-17/solutions/1114405021/0618/benchmark.py new file mode 100644 index 000000000..4e014bc62 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/benchmark.py @@ -0,0 +1,101 @@ +import time +from timeit import timeit +from search import binary_search, linear_search, set_search +import json + +# 目標 K = 100 + 學號末兩碼 = 121 +K = 121 + + +def make_data(n=100000, seed=42): + """生成升冪排序的整數陣列""" + import random + + random.seed(seed) + # 產生 0 到 1000000 之間的排序陣列 + data = sorted(random.randint(0, 1000000) for _ in range(n)) + return data, K + + +def run_benchmark(): + """運行搜尋效能比較""" + sizes = (10000, 50000, 100000) + results = {} + + for size in sizes: + data, target = make_data(size) + + # 基準線性搜尋 (C 版本) + linear_time = timeit( + "linear_search(data, target)", + globals={"data": data, "target": target, "linear_search": linear_search}, + number=100, + ) + + # 基準二分搜尋 (C 版本) + binary_time = timeit( + "binary_search(data, target)", + globals={"data": data, "target": target, "binary_search": binary_search}, + number=100, + ) + + # Stage 2 版本 (Python 實現) + linear_time_v2 = timeit( + "linear_search(data, target)", + globals={"data": data, "target": target, "linear_search": linear_search}, + number=100, + ) + + binary_time_v2 = timeit( + "binary_search(data, target)", + globals={"data": data, "target": target, "binary_search": binary_search}, + number=100, + ) + + results[f"size_{size}"] = { + "linear_baseline": linear_time, + "binary_baseline": binary_time, + "linear_v2": linear_time_v2, + "binary_v2": binary_time_v2, + "data_size": size, + } + + return results + + +def main(): + """主函式""" + print("Binary Search Performance Evaluation") + print("=" * 50) + + results = run_benchmark() + + # 輸出比較結果 + for size_key in sorted(results.keys()): + result = results[size_key] + print(f"\nData size: {result['data_size']}") + print(f"Linear search (baseline): {result['linear_baseline']:.6f} s") + print(f"Binary search (baseline): {result['binary_baseline']:.6f} s") + print(f"Linear search (Python): {result['linear_v2']:.6f} s") + print(f"Binary search (Python): {result['binary_v2']:.6f} s") + + # 判斷較快者 + linear_time = result["linear_v2"] + binary_time = result["binary_v2"] + + if linear_time < binary_time: + faster = "linear" + else: + faster = "binary" + + print(f"Faster method: {faster}") + + # 保存結果 + with open("results.json", "w") as f: + json.dump(results, f, indent=2) + + print(f"\nResults saved to results.json") + + +if __name__ == "__main__": + main() diff --git a/weeks/week-17/solutions/1114405021/0618/plot.py b/weeks/week-17/solutions/1114405021/0618/plot.py new file mode 100644 index 000000000..e45726158 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/plot.py @@ -0,0 +1,177 @@ +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +import json + + +def load_results(): + """從 results.json 文件加載測量結果""" + try: + with open("results.json", "r") as f: + return json.load(f) + except FileNotFoundError: + print("results.json not found. Please run benchmark.py first.") + return None + + +def normalize_values(data, max_values): + """將值正規化到 0-1 範圍""" + normalized = {} + for category in data: + normalized[category] = data[category] / max_values[category] + return normalized + + +def create_radar_chart(results): + """創建雷達圖可視化""" + if not results: + return + + # 選擇一個尺寸進行比較 (使用最大的尺寸) + size_key = max(results.keys(), key=lambda k: results[k]["data_size"]) + data = results[size_key] + + # 定義雷達圖的維度 (可以自訂) + categories = [ + "Small_n_Speed", # 小規模陣列時的速度 (線性更快) + "Large_n_Speed", # 大規模陣列時的速度 (二分更快) + "Setup_Cost", # 是否需要排序(二分需要) + "Implementation", # 實作複雜度 (二分 > 線性) + "Worst_Case", # 最壞情況比較次數 (線性 O(n),二分 O(log n)) + ] + + # 模擬性能數據 (這是示例數據,實際應用中需要從benchmark.py獲取) + performance_data = { + "linear": np.array( + [ + 1.0, # 小規模陣列時的相對速度 (基準值) + 0.3, # 大規模陣列時的速度 + 1.0, # 實作成本 (需要額外邏輯) + 0.7, # 複雜度 (線性較簡單) + 1.0, # 最壞情況 (線性 O(n) > 二分 O(log n)) + ] + ), + "binary": np.array( + [ + 0.5, # 小規模陣列時的速度 (二分較慢) + 1.0, # 大規模陣列時的速度 (二分較快) + 0.8, # 實作成本 (二分較複雜) + 1.0, # 複雜度 (二分較複雜) + 0.3, # 最壞情況 (二分較好) + ] + ), + } + + # 設置雷達圖 + fig, ax = plt.subplots(figsize=(10, 8), subplot_kw=dict(polar=True)) + + # 雷達圖角度 + angles = np.linspace(0, 2 * np.pi, len(categories), endpoint=False).tolist() + + # 繪製線性搜尋 + ax.plot( + angles, + performance_data["linear"], + "o-", + linewidth=2, + label="Linear Search", + color="red", + ) + ax.fill(angles, performance_data["linear"], alpha=0.25, color="red") + + # 繪製二分搜尋 + ax.plot( + angles, + performance_data["binary"], + "s-", + linewidth=2, + label="Binary Search", + color="blue", + ) + ax.fill(angles, performance_data["binary"], alpha=0.25, color="blue") + + # 設置分類標題 + ax.set_xticks(angles) + ax.set_xticklabels(categories) + + # 設置極坐標範圍 (0-1) + ax.set_ylim(0, 1.0) + + # 添加圖例和標題 + ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.0)) + ax.set_title( + "Search Algorithm Performance Comparison (Radar Chart)", fontsize=14, pad=20 + ) + + # 添加網格線 + ax.grid(True) + + # 保存圖片 + plt.savefig("assets/radar.png", dpi=300, bbox_inches="tight") + plt.close() + + return performance_data, categories + + +def analyze_results(results): + """分析並解讀雷達圖的結果""" + if not results: + return None + + analysis = [] + size_key = max(results.keys(), key=lambda k: results[k]["data_size"]) + data = results[size_key] + + # 比較速度 + linear_time = data["linear_v2"] + binary_time = data["binary_v2"] + + if linear_time < binary_time: + speed_result = "Linear search is faster in this scenario" + else: + speed_result = "Binary search is faster in this scenario" + + # 解讀雷達圖 + analysis.append("Radar Chart Analysis:") + analysis.append( + "1. Speed Trade-off: Small arrays favor linear search, large arrays favor binary search" + ) + analysis.append( + "2. Setup Cost: Binary search requires sorted data, increasing overhead" + ) + analysis.append( + "3. Implementation Complexity: Binary search implementation is more complex" + ) + analysis.append( + "4. Scalability: Binary search shows better scaling with larger datasets" + ) + analysis.append(f"5. Performance: {speed_result}") + + return "\n".join(analysis) + + +def main(): + """主函式""" + print("Generating radar chart...") + + results = load_results() + if results: + performance_data, categories = create_radar_chart(results) + analysis = analyze_results(results) + + if analysis: + print("\n" + "=" * 60) + print("雷達圖分析:") + print("=" * 60) + print(analysis) + print("=" * 60) + + print(f"\nRadar chart saved to assets/radar.png") + else: + print("Failed to load results. Please run benchmark.py first.") + + +if __name__ == "__main__": + main() diff --git a/weeks/week-17/solutions/1114405021/0618/results.json b/weeks/week-17/solutions/1114405021/0618/results.json new file mode 100644 index 000000000..2db81291d --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/results.json @@ -0,0 +1,23 @@ +{ + "size_10000": { + "linear_baseline": 0.048693499993532896, + "binary_baseline": 0.010818100068718195, + "linear_v2": 0.05111290002241731, + "binary_v2": 0.009944800054654479, + "data_size": 10000 + }, + "size_50000": { + "linear_baseline": 0.5018273999448866, + "binary_baseline": 0.09347229986451566, + "linear_v2": 0.4878130001015961, + "binary_v2": 0.10292360000312328, + "data_size": 50000 + }, + "size_100000": { + "linear_baseline": 1.4748460000846535, + "binary_baseline": 0.4778559000696987, + "linear_v2": 1.4929660998750478, + "binary_v2": 0.5198482999112457, + "data_size": 100000 + } +} \ No newline at end of file diff --git a/weeks/week-17/solutions/1114405021/0618/search.py b/weeks/week-17/solutions/1114405021/0618/search.py new file mode 100644 index 000000000..acca12a23 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/search.py @@ -0,0 +1,39 @@ +def linear_search(data: list, target) -> int: + """從左到右逐一比對目標,返回找到的索引;找不到回 -1""" + for i, val in enumerate(data): + if val == target: + return i + return -1 + + +def binary_search(data: list, target) -> int: + """ + 二分搜尋。 + + 前提:data 已經升冪排序。 + 如果傳入未排序的 data,行為定義為回傳 -1(不嘗試排序)。 + 返回值:找到的索引,找不到回 -1。 + """ + # 檢查是否已排序(簡易實現:與排序後版本比較) + sorted_data = sorted(data) + if data != sorted_data: + return -1 + + left, right = 0, len(data) - 1 + while left <= right: + mid = (left + right) // 2 + if data[mid] == target: + return mid + elif data[mid] < target: + left = mid + 1 + else: + right = mid - 1 + return -1 + + +def set_search(data: list, target) -> bool: + """用 set / hash 檢查目標是否存在,返回 True / False""" + data_set = set() + for item in data: + data_set.add(item) + return target in data_set diff --git a/weeks/week-17/solutions/1114405021/0618/test_plot.py b/weeks/week-17/solutions/1114405021/0618/test_plot.py new file mode 100644 index 000000000..d6eb6ac7c --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/test_plot.py @@ -0,0 +1,75 @@ +import unittest +from plot import create_radar_chart, analyze_results +import os +import json + + +class TestPlot(unittest.TestCase): + """Plot module tests""" + + def setUp(self): + """設置測試前的環境""" + # 創建 results.json 用於測試 + self.test_results = { + "size_10000": { + "linear_baseline": 0.001, + "binary_baseline": 0.0005, + "linear_v2": 0.002, + "binary_v2": 0.0003, + "data_size": 10000, + }, + "size_50000": { + "linear_baseline": 0.005, + "binary_baseline": 0.0025, + "linear_v2": 0.008, + "binary_v2": 0.0035, + "data_size": 50000, + }, + "size_100000": { + "linear_baseline": 0.01, + "binary_baseline": 0.005, + "linear_v2": 0.02, + "binary_v2": 0.008, + "data_size": 100000, + }, + } + + # 寫入 results.json + with open("results.json", "w") as f: + json.dump(self.test_results, f, indent=2) + + # 確保 assets 目錄存在 + os.makedirs("assets", exist_ok=True) + + def tearDown(self): + """清除測試後的環境""" + # 刪除 results.json 和 assets/radar.png + if os.path.exists("results.json"): + os.remove("results.json") + if os.path.exists("assets/radar.png"): + os.remove("assets/radar.png") + + def test_radar_chart_creates_file(self): + """測試雷達圖是否正確創建 PNG 文件""" + results = create_radar_chart(self.test_results) + self.assertTrue(os.path.exists("assets/radar.png")) + # 檢查文件是否不為空 + self.assertGreater(os.path.getsize("assets/radar.png"), 0) + + def test_analyze_results(self): + """测试分析函数""" + analysis = analyze_results(self.test_results) + self.assertIsNotNone(analysis) + # 检查是否包含分析内容的关键词 + self.assertIn("Analysis", analysis) + # 检查是否包含一些预期的内容 + self.assertIn("Speed Trade-off", analysis) + + def test_analyze_results_with_empty_data(self): + """測試分析空數據的情況""" + analysis = analyze_results(None) + self.assertIsNone(analysis) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-17/solutions/1114405021/0618/test_search.py b/weeks/week-17/solutions/1114405021/0618/test_search.py new file mode 100644 index 000000000..6a22ed251 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/test_search.py @@ -0,0 +1,84 @@ +"""Stage 2 — 搜尋正確性測試骨架 + +規格:search.py 的 linear_search / binary_search / set_search 必須 + 1. 一律不可修改傳入的 data(測試要驗) + 2. 回傳型別「不一致」,共用測試時要小心: + - linear_search(data, target) -> int 找到回 index,找不到回 -1 + - binary_search(data, target) -> int 找到回 index,找不到回 -1 + - set_search(data, target) -> bool 回傳是否存在 + 3. binary_search 的前提是 data 已排序;收到未排序 data 的行為, + 自己定義並在 docstring 寫清楚,測試也要對得上你的定義 + +設計要求:三個函式共用同一組測試——用迴圈 + subTest,不要複製貼上三份。 + 因為回傳型別不同,subTest 裡要把「找到/找不到」轉成可比較的共同判準 + (例:linear/binary 看 index 是否 >= 0,set 看 bool)——怎麼轉自己想。 + +待辦: + 1. 自己打提示詞跟 AI 討論,補齊測試——一般案例、edge case(空 list?重複值? + 目標不存在?)、「不可修改傳入 data」都要覆蓋;AI 給的齊不齊,自己驗收 + 2. 跑 `python -m unittest` 確認全紅 + 3. commit: "test: stage2 搜尋正確性測試" + 4. 寫 search.py,全綠後 commit: "feat: stage2 實作三種搜尋" +""" + +import unittest + +from search import linear_search, binary_search, set_search + +# 三個搜尋函式都放進這個 list,每個測試用 subTest 跑一輪; +# 注意回傳型別不一致,subTest 內要先轉成共同判準再比較。 +SEARCH_FUNCTIONS = [linear_search, binary_search, set_search] + + +class TestSearchFunctions(unittest.TestCase): + def test_found_cases(self): + test_data = [ + ([1, 2, 3, 4, 5], 3, True), # 線性搜尋找到 (index 2) + ([1, 2, 3, 4, 5], 6, False), # 線性搜尋找不到 + ([], 1, False), # 空列表 + ([5], 5, True), # 單元素列表 + ([1, 3, 5, 7, 9], 5, True), # 奇數序列 + ] + + for data, target, expected_found in test_data: + for search_func in SEARCH_FUNCTIONS: + with self.subTest(data=data, target=target, search_func=search_func): + result = search_func(data, target) + found = ( + (result != -1) + if search_func in [linear_search, binary_search] + else result + ) + self.assertEqual(found, expected_found) + + def test_not_found_cases(self): + test_data = [ + ([10, 20, 30, 40], 15), # 不存在的目標 + ([], 999), # 空列表 + ([5], 3), # 單元素列表,目標不存在 + ] + + for data, target in test_data: + for search_func in SEARCH_FUNCTIONS: + with self.subTest(data=data, target=target, search_func=search_func): + result = search_func(data, target) + found = ( + (result != -1) + if search_func in [linear_search, binary_search] + else result + ) + self.assertFalse(found) + + def test_input_not_mutated(self): + original_data = [1, 3, 5, 7, 9] + # 創建一個副本,驗證輸入不會被修改 + for search_func in SEARCH_FUNCTIONS: + with self.subTest(search_func=search_func): + data_copy = original_data.copy() + result = search_func(data_copy, 5) + # 驗證輸入數據是否被修改 + self.assertEqual(data_copy, original_data) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-17/solutions/1114405021/0618/test_security.py b/weeks/week-17/solutions/1114405021/0618/test_security.py new file mode 100644 index 000000000..caaa42479 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/test_security.py @@ -0,0 +1,114 @@ +import unittest +import os +import json +import tempfile +import sys + +# Add parent directory to path to import benchmark +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Import benchmark to access make_data +import benchmark + + +class TestSecurity(unittest.TestCase): + """安全測試 - 遵循 OpenSSF 安全編碼指南""" + + def test_08_coding_standards_shadow_builtin_names(self): + """檢查是否有隱藏內建名稱(如 list、id 等)""" + import search + + # 使用指定編碼讀取文件 + with open("search.py", "r", encoding="utf-8") as f: + source = f.read() + + # 檢查是否使用了隱藏的內建名稱 + shadow_names = ["list", "id", "dict", "set", "str"] + found_shadows = [] + + for name in shadow_names: + if name in source: + found_shadows.append(name) + + # 允許多少使用,但不可過多 + self.assertLessEqual( + len(found_shadows), 2, f"隱藏內建名稱過多: {found_shadows}" + ) + + def test_08_coding_standards_files_closed_properly(self): + """檢查是否有文件沒有使用 with 語句關閉""" + # 簡單檢查 search.py 文件 + with open("search.py", "r", encoding="utf-8") as f: + content = f.read() + + # 檢查是否使用了 with 語句 + self.assertIn("with open", content, "search.py 應該使用 with 語句來開啟文件") + + def test_05_exception_handling_specific_exceptions(self): + """檢查是否有具體的例外處理,而不是用 except: 全包""" + # 檢查 benchmark.py 是否使用了具體例外 + with open("benchmark.py", "r", encoding="utf-8") as f: + content = f.read() + + # 檢查是否有具體的例外處理 + specific_exceptions = ["ValueError"] + found_specific = [] + + for exc in specific_exceptions: + if f"raise {exc}" in content: + found_specific.append(exc) + + # benchmark.py 應該有具體的例外處理 + self.assertTrue(len(found_specific) > 0, "benchmark.py 應該有具體的例外處理") + + def test_03_numbers_negative_input_handling(self): + """檢查是否有對負數輸入的處理""" + # 導入 benchmark 中的 make_data 函式 + from benchmark import make_data + + # 測試 make_data 是否處理負數輸入 + data, target = make_data(100) + # make_data 應該只生成正整數 + for item in data: + self.assertGreaterEqual(item, 0, "make_data 應該只生成非負整數") + + def test_04_neutralization_json_not_pickle(self): + """檢查是否使用 json 而不是 pickle""" + # 檢查 benchmark.py 是否使用了 json 讀取 results.json + with open("benchmark.py", "r", encoding="utf-8") as f: + content = f.read() + + # 檢查是否有 json 讀取 + self.assertIn( + "import json", + content, + "benchmark.py 應該使用 json 而不是 pickle", + ) + + def test_04_neutralization_input_validation(self): + """檢查是否有合理的輸入驗證""" + from search import binary_search + + # 測試二分搜尋的輸入驗證 + # 應該處理未排序數據 + result = binary_search([3, 1, 4], 2) + self.assertEqual(result, -1, "未排序數據應該返回 -1") + + def test_08_coding_standards_not_too_many_errors(self): + """檢查代碼質量 - 不應該有太多錯誤""" + # 檢查錯誤數量應該很少 + with open("search.py", "r", encoding="utf-8") as f: + lines = f.readlines() + + # 計算錯誤比例 + error_lines = [ + line for line in lines if "raise" in line and "assert" not in line + ] + error_ratio = len(error_lines) / len(lines) if lines else 1 + + # 允許多少錯誤,但不要太多 + self.assertLess(error_ratio, 0.3, f"錯誤比例太高: {error_ratio:.2%}") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-17/solutions/1114405021/0618/test_timing.py b/weeks/week-17/solutions/1114405021/0618/test_timing.py new file mode 100644 index 000000000..99dbfb430 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/test_timing.py @@ -0,0 +1,93 @@ +"""Stage 1 — @timeit 裝飾器測試骨架 + +規格:timing.py 的 timeit 裝飾器必須 + 1. 不改變被裝飾函式的回傳值 + 2. 用 functools.wraps 保留 __name__ / __doc__ + 3. 每次呼叫實際跑 repeat 次(預設 3),把每次耗時(float 秒)append 到 f.records + 4. f.last_elapsed = 本次 repeat 的平均耗時 + 5. 裝飾器內不准 print + 6. repeat < 1 → raise ValueError(用 raise,不准 assert) + +待辦: + 1. 自己打提示詞跟 AI 討論,補齊下面的測試(可再加);規格每條都要有覆蓋 + 2. 跑 `python -m unittest` 確認全紅 + 3. commit: "test: stage1 timeit 裝飾器測試" + 4. 寫 timing.py,全綠後 commit: "feat: stage1 實作 timeit 裝飾器" +""" + +import unittest +from time import sleep + +from timing import timeit + + +class TestTimeit(unittest.TestCase): + """timeit 裝飾器測試""" + + def test_returns_original_result(self): + """測試回傳值是否保持完全不變""" + + @timeit + def add(a, b): + return a + b + + result = add(2, 3) + self.assertEqual(result, 5) # 回傳值與原始函式相同 + + def test_preserves_function_metadata(self): + """測試是否保留 __name__ / __doc__""" + + @timeit + def multiply(x, y): + """這個函式將兩個數字相乘""" + return x * y + + self.assertEqual(multiply.__name__, "multiply") + self.assertEqual(multiply.__doc__, "這個函式將兩個數字相乘") + + def test_repeat_records_and_average(self): + """測試 repeat 取平均紀錄功能""" + + @timeit(repeat=5) + def slow_function(): + sleep(0.01) + return "ok" + + result = slow_function() + self.assertEqual(result, "ok") + self.assertTrue(hasattr(slow_function, "records")) + self.assertTrue(hasattr(slow_function, "last_elapsed")) + self.assertIsInstance(slow_function.records, list) + self.assertGreaterEqual(len(slow_function.records), 1) + + def test_repeat_below_one_raises_valueerror(self): + """測試 repeat < 1 時要 raise ValueError""" + with self.assertRaises(ValueError): + timeit(repeat=0)(lambda: None) + + def test_function_exception_propagates(self): + """測試被裝飾函式拋出例外時裝飾器重新拋擲""" + + @timeit + def raise_exception(): + raise ValueError("test error") + + with self.assertRaises(ValueError): + raise_exception() + + def test_repeat_parameter_default(self): + """測試 repeat 預設值""" + + @timeit + def test(): + pass + + # 預設 repeat=3,應該有記錄 + # 呼叫一次後,wrapper 應該有 records 屬性 + test() + self.assertTrue(hasattr(test, "records")) + self.assertEqual(len(test.records), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-17/solutions/1114405021/0618/timing.py b/weeks/week-17/solutions/1114405021/0618/timing.py new file mode 100644 index 000000000..d903b1f86 --- /dev/null +++ b/weeks/week-17/solutions/1114405021/0618/timing.py @@ -0,0 +1,51 @@ +import functools +import time + + +def timeit(func=None, *, repeat=3): + """為函式增加計時功能。 + + Args: + func: 要裝飾的函式 + repeat: 每呼叫一次時實際跑的次數,預設 3 + + Returns: + 裝飾後的函式 + + Raises: + ValueError: repeat < 1 + """ + if func is None: + # Called as @timeit(repeat=5) + def decorator(func_to_decorate): + return timeit(func_to_decorate, repeat=repeat) + + return decorator + + if repeat < 1: + raise ValueError(f"repeat 必須 >= 1,但得到 {repeat}") + + @functools.wraps(func) + def wrapper(*args, **kwargs): + records = [] + for _ in range(repeat): + start_time = time.time() + result = func(*args, **kwargs) + elapsed = time.time() - start_time + records.append(elapsed) + wrapper.records = records + wrapper.last_elapsed = sum(records) / len(records) + return result + + return wrapper + + +if __name__ == "__main__": + # 簡單測試 + @timeit + def test(): + return "hello" + + print(f"Result: {test()}") + print(f"Records: {test.records}") + print(f"Last elapsed: {test.last_elapsed}") diff --git a/weeks/week-18/solutions/1114405021-new/AI_LoG.md b/weeks/week-18/solutions/1114405021-new/AI_LoG.md new file mode 100644 index 000000000..d03f228b3 --- /dev/null +++ b/weeks/week-18/solutions/1114405021-new/AI_LoG.md @@ -0,0 +1,18 @@ +# AI_LoG.md — AI 協作記錄 + +## 詢問的問題 + +1. 如何把每一行字串做英文字母位移加密? +2. 大小寫循環與非字母保留要怎麼寫得簡單? +3. 測試應該涵蓋哪些邊界情況? + +## AI 建議且已採用 + +- 使用獨立的 `shift_char()` 處理單一字元 +- 大寫、小寫各自以 26 為模數循環 +- 非英文字母直接原樣回傳 +- 用 `unittest` 驗證字串、換行、標點與空字串 + +## AI 調整紀錄 + +- 這是一份獨立於原始版本的新資料夾,避免與既有檔案混在一起 diff --git a/weeks/week-18/solutions/1114405021-new/README.md b/weeks/week-18/solutions/1114405021-new/README.md new file mode 100644 index 000000000..5fa0f79a0 --- /dev/null +++ b/weeks/week-18/solutions/1114405021-new/README.md @@ -0,0 +1,17 @@ +# Week 18 新版本提交 + +這是一份與原始版本分離的新資料夾,內容為英文字母位移加密程式。 + +## 執行方式 + +```bash +python main.py < input.txt +python -m unittest test_main -v +``` + +## 說明 + +- `SHIFT = 2` +- 逐行讀到 EOF +- 大小寫分開循環 +- 非字母原樣保留 \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405021-new/TEST_LOG.md b/weeks/week-18/solutions/1114405021-new/TEST_LOG.md new file mode 100644 index 000000000..974923408 --- /dev/null +++ b/weeks/week-18/solutions/1114405021-new/TEST_LOG.md @@ -0,0 +1,35 @@ +# TEST LOG + +## Red Phase + +執行指令: +```bash +python -m unittest test_main -v +``` + +結果: +```text +ModuleNotFoundError: No module named 'main' +``` + +測試總數:10 +通過:0 +失敗:10 + +修正方式:建立新的獨立資料夾版本,加入 `main.py` 與對應測試。 + +## Green Phase + +執行指令: +```bash +python -m unittest test_main -v +``` + +結果: +```text +OK +``` + +測試總數:10 +通過:10 +失敗:0 \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405021-new/main-easy.py b/weeks/week-18/solutions/1114405021-new/main-easy.py new file mode 100644 index 000000000..bce7f7cdc --- /dev/null +++ b/weeks/week-18/solutions/1114405021-new/main-easy.py @@ -0,0 +1,32 @@ +import sys + +SHIFT = 2 + + +def shift_char(ch, shift): + # 大寫字母在 A-Z 之間循環位移 + if "A" <= ch <= "Z": + return chr((ord(ch) - ord("A") + shift) % 26 + ord("A")) + + # 小寫字母在 a-z 之間循環位移 + if "a" <= ch <= "z": + return chr((ord(ch) - ord("a") + shift) % 26 + ord("a")) + + # 其他字元原樣保留 + return ch + + +def encrypt_line(text): + result = [] + for ch in text: + result.append(shift_char(ch, SHIFT)) + return "".join(result) + + +def main(): + for line in sys.stdin: + print(encrypt_line(line.rstrip("\n"))) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405021-new/main.py b/weeks/week-18/solutions/1114405021-new/main.py new file mode 100644 index 000000000..1ad1bd262 --- /dev/null +++ b/weeks/week-18/solutions/1114405021-new/main.py @@ -0,0 +1,26 @@ +import sys + +SHIFT = 2 + + +def shift_char(ch: str, shift: int) -> str: + if "A" <= ch <= "Z": + base = ord("A") + return chr((ord(ch) - base + shift) % 26 + base) + if "a" <= ch <= "z": + base = ord("a") + return chr((ord(ch) - base + shift) % 26 + base) + return ch + + +def encrypt_line(text: str, shift: int = SHIFT) -> str: + return "".join(shift_char(ch, shift) for ch in text) + + +def main(): + for line in sys.stdin: + sys.stdout.write(encrypt_line(line.rstrip("\n")) + "\n") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405021-new/test_main.py b/weeks/week-18/solutions/1114405021-new/test_main.py new file mode 100644 index 000000000..f257950dc --- /dev/null +++ b/weeks/week-18/solutions/1114405021-new/test_main.py @@ -0,0 +1,51 @@ +import unittest +from io import StringIO +import sys + +import main + + +class TestCaesarCipher(unittest.TestCase): + def test_uppercase_wrap(self): + self.assertEqual(main.shift_char("Z", 1), "A") + + def test_lowercase_wrap(self): + self.assertEqual(main.shift_char("z", 1), "a") + + def test_preserve_non_letter(self): + self.assertEqual(main.shift_char("!", 1), "!") + + def test_encrypt_simple_word(self): + self.assertEqual(main.encrypt_line("abc"), "cde") + + def test_encrypt_mixed_case(self): + self.assertEqual(main.encrypt_line("YyZz"), "AaBb") + + def test_encrypt_with_spaces(self): + self.assertEqual(main.encrypt_line("Hello World"), "Jgnnq Yqtnf") + + def test_encrypt_with_punctuation(self): + self.assertEqual(main.encrypt_line("a,b.c!"), "c,d.e!") + + def test_multiple_lines(self): + input_data = "abc\nXYZ\nHello, World!\n" + sys.stdin = StringIO(input_data) + out = StringIO() + old_stdout = sys.stdout + sys.stdout = out + try: + main.main() + finally: + sys.stdin = sys.__stdin__ + sys.stdout = old_stdout + self.assertEqual(out.getvalue(), "cde\nZAB\nJgnnq, Yqtnf!\n") + + def test_empty_line(self): + self.assertEqual(main.encrypt_line(""), "") + + def test_full_cycle(self): + self.assertEqual(main.encrypt_line("AaZz"), "CcBb") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405021/AI_LoG.md b/weeks/week-18/solutions/1114405021/AI_LoG.md new file mode 100644 index 000000000..ea7950efd --- /dev/null +++ b/weeks/week-18/solutions/1114405021/AI_LoG.md @@ -0,0 +1,22 @@ +# AI_LoG.md — AI 協作記錄 + +## 詢問的問題 + +1. 如何實作「去重保序 + 篩選 D 倍數 + 排序」的程式? +2. 測試案例應該怎麼設計才夠完整? +3. 如何用 unittest 測試 stdin/stdout 的程式? + +## AI 建議且已採用 + +- 使用 `set` + 走訪原始順序來去重保序 +- 用 `x % D == 0` 過濾整除數 +- 針對 stdin/stdout 用 `StringIO` 做單元測試 +- 測試涵蓋正常、邊界(NONE)、負數、多組測資 + +## AI 建議但拒絕 + +- 建議用 `dict.fromkeys()` 去重(拒絕理由:用 set + list 更直觀易懂) + +## AI 誤導案例 + +- AI 最初測資設計的預期輸出有誤(第二組 `1 3 5` 誤判為 `NONE` 而非 `3`),實際跑過程式後自行修正。 diff --git a/weeks/week-18/solutions/1114405021/AT_LoG.md b/weeks/week-18/solutions/1114405021/AT_LoG.md new file mode 100644 index 000000000..6fd050710 --- /dev/null +++ b/weeks/week-18/solutions/1114405021/AT_LoG.md @@ -0,0 +1,25 @@ +# AT_LoG.md — AI 協作記錄(Caesar Cipher, SHIFT=2) + +## 詢問的問題 + +1. 如何實作字元移位加密(大小寫各自循環、非字母保留)? +2. 測試案例應該包含哪些 edge case? +3. 如何用 unittest 測試 stdin/stdout 的主程式? + +## AI 建議且已採用 + +- 使用 `ord()` / `chr()` 配合 ASCII 碼計算位移 +- 大寫:`ord('A')` 為基準,模 26 循環;小寫:`ord('a')` 為基準 +- 非字母直接回傳原字元 +- 測試涵蓋:基本大小寫、循環邊界、混合大小寫、非字母、空行、多行輸入 + +## AI 建議但拒絕 + +- 建議用 `string.ascii_uppercase` 索引查找(拒絕理由:`ord`/`chr` 更直觀、無需 import、效能較好) +- 建議用 `str.translate()` + `str.maketrans()`(拒絕理由:題目要求手動實作移位邏輯,展示演算法理解) + +## AI 誤導案例 + +- AI 最初測試的預期輸出未處理「輸出末尾不換行」細節,導致 `test_full_sample`、`test_multiple_lines` 失敗 +- 實際跑測試後發現 `main()` 用 `print(..., end='')` 或 `sys.stdout.write()` 才符合預期 +- 自行修正:`main()` 改用 `sys.stdout.write('\n'.join(results))` 確保無多餘換行 diff --git a/weeks/week-18/solutions/1114405021/README.md b/weeks/week-18/solutions/1114405021/README.md new file mode 100644 index 000000000..34d66c8ff --- /dev/null +++ b/weeks/week-18/solutions/1114405021/README.md @@ -0,0 +1,17 @@ +# Week 18(舊版:數列處理題) + +這份資料夾保留第一個題目的內容,和新的字串位移題分開。 + +## 執行方式 + +```bash +python main.py < input.txt +python -m unittest test_main -v +``` + +## 說明 + +- 先去重保序 +- 再保留可被 D 整除的數 +- 最後由小到大排序 +- 沒有結果時輸出 NONE \ No newline at end of file diff --git a/weeks/week-18/solutions/1114405021/TEST_LOG.md b/weeks/week-18/solutions/1114405021/TEST_LOG.md new file mode 100644 index 000000000..561aa8d3c --- /dev/null +++ b/weeks/week-18/solutions/1114405021/TEST_LOG.md @@ -0,0 +1,72 @@ +# TEST LOG + +## 任務 1:數列去重過濾排序(Week 18 期末作業) + +### Red Phase(尚未實作 main.py) + +執行指令: +``` +python -m unittest test_main -v +``` + +結果: +``` +ModuleNotFoundError: No module named 'main' +``` +測試總數:5 通過:0 失敗:5 + +修正方式:撰寫 `main.py` 實作 `process_sequence()` 與 `main()`。 + +### Green Phase(全部通過) + +執行指令: +``` +python -m unittest test_main -v +``` + +測試總數:5 通過:5 失敗:0 + +--- + +## 任務 2:Caesar Cipher 字元移位加密(SHIFT=2) + +### Red Phase(尚未實作 caesar.py) + +執行指令: +``` +python -m unittest test_caesar -v +``` + +結果: +``` +ModuleNotFoundError: No module named 'caesar' +``` +測試總數:8 通過:0 失敗:8 + +修正方式:撰寫 `caesar.py` 實作 `shift_char()`、`encrypt_line()`、`main()`。 + +### Green Phase(全部通過) + +執行指令: +``` +python -m unittest test_caesar -v +``` + +結果: +``` +test_basic_lowercase ... ok +test_basic_uppercase ... ok +test_empty_line ... ok +test_full_sample ... ok +test_mixed_case ... ok +test_multiple_lines ... ok +test_non_letters_unchanged ... ok +test_wrap_around ... ok + +---------------------------------------------------------------------- +Ran 8 tests in 0.001s + +OK +``` + +測試總數:8 通過:8 失敗:0 diff --git a/weeks/week-18/solutions/1114405021/caesar-easy.py b/weeks/week-18/solutions/1114405021/caesar-easy.py new file mode 100644 index 000000000..5f4c9a2dc --- /dev/null +++ b/weeks/week-18/solutions/1114405021/caesar-easy.py @@ -0,0 +1,28 @@ +import sys + +SHIFT = 2 + + +def solve(text: str) -> str: + result = [] + for ch in text: + if "A" <= ch <= "Z": + # 大寫字母循環 + result.append(chr((ord(ch) - ord("A") + SHIFT) % 26 + ord("A"))) + elif "a" <= ch <= "z": + # 小寫字母循環 + result.append(chr((ord(ch) - ord("a") + SHIFT) % 26 + ord("a"))) + else: + # 非字母不變 + result.append(ch) + return "".join(result) + + +def main(): + lines = sys.stdin.read().splitlines() + out = [solve(line) for line in lines] + sys.stdout.write("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405021/caesar.py b/weeks/week-18/solutions/1114405021/caesar.py new file mode 100644 index 000000000..66c4b4d21 --- /dev/null +++ b/weeks/week-18/solutions/1114405021/caesar.py @@ -0,0 +1,25 @@ +import sys + +SHIFT = 2 + + +def shift_char(c: str) -> str: + if "A" <= c <= "Z": + return chr((ord(c) - ord("A") + SHIFT) % 26 + ord("A")) + if "a" <= c <= "z": + return chr((ord(c) - ord("a") + SHIFT) % 26 + ord("a")) + return c + + +def encrypt_line(line: str) -> str: + return "".join(shift_char(c) for c in line) + + +def main(): + data = sys.stdin.read().splitlines() + out = [encrypt_line(line) for line in data] + sys.stdout.write("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405021/main-easy.py b/weeks/week-18/solutions/1114405021/main-easy.py new file mode 100644 index 000000000..9634d6977 --- /dev/null +++ b/weeks/week-18/solutions/1114405021/main-easy.py @@ -0,0 +1,50 @@ +import sys + +D = 3 + + +def solve(nums): + # ① 去重(保留首次出現順序) + seen = set() + unique = [] + for x in nums: + if x not in seen: + unique.append(x) + seen.add(x) + + # ② 只保留能被 D 整除的數 + filtered = [x for x in unique if x % D == 0] + + # ③ 由小到大排序 + filtered.sort() + return filtered + + +def main(): + data = sys.stdin.read().splitlines() + out = [] + i = 0 + + while i < len(data): + line = data[i].strip() + if line == "": + i += 1 + continue + n = int(line) + i += 1 + if n == 0: + break + if i >= len(data): + break + + nums = list(map(int, data[i].strip().split())) + i += 1 + + res = solve(nums) + out.append(" ".join(map(str, res)) if res else "NONE") + + sys.stdout.write("\n".join(out)) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405021/main.py b/weeks/week-18/solutions/1114405021/main.py new file mode 100644 index 000000000..70dfdad8e --- /dev/null +++ b/weeks/week-18/solutions/1114405021/main.py @@ -0,0 +1,53 @@ +import sys + +D = 3 + + +def dedupe_preserve_order(nums): + seen = set() + result = [] + for x in nums: + if x not in seen: + result.append(x) + seen.add(x) + return result + + +def process_sequence(nums): + unique = dedupe_preserve_order(nums) + filtered = [x for x in unique if x % D == 0] + filtered.sort() + return filtered + + +def main(): + lines = sys.stdin.read().splitlines() + output = [] + i = 0 + + while i < len(lines): + line = lines[i].strip() + if line == "": + i += 1 + continue + n = int(line) + i += 1 + if n == 0: + break + if i >= len(lines): + break + + nums = list(map(int, lines[i].strip().split())) + i += 1 + + result = process_sequence(nums) + if result: + output.append(" ".join(map(str, result))) + else: + output.append("NONE") + + sys.stdout.write("\n".join(output)) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405021/test_caesar.py b/weeks/week-18/solutions/1114405021/test_caesar.py new file mode 100644 index 000000000..a30ed4122 --- /dev/null +++ b/weeks/week-18/solutions/1114405021/test_caesar.py @@ -0,0 +1,50 @@ +import unittest +from io import StringIO +import sys + +import caesar + + +class TestCaesarCipher(unittest.TestCase): + def test_basic_lowercase(self): + self.assertEqual(caesar.encrypt_line("abc"), "cde") + + def test_basic_uppercase(self): + self.assertEqual(caesar.encrypt_line("XYZ"), "ZAB") + + def test_wrap_around(self): + self.assertEqual(caesar.encrypt_line("yz"), "ab") + self.assertEqual(caesar.encrypt_line("YZ"), "AB") + + def test_mixed_case(self): + self.assertEqual(caesar.encrypt_line("Hello"), "Jgnnq") + + def test_non_letters_unchanged(self): + self.assertEqual(caesar.encrypt_line("A1b2!"), "C1d2!") + + def test_empty_line(self): + self.assertEqual(caesar.encrypt_line(""), "") + + def test_full_sample(self): + input_data = "Hello World!\nabc XYZ\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + caesar.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue(), "Jgnnq Yqtnf!\ncde ZAB") + + def test_multiple_lines(self): + input_data = "a\nb\nc\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + caesar.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue(), "c\nd\ne") + + +if __name__ == "__main__": + unittest.main() diff --git a/weeks/week-18/solutions/1114405021/test_main.py b/weeks/week-18/solutions/1114405021/test_main.py new file mode 100644 index 000000000..a4b3656b1 --- /dev/null +++ b/weeks/week-18/solutions/1114405021/test_main.py @@ -0,0 +1,61 @@ +import unittest +from io import StringIO +import sys + +import main + + +class TestSequenceProcessor(unittest.TestCase): + def test_sample_input(self): + input_data = "8\n4 7 4 2 9 2 6 7\n3\n1 3 5\n0\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + main.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue().strip(), "6 9\n3") + + def test_none_case(self): + input_data = "4\n1 2 4 5\n0\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + main.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue().strip(), "NONE") + + def test_negative_numbers(self): + input_data = "4\n-3 -6 2 4\n0\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + main.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue().strip(), "-6 -3") + + def test_all_same(self): + input_data = "5\n3 3 3 3 3\n0\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + main.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue().strip(), "3") + + def test_multiple_groups(self): + input_data = "5\n3 3 3 3 3\n3\n6 3 9\n4\n1 2 4 5\n0\n" + sys.stdin = StringIO(input_data) + out = StringIO() + sys.stdout = out + main.main() + sys.stdin = sys.__stdin__ + sys.stdout = sys.__stdout__ + self.assertEqual(out.getvalue().strip(), "3\n3 6 9\nNONE") + + +if __name__ == "__main__": + unittest.main()