สัปดาห์ที่ 07 · Workshop · Code Literacy

Workshop — 3 CLI Tools ใน 1 สัปดาห์

ปิดเฟส Code Literacy ด้วยการ "สร้างของจริง 3 ชิ้น" · แต่ละชิ้นเริ่มจาก spec → plan → code → test → docs · นี่คือ "workflow ที่ใช้จริงตลอด 15 สัปดาห์ที่เหลือ"

เป้าหมายสัปดาห์นี้

🔒 ทำไม Workshop นี้สำคัญ — มันคือ "ขั้นรู้รอบ" W04 อ่าน · W05 แก้ · W06 spec → AI สร้าง · W07 = ทำเองทั้ง cycle 3 ครั้ง จนกลายเป็นนิสัย · หลังจาก W07 คุณจะมี "กล้ามเนื้อ" ที่ใช้ได้กับทุก project · นี่คือ ทักษะที่ AI ทำให้คุณไม่ได้ — เพราะ AI ทำตามคำสั่งคุณ ไม่ใช่ทำงานแทนคุณ

🎯 3 Tools ต่างกัน แต่ใช้ Pattern เดียวกัน

ทุก CLI tool ใน 15 สัปดาห์นี้ — และในงานจริง — ใช้ "pattern" เดียวกัน:

flowchart LR input[/Input
argv / input / file/] --> validate{Valid?} validate -->|No| err[/Error msg/] validate -->|Yes| logic[Process
คำนวณ / parse / aggregate] logic --> persist[(Persist
ถ้าต้องเก็บ)] logic --> output[/Output
print / file/] persist --> output classDef io fill:#0d3f3a,stroke:#2dd4bf,color:#fff classDef logic fill:#3b2962,stroke:#8b5cf6,color:#fff classDef decision fill:#5c2618,stroke:#ff7a18,color:#fff classDef err fill:#5c1818,stroke:#ef4444,color:#fff classDef db fill:#1e3a5f,stroke:#3776ab,color:#fff class input,output io class logic logic class validate decision class err err class persist db

Tool 1 (Grade): Input→Logic→Output · Tool 2 (WC): Input(file)→Logic→Output · Tool 3 (TODO): Input(argv)→Persist→Output · เห็น pattern เดียวกันมั้ย?

🛠️ Tool 1 — Grade Calculator ตัวจริง

Spec

## Spec: Course Grade Calculator

**ปัญหา:** นักศึกษาอยากรู้ระหว่างเทอมว่า "ถ้าฉันได้คะแนน X final ฉันจะได้เกรดอะไร"
แทนที่จะเดาเอง

**Input:**
- คะแนน homework (รวมแล้ว, max 30)
- คะแนน midterm (max 30)
- คะแนน final ที่เดาว่าจะได้ (max 40)

**Output:**
- คะแนนรวม
- เกรดที่จะได้ (A/B+/B/C+/C/D+/D/F ตามเกณฑ์ภาควิชา)
- คะแนน final ขั้นต่ำเพื่อได้เกรด B (target)

**Rules:**
- ใช้เกณฑ์: 80+=A, 75+=B+, 70+=B, 65+=C+, 60+=C, 55+=D+, 50+=D, <50=F
- ถ้า input ติดลบ/เกิน max → error

**Success:**
- (hw=25, mid=24, final=32) → รวม 81 → A
- target B (70) ต้องการ final อย่างน้อยเท่าไหร่

**Non-goals:**
- ยังไม่เซฟ history
- ยังไม่รองรับวิชาที่มี weight ต่างกัน

ตัวอย่าง output

$ python grade_calc.py
คะแนน homework (max 30): 25
คะแนน midterm (max 30): 24
คะแนน final คาดการณ์ (max 40): 32

📊 คะแนนรวม: 81 / 100
🎓 เกรดที่คาดว่าได้: A

🎯 เป้าหมาย B (70+):
   ต้องการ final อย่างน้อย: 21 / 40 คะแนน
   (ตอนนี้คาดว่าได้ 32 → ผ่านเป้าแล้ว ✅)

🐍 Reference Implementation — ลองรันก่อนเขียนเอง

นี่คือ working version ที่ตรงตาม spec · รันดูก่อน · เปลี่ยนค่า input ดูพฤติกรรม

def calc_grade(total): if total >= 80: return "A" if total >= 75: return "B+" if total >= 70: return "B" if total >= 65: return "C+" if total >= 60: return "C" if total >= 55: return "D+" if total >= 50: return "D" return "F" GRADE_THRESHOLDS = {"A": 80, "B+": 75, "B": 70, "C+": 65, "C": 60, "D+": 55, "D": 50} def min_final_for(target_grade, hw, mid): threshold = GRADE_THRESHOLDS[target_grade] needed = threshold - hw - mid return max(0, needed) def validate(hw, mid, final): if not (0 <= hw <= 30): return False, f"homework ต้อง 0-30 (ได้ {hw})" if not (0 <= mid <= 30): return False, f"midterm ต้อง 0-30 (ได้ {mid})" if not (0 <= final <= 40): return False, f"final ต้อง 0-40 (ได้ {final})" return True, "" hw = int(input("คะแนน homework (max 30): ")) mid = int(input("คะแนน midterm (max 30): ")) final = int(input("คะแนน final คาดการณ์ (max 40): ")) ok, err = validate(hw, mid, final) if not ok: print(f"❌ {err}") else: total = hw + mid + final grade = calc_grade(total) needed = min_final_for("B", hw, mid) print(f"\n📊 คะแนนรวม: {total} / 100") print(f"🎓 เกรดที่คาดว่าได้: {grade}") print(f"\n🎯 เป้าหมาย B (70+):") print(f" ต้องการ final อย่างน้อย: {needed} / 40 คะแนน") if final >= needed: print(f" (ตอนนี้คาดว่าได้ {final} → ผ่านเป้าแล้ว ✅)") else: print(f" (ตอนนี้คาดว่าได้ {final} → ขาดอีก {needed - final} คะแนน ⚠️)")

💡 เปรียบเทียบกับของคุณ: หลังเขียน tool เสร็จ — เอามาวางใน Python Runner ของหน้านี้ หรือใช้ Diff Viewer เทียบบรรทัดต่อบรรทัด

🛠️ Tool 2 — Word Counter ตัวจริง

Spec

## Spec: Word/Line Counter

**ปัญหา:** อาจารย์ขอให้นักศึกษาเขียนรายงาน 1000-1500 คำ — ต้องการเครื่องมือนับเร็ว ๆ
ที่ทำงานกับไฟล์ .txt ได้

**Input:**
- path ของไฟล์ .txt (argument หรือ input)

**Output:**
- จำนวนบรรทัด
- จำนวนคำ
- จำนวนตัวอักษร (รวม / ไม่รวมช่องว่าง)
- คำที่พบบ่อย 10 อันดับแรก

**Rules:**
- ภาษาไทย: ใช้ space แยกคำ (ไม่ต้องใช้ word segmenter)
- skip บรรทัดว่าง
- ตัด punctuation ออกก่อนนับ

**Success:**
- ทำงานกับไฟล์ขนาด < 100KB
- รัน < 1 วินาที

**Non-goals:**
- ยังไม่รองรับ .docx / .pdf
- ยังไม่ใช้ Thai word segmenter (PyThaiNLP)

ตัวอย่าง output

$ python wc.py report.txt
📄 report.txt

   บรรทัด: 87
   คำ: 1247
   ตัวอักษร: 8,432 (ไม่นับช่องว่าง 7,015)

🏆 คำที่พบบ่อย:
   1. ระบบ      → 42
   2. นักศึกษา     → 38
   3. ข้อมูล    → 25
   ...

Tool นี้แนะนำ Python concept ใหม่: open(), str.split(), collections.Counter

🐍 Reference Implementation

ใน browser นี้ไม่มี file system จริง · เลยใช้ "text ตัวอย่างใน input" แทน · ของจริงใน Cursor จะใช้ open(sys.argv[1])

import re from collections import Counter # ในของจริง: with open(sys.argv[1], encoding="utf-8") as f: text = f.read() text = input("paste text ที่จะนับ: ") # split + clean lines = text.split("\n") non_empty = [l for l in lines if l.strip()] words = re.findall(r"\S+", text) # strip punctuation for counting clean_words = [re.sub(r"[\.\,\!\?\(\)\[\]\:\;\"\'…]", "", w) for w in words] clean_words = [w for w in clean_words if w] chars_all = len(text) chars_no_space = len(text.replace(" ", "").replace("\n", "")) print(f"📄 text ที่วาง\n") print(f" บรรทัด: {len(non_empty)}") print(f" คำ: {len(clean_words)}") print(f" ตัวอักษร: {chars_all:,} (ไม่นับช่องว่าง {chars_no_space:,})") print() print("🏆 คำที่พบบ่อย:") counter = Counter(clean_words) for i, (word, n) in enumerate(counter.most_common(10), 1): print(f" {i}. {word:<12} → {n}")

💡 ลองเปลี่ยน input เป็น paragraph อื่น · หรือใส่ word ซ้ำ ๆ ดูว่า top ranking ขึ้น

🛠️ Tool 3 — TODO Manager (มี JSON persistence)

Spec

## Spec: TODO Manager CLI

**ปัญหา:** นักศึกษาอยากเก็บ TODO ของวิชาต่าง ๆ — แทนการเขียนใน Notes ของมือถือ
ที่หาไม่เจอตอนต้องการ

**Commands:**
- add <ข้อความ>        → เพิ่ม TODO
- list                  → แสดงทั้งหมด
- done <id>             → mark ว่าเสร็จ
- delete <id>           → ลบ

**Persistence:**
- เซฟลง todo.json ใน folder ของโปรแกรม
- โหลด todo.json ทุกครั้งที่รัน (ถ้ามี)

**Output:**
- list แสดงเป็น:
  [ ] 1. ทำการบ้านวิชา CP
  [x] 2. ส่งใบงาน lab 3
  [ ] 3. นัด TA Tuesday

**Rules:**
- id เพิ่มทีละ 1, ไม่ใช้ id ซ้ำแม้จะลบไปแล้ว
- list เรียงตาม id

**Success:**
- เพิ่ม + รัน list → เห็น
- มาร์ค done + รัน list → เห็น [x]
- ปิดโปรแกรม + เปิดใหม่ → ข้อมูลยังอยู่

ตัวอย่าง session

$ python todo.py add "ทำการบ้านวิชา CP"
✅ เพิ่ม TODO #1

$ python todo.py add "ส่งใบงาน lab 3"
✅ เพิ่ม TODO #2

$ python todo.py list
📋 TODO List
  [ ] 1. ทำการบ้านวิชา CP
  [ ] 2. ส่งใบงาน lab 3

$ python todo.py done 1
✅ มาร์ค TODO #1 เสร็จแล้ว

$ python todo.py list
📋 TODO List
  [x] 1. ทำการบ้านวิชา CP
  [ ] 2. ส่งใบงาน lab 3

Tool นี้แนะนำ Python concept ใหม่:

🐍 Reference Implementation (in-memory simulation)

Browser ไม่มี file system · simulation นี้เก็บใน memory แทน · ของจริงใน Cursor ใช้ json.load(open("todo.json")) · ลองพิมพ์ command หลาย ๆ ครั้ง

import json import sys # In-browser: simulate JSON file with this dict # In real life: load_todos() reads todo.json file state = {"todos": [], "next_id": 1} def add_todo(text): todo = {"id": state["next_id"], "text": text, "done": False} state["todos"].append(todo) state["next_id"] += 1 print(f"✅ เพิ่ม TODO #{todo['id']}") def list_todos(): if not state["todos"]: print("📭 ยังไม่มี TODO") return print("📋 TODO List") for t in state["todos"]: check = "[x]" if t["done"] else "[ ]" print(f" {check} {t['id']}. {t['text']}") def mark_done(tid): for t in state["todos"]: if t["id"] == tid: t["done"] = True print(f"✅ มาร์ค TODO #{tid} เสร็จแล้ว") return print(f"❌ ไม่พบ TODO #{tid}") def delete_todo(tid): before = len(state["todos"]) state["todos"] = [t for t in state["todos"] if t["id"] != tid] if len(state["todos"]) < before: print(f"🗑 ลบ TODO #{tid}") else: print(f"❌ ไม่พบ TODO #{tid}") # REPL loop (real version: รับ sys.argv) while True: cmd = input("\n> ").strip() if not cmd or cmd == "quit": break parts = cmd.split(maxsplit=1) action = parts[0] if action == "add" and len(parts) > 1: add_todo(parts[1]) elif action == "list": list_todos() elif action == "done" and len(parts) > 1: mark_done(int(parts[1])) elif action == "delete" and len(parts) > 1: delete_todo(int(parts[1])) else: print("❌ ใช้: add <text> / list / done <id> / delete <id> / quit") print("\n👋 bye!")

💡 ใน Input — แต่ละบรรทัดคือ 1 command · ลองเพิ่ม / ลบ / list ดูว่า state คงอยู่

🧪 Workshop Process — สำหรับทุก Tool

  1. (วันจันทร์-อังคาร) เลือก Tool — ใช้ spec ตามที่ให้ หรือดัดให้เข้ากับ "ของจริง" ของคุณ (เช่น ใช้คะแนนวิชาที่กำลังเรียน)
  2. ขอ AI วาง plan — function names, validation, test cases · ตอบคำถามที่ AI ถามให้ครบ
  3. ขอ AI เขียน code — อ่านทุกบรรทัด · ถ้าไม่เข้าใจ → ถาม
  4. รัน test cases ทั้งหมด — บันทึกใน test-results.md
  5. ใช้กับ "ของจริง" — Tool 1: ใส่คะแนนตัวเอง · Tool 2: ใส่ไฟล์รายงานจริง · Tool 3: ใส่ TODO จริงของสัปดาห์นี้
  6. เขียน README.md ที่: คำอธิบาย + วิธีติดตั้ง + วิธีใช้ + ตัวอย่าง output + known issues
  7. commit + push GitHub — แต่ละ tool เป็นโฟลเดอร์ใน repo เดียวกัน
  8. ทำ Tool ถัดไป — repeat 1-7

📁 โครงสร้าง Repo ที่แนะนำ

cp-w07-cli-tools/
├── README.md                ← อธิบายภาพรวม + ลิงก์ไป tool แต่ละตัว
├── grade-calc/
│   ├── grade_calc.py
│   ├── README.md
│   ├── spec.md
│   └── test-results.md
├── word-counter/
│   ├── wc.py
│   ├── sample.txt
│   ├── README.md
│   ├── spec.md
│   └── test-results.md
└── todo-manager/
    ├── todo.py
    ├── todo.json           ← สร้างอัตโนมัติเมื่อรันครั้งแรก
    ├── README.md
    ├── spec.md
    └── test-results.md

📝 Template — README.md ที่ดี

# Grade Calculator

CLI tool คำนวณเกรดจากคะแนน hw + midterm + final
และบอก final ขั้นต่ำเพื่อได้ตามเป้า

## ติดตั้ง
ต้องมี Python 3.11+

## วิธีใช้
```bash
python grade_calc.py
```
แล้วกรอกคะแนนตามที่ถาม

## ตัวอย่าง
[screenshot หรือ output ตัวอย่าง]

## Known issues
- ยังไม่รองรับวิชาที่มี weight ต่างกัน
- ยังไม่ save history

## License
สำหรับใช้ในวิชา Computer Programming คณะวิศวกรรมศาสตร์ UBU

✅ "Done" Rubric — รู้ได้ยังไงว่า tool เสร็จแล้ว

"พอ" ของแต่ละคนไม่เท่ากัน · ใช้ checklist นี้ทุก tool — ถ้าได้ครบ ≥ 8/10 = ส่งได้

เกณฑ์ผ่านเมื่อ
1. มี spec.mdผ่าน Spec Scorecard ≥ 75% (ใน W06)
2. รันได้python tool.py รันแล้วไม่ crash
3. Happy path ผ่านInput ปกติ → Output ตรงตาม spec
4. Edge case ผ่านInput ผิด (empty, ติดลบ, type ผิด) → error message ชัด
5. ทำงานกับ "ของจริง"ลองใช้กับข้อมูลจริงของตัวเอง 1 ครั้ง
6. test-results.mdมีตาราง test case + ผล ≥ 5 case
7. README.mdinstall + usage + screenshot
8. Code อ่านง่ายfunction names ชัด · ไม่มี code ที่ใช้งานไม่ได้
9. Git commit ≥ 5commit ระหว่างทาง · ไม่ใช่ 1 commit ใหญ่
10. เพื่อน 1 คน "ใช้ได้"ส่งให้เพื่อน clone → รันตาม README → ใช้ได้
เคล็ดลับ — Done > Perfect 8/10 ที่ส่งทัน > 10/10 ที่ส่งช้า · ทำ "ครบ rough draft" ก่อน · ค่อยพัฒนา bonus ทีหลัง

🎯 Bonus Challenges (ทำได้ค่อยทำ)

BonusToolทำอะไร
B1grade_calcเซฟ history ลง history.json + คำสั่ง --history
B2wcรองรับหลายไฟล์ + รวมผล
B3wcใช้ argparse รับ flag เช่น --top 20
B4todoเพิ่มคำสั่ง edit <id> <new text>
B5todoเพิ่ม tag และ filter list --tag urgent
B6todoเพิ่ม due date + สีถ้าใกล้

ข้อผิดที่พบบ่อย

ข้าม spec ไปขอ code เลย — Tool 3 (TODO) ง่ายต่อการพังเพราะมี state · ไม่มี spec = ทำมั่ว
Test แค่ครั้งเดียว Tool 3 ต้อง "ปิด-เปิด" โปรแกรมแล้วทดสอบใหม่ เพราะ persistence
README.md เขียนทีหลัง / ไม่เขียน — เพื่อน + AI จะใช้ tool ของคุณไม่ได้ · เขียนตั้งแต่ commit แรก

ส่งงานสัปดาห์นี้

Reference จาก slide เดิม

Workshop นี้รวมทักษะจาก Topic 2-7 ของ slide เดิม + วิธีการ AI-assist จาก Topic 11