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
16 changes: 0 additions & 16 deletions projects/003-probability-calculator/main.py

This file was deleted.

13 changes: 13 additions & 0 deletions projects/003-probability-calculator/python/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

from probability_calculator.probability_calculator import ProbabilityCalculator




def main():
app = ProbabilityCalculator()
app.run()


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class GetUserInput:
"""Utility class that handles and validates user input."""
def validate_positive_number(prompt: str) -> int:
"""Ask the user for a positive integer number.

Args:
prompt (str): Message shown to the user.

Returns:
int: A validated positive integer.
"""
while True:
try:
number = int(input(prompt))
if number > 0:
return number
print("Please enter a positive number")
except ValueError:
print("Please enter a positive number")

def get_user_choice() -> str:
"""Request the user's menu choice (0–3).

Returns:
str: The chosen option ("0", "1", "2", "3").
"""
while (choice := input("Enter your choice: ").strip()) not in ("1", "2", "3", "0"):
print("Invalid choice. Please try again.")
else:
return choice


Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class OutputText:
def show_welcome_message():
print("=" * 80)
print("|| Welcome to Probability Calculator! ||")
print("=" * 80)
print("\nThis software is use to determine the approximate probability of drawing\n"
"certain balls randomly from a hat.\n")

def show_main_menu():
print("=" * 80)
print("Main Menu\n")

print("1. Create new hat.")
print("2. Show current hat.")
print("3. Start Experiment.")
print("0. Exit")

def exit_message():
print("=" * 80)
print("Thank you for using this software.")
print("=" * 80)
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import sys
from collections import Counter
from typing import Optional, Dict

from probability_calculator.get_user_input import GetUserInput
from probability_calculator.output_text import OutputText
from probability_calculator.prob_calculator import Hat, experiment


class ProbabilityCalculator:
"""A class to manage probability experiments using a customizable hat of balls.

This class allows the user to:
- Create a hat with colored balls.
- Show the current hat configuration.
- Run probability experiments based on expected ball draws.

Attributes:
hat (Optional[Hat]): The current hat object used for experiments.
running (bool): Controls the main loop of the menu.
user_input (GetUserInput): Utility for validated user inputs.
"""

def __init__(self) -> None:
"""Initialize the probability calculator."""
self.hat: Optional["Hat"] = None
self.running: bool = True
self.user_input = GetUserInput

def handle_choice(self) -> None:
"""Process user menu choice and invoke related actions."""
choice = self.user_input.get_user_choice()
match choice:
case "1":
self.create_hat()
case "2":
self.show_current_hat()
case "3":
self.run_experiment()
case "0":
self.running = False
OutputText.exit_message()
sys.exit(0)

def run(self) -> None:
"""Start the main loop of the probability calculator."""
OutputText.show_welcome_message()
while self.running:
OutputText.show_main_menu()
self.handle_choice()

def create_hat(self) -> None:
"""Create a hat by asking the user to input ball colors and quantities."""
balls: Dict[str, int] = {}

while True:
color = input("Enter color of ball (empty for finish): ").strip()
if color == "":
break

if color in balls:
print(f"{color} already added with {balls[color]} balls.")
print("Please choose a different color or finish.\n")
continue

qty = self.user_input.validate_positive_number(
f"How many balls for {color}? "
)
balls[color] = qty

if balls:
self.hat = Hat(**balls)
print("Hat created successfully.")
else:
print("Impossible to create new hat.")

def show_current_hat(self) -> None:
"""Display the distribution of balls in the current hat."""
if self.hat is None:
print("No hat created")
return

color_count = Counter(self.hat.contents)
for color, count in color_count.items():
print(f"{color}: {count}")

def run_experiment(self) -> None:
"""Run a probability experiment based on user-defined expectations."""
if self.hat is None:
print("No hat created")
return

expected_balls = self._expected_balls()
if not expected_balls:
print("No balls expected, operation aborted.")
return

num_balls_drawn = self._num_balls_drawn()
if num_balls_drawn is None:
return

num_experiments = self._nums_experiments()
if num_experiments is None:
return

probability = experiment(
hat=self.hat,
expected_balls=expected_balls,
num_balls_drawn=num_balls_drawn,
num_experiments=num_experiments
)

self._print_probability(probability)

def _expected_balls(self) -> Optional[Dict[str, int]]:
"""Ask the user which balls and quantities to expect.

Returns:
Optional[Dict[str, int]]: A dictionary mapping ball colors to expected quantities,
or None if no expectations were provided.
"""
expected_balls: Dict[str, int] = {}

print("For the purpose of the experiment, we want to know what color ball and quantity you want to extract")

while True:
color = input("What color do you want to extract? (blank to confirm)").strip().lower()
if color == "":
break

try:
quantity = self.user_input.validate_positive_number(
f"Quantity of balls {color}: "
)
expected_balls[color] = quantity
print(f"Added {quantity} of {color} balls to the expected\n")

except ValueError:
print("Insert a valid number.")

if expected_balls:
return expected_balls

print("No ball expected specified. Operation aborted.")
return None

def _num_balls_drawn(self) -> Optional[int]:
"""Ask the user how many balls should be drawn.

Returns:
Optional[int]: Number of balls to draw, or None if invalid.
"""
num_balls_drawn = self.user_input.validate_positive_number(
"How many balls should be drawn for each experiment? "
)

if self.hat and num_balls_drawn > len(self.hat.contents):
print("Number of balls drawn exceeds number of balls in the hat.")

return num_balls_drawn

def _nums_experiments(self) -> Optional[int]:
"""Ask the user for the number of experiments to perform.

Returns:
Optional[int]: Number of experiments.
"""
while (num_experiments:= self.user_input.validate_positive_number("How many experiments want to execute? ")):

if num_experiments <= 0:
print("Number of experiments must be greater than 0.")
continue

if num_experiments < 1000:
print(
f"Warning: {num_experiments} experiments may yield inaccurate results. "
"At least 1000 are recommended."
)
confirm = input("Continue? (y/n) ").lower()
if confirm not in ("y", "yes"):
continue

if num_experiments > 50000:
print("Warning: This might require more time.")
confirm = input("Continue? (y/n) ").lower()
if confirm not in ("y", "yes"):
continue

print(f"{num_experiments} experiments will be performed.")
return num_experiments

def _print_probability(self, probability: float) -> None:
"""Print the resulting probability.

Args:
probability (float): The probability computed by the experiment.
"""
percentage = probability * 100

print("\n" + "=" * 60)
print("RESULTS")
print("=" * 60)
print(f"Percentage: {percentage:.2f}%")
12 changes: 6 additions & 6 deletions projects/003-probability-calculator/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
prob_calculator.random.seed(95)
class UnitTests(unittest.TestCase):
def test_hat_class_contents(self):
hat = prob_calculator.Hat(red=3,blue=2)
hat = prob_calculator.Hat(red=3, blue=2)
actual = hat.contents
expected = ["red","red","red","blue","blue"]
self.assertEqual(actual, expected, 'Expected creation of hat object to add correct contents.')

def test_hat_draw(self):
hat = prob_calculator.Hat(red=5,blue=2)
hat = prob_calculator.Hat(red=5, blue=2)
actual = hat.draw(2)
expected = ['blue', 'red']
self.assertEqual(actual, expected, 'Expected hat draw to return two random items from hat contents.')
Expand All @@ -19,13 +19,13 @@ def test_hat_draw(self):
self.assertEqual(actual, expected, 'Expected hat draw to reduce number of items in contents.')

def test_prob_experiment(self):
hat = prob_calculator.Hat(blue=3,red=2,green=6)
probability = prob_calculator.experiment(hat=hat, expected_balls={"blue":2,"green":1}, num_balls_drawn=4, num_experiments=1000)
hat = prob_calculator.Hat(blue=3, red=2, green=6)
probability = prob_calculator.experiment(hat=hat, expected_balls={"blue":2, "green":1}, num_balls_drawn=4, num_experiments=1000)
actual = probability
expected = 0.272
self.assertAlmostEqual(actual, expected, delta = 0.01, msg = 'Expected experiment method to return a different probability.')
hat = prob_calculator.Hat(yellow=5,red=1,green=3,blue=9,test=1)
probability = prob_calculator.experiment(hat=hat, expected_balls={"yellow":2,"blue":3,"test":1}, num_balls_drawn=20, num_experiments=100)
hat = prob_calculator.Hat(yellow=5, red=1, green=3, blue=9, test=1)
probability = prob_calculator.experiment(hat=hat, expected_balls={"yellow":2, "blue":3, "test":1}, num_balls_drawn=20, num_experiments=100)
actual = probability
expected = 1.0
self.assertAlmostEqual(actual, expected, delta = 0.01, msg = 'Expected experiment method to return a different probability.')
Expand Down