Skip to content
Open
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
40 changes: 40 additions & 0 deletions weeks/week-18/solutions/1114405019/AI_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# AI 使用紀錄:第三題(任意進位的數字根)

依 week-18 README 的「AI 使用方式」五步驟,實際操作紀錄如下。

## 1. 讀題目設計 unit test,加繁中註解

先把題目(base 依學號查表得 6、需多輪收斂、x=0 特例、base 可能是 16)整理成假設與函式簽名提案,經我確認後才寫測試:

- 產出 `test_digital_root.py`,針對 `digit_sum_in_base` 與 `digital_root` 兩個函式設計測試,涵蓋:
- 0 的特例(規定值,非公式算出)
- x < base 的單一位數(免迭代)
- 63 案例需要兩輪才收斂(最容易漏掉的邊界)
- 大數(10^9)確認不死迴圈
- base=16 時數字大於 9 仍以十進位數值相加
- 在實作之前先跑一次測試,確認是紅燈(`ModuleNotFoundError`),紀錄於 `test_red.txt`。

## 2. 寫程式並跑完測試,保留測試紀錄

確認測試案例後才寫 `digital_root.py`(拆成 `digit_sum_in_base` 轉進位+相加、`digital_root` 收斂迴圈、`main` 用 EOF 讀取輸入)。跑測試轉綠燈,紀錄於 `test_green.txt`;另用題目 Sample I/O(0/8/63)驗證實際輸出,紀錄於 `sample_io.txt`。

## 3. 加上繁體中文的註解說明

為 `digital_root.py` 補上逐行的繁體中文註解,說明 `x % base` / `x // base` 的短除法概念,以及收斂迴圈的終止條件。

## 4. 更簡單、更好記的版本(`-easy`)

請 AI 提供不拆 helper function、單一函式內用巢狀迴圈完成的「好記版」,產出 `digital_root-easy.py`,並用同一組 Sample I/O 驗證輸出與手打版一致(`sample_io_easy.txt`)。

## 5. 加上繁體中文的詳細註解說明

為 `digital_root-easy.py` 補上詳細的繁體中文註解,包含口訣式的邏輯說明(「只要還是兩位數以上就繼續加總,直到變成一位數」)及 x=0 為何不需要特例判斷。

## 使用的 AI 工具

Claude Code(claude-sonnet-4-6)。

## 人工確認事項

- 函式簽名提案、測試案例內容、Sample I/O 預期輸出(base=6 下 0/8/63 → 0/3/3)均由我手動確認後才進入下一步。
- 所有測試與 Sample I/O 結果均實際執行驗證,未盲目採信 AI 輸出。
24 changes: 24 additions & 0 deletions weeks/week-18/solutions/1114405019/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 第三題:任意進位的數字根(base=6)

依學號末兩碼 19,個位 9 查對照表得 `base=6`。

## 檔案說明

| 檔案 | 說明 |
| --- | --- |
| `digital_root-easy.py` | AI 教的簡單版本:單一函式 `digital_root_easy`,不拆 helper function,邏輯較好記,附詳細繁體中文註解 |
| `digital_root.py` | 手打版本:拆成 `digit_sum_in_base`(單輪轉進位+相加)與 `digital_root`(收斂迴圈)兩個函式 |
| `test_digital_root.py` | 針對手打版本的 unittest 測試,涵蓋 0、單輪收斂、雙輪收斂、大數、base=16 等邊界 |
| `test_red.txt` | 實作前跑測試的紀錄(紅燈,`ModuleNotFoundError`) |
| `test_green.txt` | 實作後跑測試的紀錄(綠燈,10 個測試全過) |
| `sample_io.txt` | 手打版本對題目 Sample I/O(輸入 0/8/63)的實際輸出 |
| `sample_io_easy.txt` | 簡單版本對同一組 Sample I/O 的實際輸出(用來確認兩版本結果一致) |
| `AI_LOG.md` | AI 使用紀錄,依 week-18 README 的五步驟記錄實際操作過程與人工確認點 |

## 執行方式

```
echo -e "0\n8\n63" | python digital_root.py
echo -e "0\n8\n63" | python digital_root-easy.py
python -m unittest test_digital_root.py -v
```
40 changes: 40 additions & 0 deletions weeks/week-18/solutions/1114405019/digital_root-easy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import sys

BASE = 6 # 學號末兩碼 19,個位 9 查對照表得 base=6


def digital_root_easy(x: int, base: int) -> int:
"""
數字根的「好記版」寫法:不拆成兩個函式,直接用一個迴圈做到底。

口訣:只要 x 在 base 進位下還是兩位數以上(也就是 x >= base),
就把它的每一位數字加起來,變成新的 x,再檢查一次。
一直重複,直到 x 變成一位數(x < base)為止。

x=0 的情況:0 < base 一定成立,迴圈完全不會執行,直接回傳 0,
剛好符合「0 的數字根固定是 0」的規定,不需要額外特例判斷。
"""
while x >= base:
# 這裡是「短除法」的概念:每次用 x % base 拿到最右邊一位數字,
# 再用 x // base 把這一位丟掉,繼續處理剩下的位數,
# 直到 x 被除到 0,總和就是這一輪所有位數加起來的結果。
digit_sum = 0
while x > 0:
digit_sum += x % base
x //= base
x = digit_sum # 用這一輪算出的總和取代 x,回到外層迴圈再檢查一次
return x


def main() -> None:
# 跟手打版一樣讀到 EOF 結束,不是用 0 當終止值
for line in sys.stdin:
line = line.strip()
if not line:
continue
x = int(line)
print(digital_root_easy(x, BASE))


if __name__ == "__main__":
main()
38 changes: 38 additions & 0 deletions weeks/week-18/solutions/1114405019/digital_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import sys

BASE = 6 # 學號末兩碼 19,個位 9 查對照表得 base=6


def digit_sum_in_base(x: int, base: int) -> int:
"""將十進位非負整數 x 換算成 base 進位,回傳各位數字相加的十進位總和"""
total = 0
while x > 0:
# x % base:取出 base 進位下最右邊那一位的數字(仍以十進位數值表示)
# x // base:把這一位去掉,準備取下一位,相當於短除法不斷往左移
total += x % base
x //= base
return total


def digital_root(x: int, base: int) -> int:
"""重複呼叫 digit_sum_in_base,直到結果在 base 進位下為一位數,回傳數字根"""
# x < base 代表 x 在 base 進位下只剩一位數,這就是收斂的終止條件;
# 例如 63 -> digit_sum_in_base 後變成 8,8 在六進位仍是兩位數(12),
# 所以還要再跑一輪,直到結果小於 base 為止
while x >= base:
x = digit_sum_in_base(x, base)
return x


def main() -> None:
# 讀到 EOF 結束(跟第一題用 n=0 終止不同,這題沒有終止值)
for line in sys.stdin:
line = line.strip()
if not line:
continue
x = int(line)
print(digital_root(x, BASE))


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions weeks/week-18/solutions/1114405019/sample_io.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
0
3
3
3 changes: 3 additions & 0 deletions weeks/week-18/solutions/1114405019/sample_io_easy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
0
3
3
59 changes: 59 additions & 0 deletions weeks/week-18/solutions/1114405019/test_digital_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import unittest

from digital_root import digit_sum_in_base, digital_root


class TestDigitSumInBase(unittest.TestCase):
def test_single_round_conversion(self):
# 8 的六進位是 12,1+2=3:驗證「轉進位+單輪相加」本身算得對
self.assertEqual(digit_sum_in_base(8, 6), 3)

def test_result_can_still_be_multi_digit(self):
# 63 的六進位是 143,1+4+3=8:8 在六進位仍是兩位數(12),
# 這個中間值是「需要再轉一輪才會收斂」的關鍵,不能漏掉
self.assertEqual(digit_sum_in_base(63, 6), 8)

def test_zero_has_no_digits_to_sum(self):
self.assertEqual(digit_sum_in_base(0, 6), 0)


class TestDigitalRoot(unittest.TestCase):
def test_zero_is_fixed_to_zero_by_spec(self):
# 題目明文規定 0 的數字根固定為 0,不是公式算出來的結果
self.assertEqual(digital_root(0, 6), 0)

def test_value_less_than_base_needs_no_iteration(self):
# 4 < 6,在六進位下本身就是一位數,不需要任何累加迭代
self.assertEqual(digital_root(4, 6), 4)

def test_converges_after_one_round(self):
# 8 -> 12(六進位) -> 1+2=3,一輪迭代即收斂
self.assertEqual(digital_root(8, 6), 3)

def test_converges_after_two_rounds(self):
# 63 -> 143(六進位) -> 8 -> 12(六進位) -> 3
# 第一輪相加後仍是兩位數,必須再轉一次進位再加一次,
# 這是最容易漏掉的遞迴/迴圈邊界
self.assertEqual(digital_root(63, 6), 3)

def test_large_value_terminates_with_valid_single_digit(self):
# x 接近 10^9 時,確認迴圈會結束且收斂到合法的一位數範圍,
# 避免因為迴圈邊界寫錯造成死迴圈或效能爆炸
root = digital_root(10**9, 6)
self.assertGreaterEqual(root, 0)
self.assertLess(root, 6)

def test_base16_single_round(self):
# 16 的十六進位是 "10",1+0=1,一位數即收斂;
# 確認邏輯沒有寫死成只服務小 base
self.assertEqual(digital_root(16, 16), 1)

def test_base16_digit_can_exceed_nine(self):
# 255 的十六進位是 FF(十進位數字 15,15),15+15=30,
# 30 的十六進位是 "1E"(1,14),1+14=15,15<16 收斂;
# 驗證進位數字大於 9 時仍以十進位數值相加,不是只處理 0-9 字元
self.assertEqual(digital_root(255, 16), 15)


if __name__ == "__main__":
unittest.main()
15 changes: 15 additions & 0 deletions weeks/week-18/solutions/1114405019/test_green.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
test_result_can_still_be_multi_digit (test_digital_root.TestDigitSumInBase.test_result_can_still_be_multi_digit) ... ok
test_single_round_conversion (test_digital_root.TestDigitSumInBase.test_single_round_conversion) ... ok
test_zero_has_no_digits_to_sum (test_digital_root.TestDigitSumInBase.test_zero_has_no_digits_to_sum) ... ok
test_base16_digit_can_exceed_nine (test_digital_root.TestDigitalRoot.test_base16_digit_can_exceed_nine) ... ok
test_base16_single_round (test_digital_root.TestDigitalRoot.test_base16_single_round) ... ok
test_converges_after_one_round (test_digital_root.TestDigitalRoot.test_converges_after_one_round) ... ok
test_converges_after_two_rounds (test_digital_root.TestDigitalRoot.test_converges_after_two_rounds) ... ok
test_large_value_terminates_with_valid_single_digit (test_digital_root.TestDigitalRoot.test_large_value_terminates_with_valid_single_digit) ... ok
test_value_less_than_base_needs_no_iteration (test_digital_root.TestDigitalRoot.test_value_less_than_base_needs_no_iteration) ... ok
test_zero_is_fixed_to_zero_by_spec (test_digital_root.TestDigitalRoot.test_zero_is_fixed_to_zero_by_spec) ... ok

----------------------------------------------------------------------
Ran 10 tests in 0.000s

OK
19 changes: 19 additions & 0 deletions weeks/week-18/solutions/1114405019/test_red.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
test_digital_root (unittest.loader._FailedTest.test_digital_root) ... ERROR

======================================================================
ERROR: test_digital_root (unittest.loader._FailedTest.test_digital_root)
----------------------------------------------------------------------
ImportError: Failed to import test module: test_digital_root
Traceback (most recent call last):
File "C:\Users\yiteng\AppData\Local\Programs\Python\Python311\Lib\unittest\loader.py", line 162, in loadTestsFromName
module = __import__(module_name)
^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\yiteng\Downloads\0622-3\2026-python\weeks\week-18\solutions\1114405019\test_digital_root.py", line 3, in <module>
from digital_root import digit_sum_in_base, digital_root
ModuleNotFoundError: No module named 'digital_root'


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)