From 731a5ba035f455ba1c6374ab8987204ded88840c Mon Sep 17 00:00:00 2001 From: "donlaya\\Admin" Date: Thu, 23 Jul 2026 01:54:22 +0700 Subject: [PATCH] Complete a technical assessment --- SOLUTIONS.md | 31 +++++ tasks/01-run-length/python/rle.py | 39 ++++++- tasks/02-fix-the-bug/python/buggy_counter.py | 10 +- tasks/03-sync-aggregator/python/aggregator.py | 110 ++++++++++++------ tasks/04-sql-reasoning/python/queries.py | 44 +++++++ 5 files changed, 189 insertions(+), 45 deletions(-) create mode 100644 SOLUTIONS.md diff --git a/SOLUTIONS.md b/SOLUTIONS.md new file mode 100644 index 0000000..2ff2284 --- /dev/null +++ b/SOLUTIONS.md @@ -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 \ No newline at end of file diff --git a/tasks/01-run-length/python/rle.py b/tasks/01-run-length/python/rle.py index 15a3605..a181ed4 100644 --- a/tasks/01-run-length/python/rle.py +++ b/tasks/01-run-length/python/rle.py @@ -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 \ No newline at end of file diff --git a/tasks/02-fix-the-bug/python/buggy_counter.py b/tasks/02-fix-the-bug/python/buggy_counter.py index 6c44948..2728713 100644 --- a/tasks/02-fix-the-bug/python/buggy_counter.py +++ b/tasks/02-fix-the-bug/python/buggy_counter.py @@ -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 diff --git a/tasks/03-sync-aggregator/python/aggregator.py b/tasks/03-sync-aggregator/python/aggregator.py index 149ae51..2f1f989 100644 --- a/tasks/03-sync-aggregator/python/aggregator.py +++ b/tasks/03-sync-aggregator/python/aggregator.py @@ -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 \ No newline at end of file diff --git a/tasks/04-sql-reasoning/python/queries.py b/tasks/04-sql-reasoning/python/queries.py index 30c9138..54fc311 100644 --- a/tasks/04-sql-reasoning/python/queries.py +++ b/tasks/04-sql-reasoning/python/queries.py @@ -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);", \ No newline at end of file