diff --git a/weeks/week-18/solutions/1114405055/ai_log_q3_digital_root.md b/weeks/week-18/solutions/1114405055/ai_log_q3_digital_root.md new file mode 100644 index 000000000..cc3920037 --- /dev/null +++ b/weeks/week-18/solutions/1114405055/ai_log_q3_digital_root.md @@ -0,0 +1,24 @@ +# AI 協作紀錄 — 第三題:任意進位的數字根 + +## 需求確認 +- 學號 1114405055 末兩碼 55 → 個位 u=5 → 查表 base=7(依使用者明確指定,不可混用其他題目的終止邏輯) +- 輸入:每行一個十進位非負整數 x,讀到 EOF 結束(與第二題相同,但不同於第一題的 n=0 終止) +- x=0 的數字根規定為 0,不能套一般迭代公式硬算 + +## 設計決策(先問後做) +- **base 是否做成命令列/環境變數參數?** 已詢問使用者,選擇直接寫死 `BASE = 7` 常數,理由是題目已用學號決定 base,做成可帶參數屬於題目未要求的額外設計(違反「最小化、不過度設計」原則)。 +- **函式拆兩個(digit_sum_in_base / digital_root)而非合成一個?** + 因為測試需要分別驗證「兩位數但只迭代一次就收斂」與「需要兩輪才收斂」這兩種邊界情境,若合成一個函式無法在測試中檢查中間結果,故拆開。 + +## 開發流程(TDD:紅燈 → 綠燈) +1. 先寫 `test_digital_root.py`,涵蓋使用者列出的所有 edge case(x=0、x int: + """把 x 換算成 base 進位,將各位數字相加,回傳十進位整數。""" + total = 0 + while x > 0: + total += x % base + x //= base + return total + + +def digital_root(x: int, base: int) -> int: + """重複對 base 進位下的各位數字相加,直到結果是一位數(< base)。""" + if x == 0: + return 0 + while x >= base: + x = digit_sum_in_base(x, base) + return x + + +def main() -> None: + import sys + + for line in sys.stdin: + line = line.strip() + if not line: + continue + x = int(line) + print(digital_root(x, BASE)) + + +if __name__ == "__main__": + main() diff --git a/weeks/week-18/solutions/1114405055/test_digital_root.py b/weeks/week-18/solutions/1114405055/test_digital_root.py new file mode 100644 index 000000000..b8cee1d1d --- /dev/null +++ b/weeks/week-18/solutions/1114405055/test_digital_root.py @@ -0,0 +1,74 @@ +# 第三題:任意進位的數字根 -- 測試 +# 學號 1114405055 末兩碼 55,個位 u=5,查表得 base=7 +# +# digit_sum_in_base(x, base):x 換算成 base 進位後,把各位數字加總,回傳十進位整數 +# digital_root(x, base):重複呼叫 digit_sum_in_base 直到結果是 base 進位下的一位數 +# x=0 時題目規定直接回傳 0,不套公式硬算 + +import pytest + +from digital_root import digit_sum_in_base, digital_root + +BASE = 7 + + +def test_zero_is_zero_by_definition(): + # 題目明文規定 x=0 的數字根固定是 0,不能用一般迭代公式去算 + assert digital_root(0, BASE) == 0 + + +def test_already_single_digit_no_iteration_needed(): + # x < base 時,本身在 base 進位下就是一位數,不需要任何累加迭代 + for x in range(1, BASE): + assert digital_root(x, BASE) == x + + +def test_sample_8_one_iteration_to_converge(): + # 8 在 7 進位是 11 -> 1+1=2,2 < 7 已是一位數,迭代一次即收斂 + assert digit_sum_in_base(8, BASE) == 2 + assert digital_root(8, BASE) == 2 + + +def test_sample_63_needs_two_iterations_to_converge(): + # 63 在 7 進位是 120 -> 1+2+0=3,3 < 7 已是一位數 + # 這組驗證「相加一次後仍可能不是一位數,需再轉一次進位再加一次」的收斂邊界 + assert digit_sum_in_base(63, BASE) == 3 + assert digital_root(63, BASE) == 3 + + +def test_needs_multiple_rounds_to_converge_to_single_digit(): + # 自行驗算:x=1000000 在 7 進位是 11333311 + # 第一輪:1+1+3+3+3+3+1+1 = 16,16 在 7 進位是 22(兩位數,尚未收斂) + # 第二輪:2+2 = 4,4 < 7 已是一位數 + # 證明 digital_root 必須是「迴圈直到一位數」而非只做固定一次相加 + x = 1_000_000 + first_round = digit_sum_in_base(x, BASE) + assert first_round == 16 + assert first_round >= BASE # 第一次相加後仍是兩位數,尚未收斂 + second_round = digit_sum_in_base(first_round, BASE) + assert second_round == 4 + assert digital_root(x, BASE) == 4 + + +def test_large_value_terminates_quickly(): + # x 接近上限 1e9,確認迴圈會收斂且不會死迴圈或效能爆炸 + x = 10**9 + result = digital_root(x, BASE) + assert 0 <= result < BASE + + +@pytest.mark.parametrize("x", [0, 1, 6, 7, 8, 63, 1_000_000, 10**9]) +def test_digital_root_always_single_digit_in_base(x): + # 數字根定義上必須落在 [0, base) 之間(在 base 進位下是一位數) + assert 0 <= digital_root(x, BASE) < BASE + + +def test_base_16_conversion_not_hardcoded_for_small_base(): + # 題目特別提醒 base 可能是 16,確認進位轉換/輸出邏輯沒有寫死成只服務小 base + # 255 在 16 進位是 FF -> 十進位數字相加 15+15=30,30 在 16 進位是 1E -> 1+14=15 + base16 = 16 + assert digit_sum_in_base(255, base16) == 30 + assert digital_root(255, base16) == 15 + # 16 進位下剛好一位數的情況 + assert digital_root(15, base16) == 15 + assert digital_root(0, base16) == 0 diff --git a/weeks/week-18/solutions/1114405055/test_log_q3_green.txt b/weeks/week-18/solutions/1114405055/test_log_q3_green.txt new file mode 100644 index 000000000..dfde15351 --- /dev/null +++ b/weeks/week-18/solutions/1114405055/test_log_q3_green.txt @@ -0,0 +1,24 @@ +============================= test session starts ============================= +platform win32 -- Python 3.13.9, pytest-8.4.2, pluggy-1.5.0 -- C:\Users\User\anaconda3\python.exe +cachedir: .pytest_cache +rootdir: C:\Users\User\Desktop\0622-3\2026-python\weeks\week-18\solutions\1114405055 +plugins: anyio-4.10.0 +collecting ... collected 15 items + +test_digital_root.py::test_zero_is_zero_by_definition PASSED [ 6%] +test_digital_root.py::test_already_single_digit_no_iteration_needed PASSED [ 13%] +test_digital_root.py::test_sample_8_one_iteration_to_converge PASSED [ 20%] +test_digital_root.py::test_sample_63_needs_two_iterations_to_converge PASSED [ 26%] +test_digital_root.py::test_needs_multiple_rounds_to_converge_to_single_digit PASSED [ 33%] +test_digital_root.py::test_large_value_terminates_quickly PASSED [ 40%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[0] PASSED [ 46%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[1] PASSED [ 53%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[6] PASSED [ 60%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[7] PASSED [ 66%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[8] PASSED [ 73%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[63] PASSED [ 80%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[1000000] PASSED [ 86%] +test_digital_root.py::test_digital_root_always_single_digit_in_base[1000000000] PASSED [ 93%] +test_digital_root.py::test_base_16_conversion_not_hardcoded_for_small_base PASSED [100%] + +============================= 15 passed in 0.02s ============================== diff --git a/weeks/week-18/solutions/1114405055/test_log_q3_red.txt b/weeks/week-18/solutions/1114405055/test_log_q3_red.txt new file mode 100644 index 000000000..28ade1cc3 --- /dev/null +++ b/weeks/week-18/solutions/1114405055/test_log_q3_red.txt @@ -0,0 +1,22 @@ +============================= test session starts ============================= +platform win32 -- Python 3.13.9, pytest-8.4.2, pluggy-1.5.0 -- C:\Users\User\anaconda3\python.exe +cachedir: .pytest_cache +rootdir: C:\Users\User\Desktop\0622-3\2026-python\weeks\week-18\solutions\1114405055 +plugins: anyio-4.10.0 +collecting ... collected 0 items / 1 error + +=================================== ERRORS ==================================== +____________________ ERROR collecting test_digital_root.py ____________________ +ImportError while importing test module 'C:\Users\User\Desktop\0622-3\2026-python\weeks\week-18\solutions\1114405055\test_digital_root.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +C:\Users\User\anaconda3\Lib\importlib\__init__.py:88: in import_module + return _bootstrap._gcd_import(name[level:], package, level) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +test_digital_root.py:10: in + from digital_root import digit_sum_in_base, digital_root +E ModuleNotFoundError: No module named 'digital_root' +=========================== short test summary info =========================== +ERROR test_digital_root.py +!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +============================== 1 error in 0.08s ===============================