Skip to content

Commit 74b9bff

Browse files
Merge pull request steam-bell-92#1291 from ruhelamahi7-code/feature/quick-sort-visualizer
feat: add Quick Sort Visualizer (web + CLI)
2 parents 509d59b + 0db5dc8 commit 74b9bff

8 files changed

Lines changed: 608 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ deactivate
240240

241241
## 🌐 Web App Catalog
242242

243-
The browser app currently exposes 39 projects:
243+
The browser app currently exposes 40 projects:
244244

245245
### Games
246246

@@ -271,6 +271,7 @@ The browser app currently exposes 39 projects:
271271
- Armstrong Numbers
272272
- Binary Search
273273
- Bubble Sort
274+
- Quick Sort
274275
- Calculator
275276
- Collatz Conjecture
276277
- Coordinate to Polar

math/Quick-Sort/Quick-Sort.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import sys
2+
import os
3+
4+
# Add project root to sys.path
5+
if "__file__" in globals():
6+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
7+
else:
8+
sys.path.append(os.path.abspath(os.getcwd()))
9+
10+
from utils.validation import get_choice, get_int_list
11+
12+
13+
def quick_sort(arr: list[int], reverse: bool = False) -> list[int]:
14+
"""Sorts a list of integers using the Quick Sort algorithm.
15+
16+
Args:
17+
arr: The list of integers to sort.
18+
reverse: If True, sorts in descending order. Otherwise, ascending.
19+
20+
Returns:
21+
A new sorted list.
22+
"""
23+
# Base case: a list of 0 or 1 elements is already sorted
24+
if len(arr) <= 1:
25+
return arr.copy()
26+
27+
# Choose the last element as the pivot
28+
pivot = arr[-1]
29+
left = []
30+
right = []
31+
32+
# Partition the remaining elements around the pivot
33+
for value in arr[:-1]:
34+
if not reverse and value <= pivot:
35+
left.append(value)
36+
elif not reverse and value > pivot:
37+
right.append(value)
38+
elif reverse and value >= pivot:
39+
left.append(value)
40+
else:
41+
right.append(value)
42+
43+
# Recursively sort left and right partitions, then combine
44+
return quick_sort(left, reverse) + [pivot] + quick_sort(right, reverse)
45+
46+
47+
def main() -> None:
48+
print("=" * 50)
49+
print("⚡ QUICK SORT INTERACTIVE TOOL ⚡")
50+
print("=" * 50)
51+
print("Sort a list of numbers in Ascending or Descending order.\n")
52+
53+
while True:
54+
print("=" * 50)
55+
56+
arr = get_int_list(
57+
prompt="➡️ Enter numbers to sort separated by spaces (e.g., 64 34 25): ",
58+
error_empty="❌ Error: Input cannot be empty!",
59+
error_invalid="❌ Error: Please enter valid integers only."
60+
)
61+
62+
print("\nChoose sorting order:")
63+
print("1️⃣ Ascending")
64+
print("2️⃣ Descending")
65+
66+
order_choice = get_choice(
67+
prompt="🎯 Enter your choice (1 or 2): ",
68+
choices=["1", "2"],
69+
error_invalid="❌ Invalid sorting choice! Please select 1 or 2."
70+
)
71+
72+
reverse = (order_choice == "2")
73+
74+
sorted_arr = quick_sort(arr, reverse)
75+
76+
print(f"\n📊 Original list: {arr}")
77+
if reverse:
78+
print(f"✅ Sorted list (Descending): {sorted_arr}")
79+
else:
80+
print(f"✅ Sorted list (Ascending): {sorted_arr}")
81+
82+
again = input("\n🔄 Do you want to sort another list? (y/n): ").strip().lower()
83+
if again != 'y':
84+
print("\n👋 Thanks for using Quick Sort Tool! Goodbye!\n")
85+
break
86+
87+
88+
if __name__ == "__main__":
89+
main()

tests/test_quick_sort.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import unittest
2+
from unittest.mock import patch
3+
import io
4+
import os
5+
import importlib.util
6+
7+
# Absolute path to Quick-Sort.py
8+
file_path = os.path.join(
9+
os.path.dirname(__file__), "..",
10+
"math", "Quick-Sort", "Quick-Sort.py"
11+
)
12+
file_path = os.path.abspath(file_path)
13+
14+
# Load module dynamically
15+
spec = importlib.util.spec_from_file_location("quick_sort_module", file_path)
16+
quick_sort_module = importlib.util.module_from_spec(spec)
17+
spec.loader.exec_module(quick_sort_module)
18+
19+
quick_sort = quick_sort_module.quick_sort
20+
main = quick_sort_module.main
21+
22+
23+
class TestQuickSort(unittest.TestCase):
24+
25+
def test_quick_sort_ascending(self):
26+
# Ascending order
27+
self.assertEqual(quick_sort([64, 34, 25, 12, 22, 11, 90]), [11, 12, 22, 25, 34, 64, 90])
28+
self.assertEqual(quick_sort([5, 1, 4, 2, 8]), [1, 2, 4, 5, 8])
29+
30+
def test_quick_sort_descending(self):
31+
# Descending order
32+
self.assertEqual(quick_sort([64, 34, 25, 12, 22, 11, 90], reverse=True), [90, 64, 34, 25, 22, 12, 11])
33+
self.assertEqual(quick_sort([5, 1, 4, 2, 8], reverse=True), [8, 5, 4, 2, 1])
34+
35+
def test_quick_sort_empty_and_single_item(self):
36+
# Empty array
37+
self.assertEqual(quick_sort([]), [])
38+
# Single-item array
39+
self.assertEqual(quick_sort([42]), [42])
40+
41+
def test_quick_sort_negative_integers(self):
42+
# Lists with negative integers
43+
self.assertEqual(quick_sort([-5, -1, -10, 0, 5]), [-10, -5, -1, 0, 5])
44+
self.assertEqual(quick_sort([-5, -1, -10, 0, 5], reverse=True), [5, 0, -1, -5, -10])
45+
46+
def test_quick_sort_already_sorted(self):
47+
# Already sorted ascending
48+
self.assertEqual(quick_sort([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5])
49+
# Already sorted descending
50+
self.assertEqual(quick_sort([5, 4, 3, 2, 1], reverse=True), [5, 4, 3, 2, 1])
51+
52+
def test_quick_sort_with_duplicates(self):
53+
# Lists with duplicate values
54+
self.assertEqual(quick_sort([5, 3, 8, 3, 9, 1, 5]), [1, 3, 3, 5, 5, 8, 9])
55+
56+
@patch('builtins.input')
57+
@patch('sys.stdout', new_callable=io.StringIO)
58+
def test_main_flow_ascending(self, mock_stdout, mock_input):
59+
# Ascending sort flow
60+
mock_input.side_effect = ["64 34 25", "1", "n"]
61+
main()
62+
output = mock_stdout.getvalue()
63+
self.assertIn("Original list: [64, 34, 25]", output)
64+
self.assertIn("Sorted list (Ascending): [25, 34, 64]", output)
65+
66+
@patch('builtins.input')
67+
@patch('sys.stdout', new_callable=io.StringIO)
68+
def test_main_flow_descending(self, mock_stdout, mock_input):
69+
# Descending sort flow
70+
mock_input.side_effect = ["64 34 25", "2", "n"]
71+
main()
72+
output = mock_stdout.getvalue()
73+
self.assertIn("Original list: [64, 34, 25]", output)
74+
self.assertIn("Sorted list (Descending): [64, 34, 25]", output)
75+
76+
@patch('builtins.input')
77+
@patch('sys.stdout', new_callable=io.StringIO)
78+
def test_main_flow_invalid_inputs(self, mock_stdout, mock_input):
79+
# Invalid inputs validation flow
80+
mock_input.side_effect = ["", "64 abc 25", "64 34 25", "3", "64 34 25", "1", "y", "12 11", "2", "n"]
81+
main()
82+
output = mock_stdout.getvalue()
83+
self.assertIn("Error: Input cannot be empty!", output)
84+
self.assertIn("Error: Please enter valid integers only.", output)
85+
self.assertIn("Invalid sorting choice! Please select 1 or 2.", output)
86+
self.assertIn("Sorted list (Ascending): [25, 34, 64]", output)
87+
self.assertIn("Sorted list (Descending): [12, 11]", output)
88+
89+
90+
if __name__ == '__main__':
91+
unittest.main()
16.3 KB
Loading

web-app/generate_banners.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ def draw_o(ox, oy):
660660
("Projectile Motion", "math", "projectile-motion.webp"),
661661
("Binary Search", "math", "binary-search.webp"),
662662
("Bubble Sort", "math", "bubble-sort.webp"),
663+
("Quick Sort", "math", "quick-sort.webp"),
663664
("Tower of Hanoi", "math", "tower-of-hanoi.webp"),
664665
("Matrix Calculator", "math", "matrix-calculator.webp"),
665666
("Fourier Series", "math", "fourier-series.webp"),

web-app/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ <h3>Legal</h3>
848848
<script defer src="js/projects/number-guessing.js"></script>
849849
<script defer src="js/projects/binary-search.js"></script>
850850
<script defer src="js/projects/bubble-sort.js"></script>
851+
<script defer src="js/projects/quick-sort.js"></script>
851852
<script defer src="js/projects/hangman.js"></script>
852853
<script defer src="js/projects/flames.js"></script>
853854
<script defer src="js/projects/fibonacci.js"></script>
@@ -1056,6 +1057,13 @@ <h3>Legal</h3>
10561057
desc: "Visualize bubble sort algorithm",
10571058
tags: "math,algorithm",
10581059
},
1060+
{
1061+
project: "quick-sort",
1062+
title: "Quick Sort",
1063+
category: "math",
1064+
desc: "Visualize quick sort algorithm with pivot partitioning",
1065+
tags: "math,algorithm",
1066+
},
10591067
{
10601068
project: "fibonacci",
10611069
title: "Fibonacci",

web-app/js/projects.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ function getProjectHTML(projectName) {
2929
'reverse-hangman': () => getReverseHangmanHTML,
3030
'snake-game': getSnakeGameHTML(),
3131
'bubble-sort': getBubbleSortHTML(),
32+
'quick-sort': getQuickSortHTML(),
33+
'fourier-series': getFourierSeriesHTML()
3234
'fourier-series': getFourierSeriesHTML(),
3335
'pathfinding-visualizer': getPathfindingVisualizerHTML()
3436
};
@@ -1849,6 +1851,15 @@ const projectInstructions = {
18491851
"Watch the sorting visualization"
18501852
]
18511853
},
1854+
"quick-sort": {
1855+
title: "⚡ How Quick Sort Works",
1856+
steps: [
1857+
"Enter an array of numbers",
1858+
"A pivot element is chosen",
1859+
"Smaller elements go left, larger go right",
1860+
"Process repeats until fully sorted"
1861+
]
1862+
},
18521863
"tower-of-hanoi": {
18531864
title: "🗼 How to Solve Tower of Hanoi",
18541865
steps: [
@@ -3180,6 +3191,7 @@ function initializeProject(projectName) {
31803191
fibonacci: "initFibonacci",
31813192
"binary-search": "initBinarySearch",
31823193
"bubble-sort": "initBubbleSort",
3194+
"quick-sort": "initQuickSort",
31833195
"progression-recognizer": "initProgressionRecognizer",
31843196
"pascal-triangle": "initPascalTriangle",
31853197
armstrong: "initArmstrong",

0 commit comments

Comments
 (0)