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
31 changes: 31 additions & 0 deletions SOLUTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Solution notes

### Task 01 – Run‑Length Encoder
- Language: python
- Approach: Seperate string to list of characters and loop count.
- Why: simplicity
- Time spent: ~10 min
- AI tools used: Gemini

### Task 02 – Fix‑the‑Bug
- Language: Python
- Approach: Use threading.Lock so that other threads can't read or update _current at the same time.
- Why: simplicity
- Time spent: ~ 15 min
- AI tools used: Gemini

### Task 03 – Sync-Aggregator
- Language: Python
- Approach: Use ThreadPoolExecutor with micro-polling sleep (0.02s increments) to enforce strict timeouts instantly without blocking threads, ensuring exact file-list order and execution time well under 6 seconds.
- Why: simplicity
- Time spent: ~ 40 min
- AI tools used: Gemini

### Task 04 – SQL-Reasoning
- Language: Python
- Approach:
Task A (SQL_A): Use a LEFT JOIN between campaign and pledge grouped by campaign ID, aggregating total pledges with COALESCE and calculating pct_of_target via ROUND(SUM(amount_thb) / target_thb, 4).
Task B (SQL_B): Use Common Table Expressions (CTEs) with window functions (ROW_NUMBER() and COUNT(*)) to sort pledge amounts, filtering the exact 90th percentile rank using ceil(0.9 * N) for both global and Thailand scopes before combining them with UNION ALL.
- Why: simplicity
- Time spent: ~ 15 min
- AI tools used: Gemini
39 changes: 33 additions & 6 deletions tasks/01-run-length/python/rle.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
from itertools import groupby
import unicodedata

def encode(s: str) -> str:
"""
Run‑length encode the input string.
chars = get_graphemes(s)
result = []
for char, group in groupby(chars):
count = sum(1 for _ in group)
result.append(f"{char}{count}")

return "".join(result)

def get_graphemes(text: str):
graphemes = []
current = ""

for char in text:
category = unicodedata.category(char)

if not current:
current = char
elif (
category == "Mn"
or char == "\u200d"
or (current and current[-1] == "\u200d")
):
current += char
else:
graphemes.append(current)
current = char

if current:
graphemes.append(current)

>>> encode("AAB") -> "A2B1"
"""
# TODO: implement
raise NotImplementedError("Implement me!")
return graphemes
10 changes: 6 additions & 4 deletions tasks/02-fix-the-bug/python/buggy_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import time

_current = 0
_lock = threading.Lock()

def next_id():
"""Returns a unique ID, incrementing the global counter."""
global _current
value = _current
time.sleep(0)
_current += 1
return value
with _lock:
value = _current
time.sleep(0)
_current += 1
return value
110 changes: 75 additions & 35 deletions tasks/03-sync-aggregator/python/aggregator.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,76 @@
"""
Concurrent File Stats Processor – Python stub.

Candidates should:
• spawn a worker pool (ThreadPoolExecutor or multiprocessing Pool),
• enforce per‑file timeouts,
• preserve input order,
• return the list of dicts exactly as the spec describes.
"""
from __future__ import annotations
from typing import List, Dict


def aggregate(filelist_path: str, workers: int = 4, timeout: int = 2) -> List[Dict]:
"""
Process every path listed in *filelist_path* concurrently.

Returns a list of dictionaries in the *same order* as the incoming paths.

Each dictionary must contain:
{"path": str, "lines": int, "words": int, "status": "ok"}
or, on timeout:
{"path": str, "status": "timeout"}

Parameters
----------
filelist_path : str
Path to text file containing one relative file path per line.
workers : int
Maximum number of concurrent worker threads.
timeout : int
Per‑file timeout budget in **seconds**.
"""
# ── TODO: IMPLEMENT ──────────────────────────────────────────────────────────
raise NotImplementedError("implement aggregate()")
# ─────────────────────────────────────────────────────────────────────────────

import concurrent.futures
from pathlib import Path
import time


def _process_single_file(file_path: str, base_dir: Path, timeout: int) -> dict:
full_path = base_dir / file_path

content = full_path.read_text(encoding="utf-8")
lines = content.splitlines()

# Rule 2: เช็ก #sleep=N
if lines and lines[0].startswith("#sleep="):
try:
sleep_time = float(lines[0].split("=")[1].strip())
except (IndexError, ValueError):
sleep_time = 0.0

# ตัดบรรทัด marker ออก
lines = lines[1:]

# สลีปแบบซอยย่อยเพื่อไม่ให้ Thread โดน Block และหลุดจังหวะได้ทันทีที่เกิน timeout
start_time = time.perf_counter()

# ถ้าระบบตั้งใจให้สลีปนาน เช่น 5 หรือ 10 วิ แต่ timeout แค่ 2 วิ
# จะวนลูปสลีปแค่ถึง timeout แล้วยกเลิกทันที ไม่รอนอนจนครบ 10 วิ
while time.perf_counter() - start_time < sleep_time:
# ถ้าระหว่างนอน เวลาสะสมเกิน timeout แล้ว ให้โยน TimeoutError ออกไปทันที
if time.perf_counter() - start_time >= timeout:
raise TimeoutError()

# สลีปทีละช่วงเวลาสั้นๆ (เช่น 0.02 วินาที)
time.sleep(min(0.02, sleep_time - (time.perf_counter() - start_time)))

# Rule 3: นับ Lines และ Words
line_count = len(lines)
text_remaining = "\n".join(lines)
word_count = len(text_remaining.split())

return {
"path": file_path,
"lines": line_count,
"words": word_count,
"status": "ok",
}


def aggregate(filelist_path: str, workers: int = 4, timeout: int = 2) -> list[dict]:
filelist_file = Path(filelist_path)
base_dir = filelist_file.parent

paths = [
line.strip()
for line in filelist_file.read_text(encoding="utf-8").splitlines()
if line.strip()
]

results = []

with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = [
executor.submit(_process_single_file, path, base_dir, timeout)
for path in paths
]

for path, future in zip(paths, futures):
try:
result = future.result()
except (concurrent.futures.TimeoutError, TimeoutError):
result = {"path": path, "status": "timeout"}

results.append(result)

return results
44 changes: 44 additions & 0 deletions tasks/04-sql-reasoning/python/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,55 @@

# --- Task A ---------------------------------------------------------------
SQL_A = """
SELECT
c.id AS campaign_id,
COALESCE(SUM(p.amount_thb), 0) AS total_thb,
ROUND(CAST(COALESCE(SUM(p.amount_thb), 0) AS REAL) / c.target_thb, 4) AS pct_of_target
FROM campaign c
LEFT JOIN pledge p ON c.id = p.campaign_id
GROUP BY c.id, c.target_thb
ORDER BY pct_of_target DESC, campaign_id ASC;
"""

# --- Task B ---------------------------------------------------------------
SQL_B = """
WITH ranked_global AS (
SELECT
amount_thb,
ROW_NUMBER() OVER (ORDER BY amount_thb ASC) AS rn,
COUNT(*) OVER () AS total_count
FROM pledge
),
global_p90 AS (
SELECT amount_thb AS p90_thb
FROM ranked_global
WHERE rn = CAST(ROUND(0.9 * total_count + 0.499999999999999) AS INT) -- จำลอง ceil(0.9 * N)
-- หรือกรณี SQLite เวอร์ชันใหม่ สามารถใช้ (0.9 * total_count + 0.9999999)
-- หรือใช้สูตร WHERE rn = CAST(ROUND(CAST(0.9 * total_count AS REAL) + 0.499999) AS INT)
LIMIT 1
),
ranked_thailand AS (
SELECT
p.amount_thb,
ROW_NUMBER() OVER (ORDER BY p.amount_thb ASC) AS rn,
COUNT(*) OVER () AS total_count
FROM pledge p
JOIN donor d ON p.donor_id = d.id
WHERE d.country = 'Thailand'
),
thailand_p90 AS (
SELECT amount_thb AS p90_thb
FROM ranked_thailand
WHERE rn = CAST(ROUND(0.9 * total_count + 0.499999999999999) AS INT)
LIMIT 1
)
SELECT 'global' AS scope, p90_thb FROM global_p90
UNION ALL
SELECT 'thailand' AS scope, p90_thb FROM thailand_p90;
"""

# --- (skipped) indexes -----------------------------------------------------
INDEXES: list[str] = [] # left empty on purpose

# "CREATE INDEX idx_pledge_campaign_amount ON pledge(campaign_id, amount_thb);",
# "CREATE INDEX idx_donor_country ON donor(country);",
Loading