From 68b7b81741b2688177205315b4323ceb36388188 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sat, 16 Aug 2025 20:44:47 +0200 Subject: [PATCH] new problem and deleted the obsolete problem loader --- src/problem_loader.py | 15 - src/problems/regex-phone/description.md | 58 ++++ src/problems/regex-phone/manifest.json | 7 + src/problems/regex-phone/test.py | 33 ++ src/templates/index.html | 117 +------ src/templates/ranking.html | 101 ------ src/templates/script.js | 402 +++++++++++++++--------- 7 files changed, 363 insertions(+), 370 deletions(-) delete mode 100644 src/problem_loader.py create mode 100644 src/problems/regex-phone/description.md create mode 100644 src/problems/regex-phone/manifest.json create mode 100644 src/problems/regex-phone/test.py delete mode 100644 src/templates/ranking.html diff --git a/src/problem_loader.py b/src/problem_loader.py deleted file mode 100644 index 5c85a43..0000000 --- a/src/problem_loader.py +++ /dev/null @@ -1,15 +0,0 @@ -import json -import os -import threading -import time -from models import db, Problem -from flask import current_app - -def schedule_problem_reload(app, json_path, interval_hours=10): - def reload_loop(): - while True: - with app.app_context(): - load_problems_from_json(json_path) - time.sleep(interval_hours * 3600) - t = threading.Thread(target=reload_loop, daemon=True) - t.start() diff --git a/src/problems/regex-phone/description.md b/src/problems/regex-phone/description.md new file mode 100644 index 0000000..c98fad1 --- /dev/null +++ b/src/problems/regex-phone/description.md @@ -0,0 +1,58 @@ +# Phone Number Regular Expression Validation + +You are asked to write a function that checks if a given string is a valid phone number. + +A valid phone number must follow this format: + +```python +123-456-7890 +``` + +* It contains **3 digits**, followed by a **dash (-)** +* Then another **3 digits**, followed by a **dash (-)** +* Then exactly **4 digits** + +If the string matches this exact format, return **True**. Otherwise, return **False**. + +--- + +### Example 1 + +```python +Input: "123-456-7890" +Output: True +``` + +### Example 2 + +```python +Input: "1234567890" +Output: False +``` + +### Example 3 + +```python +Input: "abc-def-ghij" +Output: False +``` + +--- + +### Function Signature + +```python +import re + +def is_valid_phone_number(phone_number: str) -> bool: + return bool("Your Solution Here!") +``` + +--- + +### Hint πŸ”‘ + +* Use the **`re`** (regular expression) library. +* `\d` means β€œa digit” in regex. +* You will need exactly **3 digits**, then a dash, then **3 digits**, another dash, then **4 digits**. +* Anchors `^` (start of string) and `$` (end of string) can help ensure the whole string matches. \ No newline at end of file diff --git a/src/problems/regex-phone/manifest.json b/src/problems/regex-phone/manifest.json new file mode 100644 index 0000000..607c20f --- /dev/null +++ b/src/problems/regex-phone/manifest.json @@ -0,0 +1,7 @@ +{ + "title": "Regex Phonenumber", + "description": "A regex problem to match phone numbers in various formats.", + "description_md": "problems/regex-phone/description.md", + "difficulty": "hard", + "test_code": "problems/regex-phone/test.py" +} \ No newline at end of file diff --git a/src/problems/regex-phone/test.py b/src/problems/regex-phone/test.py new file mode 100644 index 0000000..799e1da --- /dev/null +++ b/src/problems/regex-phone/test.py @@ -0,0 +1,33 @@ +import re +import unittest + +## def is_valid_phone_number(phone_number : str): + ## return bool(re.search(r"^(\d{3}-){2}\d{4}$", phone_number)) + +import unittest + +class TestPhoneNumberRegex(unittest.TestCase): + def test_if_valid(self): + test_cases = [ + ("123-456-7890", True), # Valid format + ("111-222-3333", True), # Another valid format + ("abc-def-ghij", False), # Letters instead of digits + ("1234567890", False), # Missing dashes + ("123-45-67890", False), # Wrong grouping + ("12-3456-7890", False), # Wrong grouping again + ("", False), # Empty string + ] + print("\nPHONE NUMBER VALIDATION TEST RESULTS") + + for phone, expected in test_cases: + try: + actual = is_valid_phone_number(phone) # pyright: ignore[reportUndefinedVariable] + status = "βœ“ PASS" if actual == expected else "βœ— FAIL" + print(f"{status} | Input: '{phone}' -> Got: {actual} | Expected: {expected}") + self.assertEqual(actual, expected) + except Exception as e: + print(f"βœ— ERROR | Input: '{phone}' -> Exception: {e}") + raise + +if __name__ == "__main__": + unittest.main(verbosity=2) \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html index b1e7943..14dec25 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -23,6 +23,12 @@ } #rankInfoBtn.active { color: #2563eb; cursor:pointer; transition: transform 0.3s ease; } #rankInfoBtn.active { transform: rotate(90deg); } + +/* Highlight top rank */ +.rank-1 { background-color: #f0fff0; } +.rank-1 td:first-child { font-weight: bold; color: #2e7d32; } +.sort-asc::after { content: " ↑"; } +.sort-desc::after { content: " ↓"; } @@ -86,7 +92,7 @@ {{ entry[0] }} {{ problem_titles.get(entry[1], 'Unknown') }} @@ -125,111 +131,6 @@ - - + - + \ No newline at end of file diff --git a/src/templates/ranking.html b/src/templates/ranking.html deleted file mode 100644 index c63cae3..0000000 --- a/src/templates/ranking.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - -Leaderboard Ranking System - - - - -

Leaderboard Ranking System

- -

-The leaderboard uses a weighted scoring system to determine who ranks first. -Rather than simply sorting by a single column, the system considers multiple performance metrics: -

- -

Metrics Used

- - -

Score Calculation

-

-Each metric is normalized against the best result in the leaderboard. -The formula for the score is: -

- -
- -runtimeScore = (yourRuntime / bestRuntime)
-memoryScore = (yourMemory / bestMemory)
-overallScore = runtimeScore Γ— 0.7 + memoryScore Γ— 0.3 -
-
- - - -

Ranking Rules

-
    -
  1. Players are sorted by overallScore (lowest first).
  2. -
  3. If two players have the same score, ties are broken by earlier timestamp (who submitted first).
  4. -
  5. Ranks are updated dynamically when you click the "Rank" column header.
  6. -
- -

Example

-
-Best Runtime: 1.000s -Best Memory: 50MB - -Player A: Runtime = 1.200s, Memory = 60MB -runtimeScore = 1.200 / 1.000 = 1.20 -memoryScore = 60 / 50 = 1.20 -overallScore = (1.20 Γ— 0.7) + (1.20 Γ— 0.3) = 1.20 - -Player B: Runtime = 1.100s, Memory = 70MB -runtimeScore = 1.100 / 1.000 = 1.10 -memoryScore = 70 / 50 = 1.40 -overallScore = (1.10 Γ— 0.7) + (1.40 Γ— 0.3) = 1.19 - -Winner: Player B (score 1.19 is better than 1.20) -
- -

Why This Method?

-

-This ranking system prevents a player with an extremely low runtime but huge memory usage (or vice versa) from automatically winning. -It rewards balanced solutions while still prioritizing speed. -

- - - diff --git a/src/templates/script.js b/src/templates/script.js index c021281..6106ca8 100644 --- a/src/templates/script.js +++ b/src/templates/script.js @@ -1,164 +1,274 @@ -// ======================= -// Problem search -// ======================= -const problemSearch = document.getElementById('problemSearch'); -const problemsContainer = document.getElementById('problemsContainer'); -const problemItems = problemsContainer.querySelectorAll('.problem-item'); - -problemSearch.addEventListener('input', () => { - const searchTerm = problemSearch.value.toLowerCase(); +document.addEventListener('DOMContentLoaded', () => { + // Problem search + const problemSearch = document.getElementById('problemSearch'); + const problemsContainer = document.getElementById('problemsContainer'); + const problemItems = problemsContainer?.querySelectorAll('.problem-item') || []; + + problemSearch?.addEventListener('input', () => { + const term = problemSearch.value.toLowerCase().trim(); problemItems.forEach(item => { - const name = item.dataset.name.toLowerCase(); - const desc = item.dataset.desc?.toLowerCase() || ''; - item.style.display = (name.includes(searchTerm) || desc.includes(searchTerm)) ? '' : 'none'; + const name = item.dataset.name?.toLowerCase() || ''; + const desc = item.dataset.desc?.toLowerCase() || ''; + const shouldShow = !term || name.includes(term) || desc.includes(term); + item.style.display = shouldShow ? '' : 'none'; }); -}); + }); -// ======================= -// Leaderboard filtering -// ======================= -const userSearch = document.getElementById('userSearch'); -const problemFilter = document.getElementById('problemFilter'); -const runtimeFilter = document.getElementById('runtimeFilter'); -const leaderboardBody = document.getElementById('leaderboardBody'); -const leaderboardRows = Array.from(leaderboardBody.querySelectorAll('tr')); -const sortableHeaders = document.querySelectorAll('.sortable'); + // Leaderboard functionality + const problemFilter = document.getElementById('problemFilter'); + const runtimeFilter = document.getElementById('runtimeFilter'); + const leaderboardBody = document.getElementById('leaderboardBody'); + const sortableHeaders = document.querySelectorAll('.sortable'); + + let currentSort = { column: 'rank', direction: 'asc' }; + let allRows = []; -let currentSort = { column: null, direction: 'asc' }; + // Initialize rows array + function initializeRows() { + allRows = Array.from(leaderboardBody.querySelectorAll('tr')).map(row => { + return { + element: row, + user: row.dataset.user || '', + problem: row.dataset.problem || '', + runtime: parseFloat(row.dataset.runtime) || 0, + memory: parseFloat(row.dataset.memory) || 0, + timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(), + language: row.dataset.language || '', + originalIndex: Array.from(leaderboardBody.children).indexOf(row) + }; + }); + } -function filterLeaderboard() { - const userTerm = userSearch.value.toLowerCase(); - const problemTerm = problemFilter.value.toLowerCase(); - const runtimeType = runtimeFilter.value; + function updateRankClasses() { + const visibleRows = allRows.filter(row => row.element.style.display !== 'none'); + visibleRows.forEach((rowData, index) => { + const rank = index + 1; + const row = rowData.element; + + // Update rank cell + const rankCell = row.cells[0]; + if (rankCell) rankCell.textContent = rank; + + // Update rank classes + row.className = row.className.replace(/\brank-\d+\b/g, ''); + if (rank === 1) row.classList.add('rank-1'); + else if (rank <= 3) row.classList.add('rank-top3'); + }); + } - leaderboardRows.forEach(row => { - const user = row.dataset.user.toLowerCase(); - const problem = row.dataset.problem.toLowerCase(); - const runtime = parseFloat(row.dataset.runtime); + function calculateOverallRanking() { + const visibleRows = allRows.filter(row => row.element.style.display !== 'none'); + + if (visibleRows.length === 0) return; - const showUser = user.includes(userTerm); - const showProblem = problem.includes(problemTerm); + // Group submissions by problem to find the best performance for each + const problemBests = {}; + + visibleRows.forEach(rowData => { + const problem = rowData.problem; + if (!problemBests[problem]) { + problemBests[problem] = { + bestRuntime: Infinity, + bestMemory: Infinity + }; + } + + problemBests[problem].bestRuntime = Math.min(problemBests[problem].bestRuntime, rowData.runtime); + problemBests[problem].bestMemory = Math.min(problemBests[problem].bestMemory, rowData.memory); + }); - let showRuntime = true; - if (runtimeType === 'best') { - const userProblemRows = leaderboardRows.filter(r => - r.dataset.user === row.dataset.user && - r.dataset.problem === row.dataset.problem - ); - const bestRuntime = Math.min(...userProblemRows.map(r => parseFloat(r.dataset.runtime))); - showRuntime = runtime === bestRuntime; - } else if (runtimeType === 'worst') { - const userProblemRows = leaderboardRows.filter(r => - r.dataset.user === row.dataset.user && - r.dataset.problem === row.dataset.problem - ); - const worstRuntime = Math.max(...userProblemRows.map(r => parseFloat(r.dataset.runtime))); - showRuntime = runtime === worstRuntime; + // Calculate normalized scores for each submission + visibleRows.forEach(rowData => { + const problemBest = problemBests[rowData.problem]; + + // Prevent division by zero + const runtimeScore = problemBest.bestRuntime > 0 ? + rowData.runtime / problemBest.bestRuntime : 1; + const memoryScore = problemBest.bestMemory > 0 ? + rowData.memory / problemBest.bestMemory : 1; + + // Weighted overall score (70% runtime, 30% memory) + rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3; + }); + + // Sort by overall score (lower is better), then by timestamp (earlier is better for ties) + visibleRows.sort((a, b) => { + const scoreDiff = a.overallScore - b.overallScore; + if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; // Use small epsilon for float comparison + + // If scores are essentially equal, prefer earlier submission + return a.timestamp - b.timestamp; + }); + + // Reorder DOM elements and update ranks + visibleRows.forEach((rowData, index) => { + leaderboardBody.appendChild(rowData.element); + }); + + updateRankClasses(); + } + + function filterLeaderboard() { + const problemTerm = (problemFilter?.value || '').toLowerCase().trim(); + const runtimeType = runtimeFilter?.value || 'all'; + + // Reset all rows to visible first + allRows.forEach(rowData => { + rowData.element.style.display = ''; + }); + + // Apply problem filter + if (problemTerm) { + allRows.forEach(rowData => { + const problemMatch = rowData.problem.toLowerCase().includes(problemTerm); + if (!problemMatch) { + rowData.element.style.display = 'none'; } - - row.style.display = (showUser && showProblem && showRuntime) ? '' : 'none'; - }); -} - -// ======================= -// Helper: Get cell value -// ======================= -function getCellValue(row, column) { - const index = Array.from(document.querySelectorAll('th')).findIndex(th => th.dataset.sort === column); - let val = row.cells[index].textContent.trim(); - - if (['runtime', 'memory', 'rank'].includes(column)) return parseFloat(val) || 0; - if (column === 'timestamp') return new Date(val).getTime(); - return val.toLowerCase(); -} - -function compareValues(a, b, direction) { - if (typeof a === 'number' && typeof b === 'number') { - return direction === 'asc' ? a - b : b - a; + }); } - if (a < b) return direction === 'asc' ? -1 : 1; - if (a > b) return direction === 'asc' ? 1 : -1; - return 0; -} + + // Apply runtime filter (best/worst per user per problem) + if (runtimeType === 'best' || runtimeType === 'worst') { + const userProblemGroups = {}; + + // Group by user + problem combination + allRows.forEach(rowData => { + if (rowData.element.style.display === 'none') return; + + const key = `${rowData.user}::${rowData.problem}`; + if (!userProblemGroups[key]) { + userProblemGroups[key] = []; + } + userProblemGroups[key].push(rowData); + }); + + // Hide all except best/worst for each user-problem combination + Object.values(userProblemGroups).forEach(group => { + if (group.length <= 1) return; + + // Sort by runtime + group.sort((a, b) => a.runtime - b.runtime); + + const keepIndex = runtimeType === 'best' ? 0 : group.length - 1; + group.forEach((rowData, index) => { + if (index !== keepIndex) { + rowData.element.style.display = 'none'; + } + }); + }); + } + + calculateOverallRanking(); + } -// ======================= -// Calculate "Overall" score -// ======================= -function calculateOverallRank() { - const rows = Array.from(leaderboardBody.querySelectorAll('tr')); - const runtimes = rows.map(r => parseFloat(r.dataset.runtime) || Infinity); - const memories = rows.map(r => parseFloat(r.dataset.memory) || Infinity); - - const minRuntime = Math.min(...runtimes); - const minMemory = Math.min(...memories); - - rows.forEach(row => { - const runtimeScore = (parseFloat(row.dataset.runtime) || Infinity) / minRuntime; - const memoryScore = (parseFloat(row.dataset.memory) || Infinity) / minMemory; - // Weighted score: runtime 70%, memory 30% - const score = runtimeScore * 0.7 + memoryScore * 0.3; - row.dataset.overallScore = score; - }); -} - -// ======================= -// Sorting -// ======================= -function sortLeaderboard(column, direction) { - let rows = Array.from(leaderboardBody.querySelectorAll('tr')); + function getCellValue(rowData, column) { + switch (column) { + case 'rank': + return parseInt(rowData.element.cells[0]?.textContent) || 0; + case 'user': + return rowData.user.toLowerCase(); + case 'problem': + return rowData.problem.toLowerCase(); + case 'runtime': + return rowData.runtime; + case 'memory': + return rowData.memory; + case 'timestamp': + return rowData.timestamp; + case 'language': + return rowData.language.toLowerCase(); + default: + return ''; + } + } + function sortLeaderboard(column, direction) { if (column === 'rank') { - calculateOverallRank(); - rows.sort((a, b) => parseFloat(a.dataset.overallScore) - parseFloat(b.dataset.overallScore)); - // Update displayed rank - rows.forEach((row, index) => { - row.cells[0].textContent = index + 1; - }); - } else { - rows.sort((rowA, rowB) => { - const valA = getCellValue(rowA, column); - const valB = getCellValue(rowB, column); - return compareValues(valA, valB, direction); - }); + calculateOverallRanking(); + return; } - - rows.forEach(row => leaderboardBody.appendChild(row)); -} - -// ======================= -// Event listeners -// ======================= -userSearch.addEventListener('input', filterLeaderboard); -problemFilter.addEventListener('input', filterLeaderboard); -runtimeFilter.addEventListener('change', filterLeaderboard); - -sortableHeaders.forEach(header => { - header.addEventListener('click', () => { - const column = header.dataset.sort; - - sortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc')); - - if (currentSort.column === column) { - currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; - } else { - currentSort.column = column; - currentSort.direction = 'asc'; - } - - header.classList.add(`sort-${currentSort.direction}`); - sortLeaderboard(column, currentSort.direction); + + const visibleRows = allRows.filter(row => row.element.style.display !== 'none'); + + visibleRows.sort((a, b) => { + const valueA = getCellValue(a, column); + const valueB = getCellValue(b, column); + + let comparison = 0; + if (typeof valueA === 'number' && typeof valueB === 'number') { + comparison = valueA - valueB; + } else { + comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + } + + return direction === 'asc' ? comparison : -comparison; }); -}); -const rankInfoBtn = document.getElementById('rankInfoBtn'); -const rankingExplanation = document.getElementById('rankingExplanation'); + // Reorder DOM elements + visibleRows.forEach(rowData => { + leaderboardBody.appendChild(rowData.element); + }); + + updateRankClasses(); + } -rankInfoBtn.addEventListener('click', () => { - if (rankingExplanation.style.display === 'none' || rankingExplanation.style.display === '') { - rankingExplanation.style.display = 'block'; - rankInfoBtn.classList.add('active'); - } else { - rankingExplanation.style.display = 'none'; - rankInfoBtn.classList.remove('active'); + // Event listeners for sorting + sortableHeaders.forEach(header => { + header.addEventListener('click', () => { + const column = header.dataset.sort; + if (!column) return; + + // Remove sorting classes from all headers + sortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc')); + + // Toggle sort direction + if (currentSort.column === column) { + currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; + } else { + currentSort.column = column; + currentSort.direction = 'asc'; + } + + // Add sorting class to current header + header.classList.add(`sort-${currentSort.direction}`); + + sortLeaderboard(column, currentSort.direction); + }); + }); + + // Filter event listeners + problemFilter?.addEventListener('input', filterLeaderboard); + runtimeFilter?.addEventListener('change', filterLeaderboard); + + // Rank info popout + const rankInfoBtn = document.getElementById('rankInfoBtn'); + const rankingExplanation = document.getElementById('rankingExplanation'); + + rankInfoBtn?.addEventListener('click', (e) => { + e.preventDefault(); + rankingExplanation?.classList.toggle('active'); + rankInfoBtn?.classList.toggle('active'); + }); + + // Close ranking explanation when clicking outside + document.addEventListener('click', (e) => { + if (rankingExplanation?.classList.contains('active') && + !rankingExplanation.contains(e.target) && + !rankInfoBtn?.contains(e.target)) { + rankingExplanation.classList.remove('active'); + rankInfoBtn?.classList.remove('active'); } -}); + }); + + // Initialize everything + if (leaderboardBody && leaderboardBody.children.length > 0) { + initializeRows(); + calculateOverallRanking(); + + // Set initial sort indicator + const defaultHeader = document.querySelector('[data-sort="rank"]'); + if (defaultHeader) { + defaultHeader.classList.add('sort-asc'); + } + } +}); \ No newline at end of file