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
24 changes: 24 additions & 0 deletions weeks/week-18/solutions/1114405055/ai_log_q3_digital_root.md
Original file line number Diff line number Diff line change
@@ -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<base、樣例 8/63 驗算、需兩輪收斂的大數、x 接近 1e9、base=16 額外驗證)。
2. 執行 pytest 確認紅燈:因 `digital_root.py` 尚未存在,出現 `ModuleNotFoundError`,紀錄於 `test_log_q3_red.txt`。
3. 使用者確認測試案例後,才寫 `digital_root.py` 實作。
4. 重新執行 pytest 確認綠燈:15 個測試全數通過,紀錄於 `test_log_q3_green.txt`。
5. 用使用者給的 sample input(0/8/63)手動跑過 `digital_root.py` 的 stdin/EOF 流程,輸出與使用者手算結果(0/2/3)一致。

## Git 流程
- 新建分支 `1114405055-week18-q3-digital-root`,依使用者要求拆成兩個 commit:
1. 紅燈 commit:只含測試檔與 `test_log_q3_red.txt`
2. 綠燈 commit:只含實作檔與 `test_log_q3_green.txt`
- 因本機帳號對上游 repo(DevSecOpsLab-CSIE-NPU/2026-python)無推送權限,改 push 到使用者自己的 fork(yuchi-create/2026-python),再由使用者開 PR 到上游。
37 changes: 37 additions & 0 deletions weeks/week-18/solutions/1114405055/digital_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 第三題:任意進位的數字根
# 學號 1114405055 末兩碼 55,個位 u=5,查表得 base=7

BASE = 7


def digit_sum_in_base(x: int, base: int) -> 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()
74 changes: 74 additions & 0 deletions weeks/week-18/solutions/1114405055/test_digital_root.py
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions weeks/week-18/solutions/1114405055/test_log_q3_green.txt
Original file line number Diff line number Diff line change
@@ -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 ==============================
22 changes: 22 additions & 0 deletions weeks/week-18/solutions/1114405055/test_log_q3_red.txt
Original file line number Diff line number Diff line change
@@ -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 <module>
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 ===============================