diff --git a/projects/003-probability-calculator/main.py b/projects/003-probability-calculator/main.py deleted file mode 100644 index a3eb31d..0000000 --- a/projects/003-probability-calculator/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# This entrypoint file to be used in development. Start by reading README.md -import prob_calculator -from unittest import main - -prob_calculator.random.seed(95) -hat = prob_calculator.Hat(blue=4, red=2, green=6) -probability = prob_calculator.experiment( - hat=hat, - expected_balls={"blue": 2, - "red": 1}, - num_balls_drawn=4, - num_experiments=3000) -print("Probability:", probability) - -# Run unit tests automatically -main(module='test_module', exit=False) diff --git a/projects/003-probability-calculator/python/main.py b/projects/003-probability-calculator/python/main.py index e69de29..6f57061 100644 --- a/projects/003-probability-calculator/python/main.py +++ b/projects/003-probability-calculator/python/main.py @@ -0,0 +1,13 @@ + +from probability_calculator.probability_calculator import ProbabilityCalculator + + + + +def main(): + app = ProbabilityCalculator() + app.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/projects/003-probability-calculator/python/probability_calculator/__init__.py b/projects/003-probability-calculator/python/probability_calculator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projects/003-probability-calculator/python/probability_calculator/get_user_input.py b/projects/003-probability-calculator/python/probability_calculator/get_user_input.py new file mode 100644 index 0000000..3b364a1 --- /dev/null +++ b/projects/003-probability-calculator/python/probability_calculator/get_user_input.py @@ -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 + + diff --git a/projects/003-probability-calculator/python/probability_calculator/output_text.py b/projects/003-probability-calculator/python/probability_calculator/output_text.py new file mode 100644 index 0000000..53a5d34 --- /dev/null +++ b/projects/003-probability-calculator/python/probability_calculator/output_text.py @@ -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) \ No newline at end of file diff --git a/projects/003-probability-calculator/prob_calculator.py b/projects/003-probability-calculator/python/probability_calculator/prob_calculator.py similarity index 100% rename from projects/003-probability-calculator/prob_calculator.py rename to projects/003-probability-calculator/python/probability_calculator/prob_calculator.py diff --git a/projects/003-probability-calculator/python/probability_calculator/probability_calculator.py b/projects/003-probability-calculator/python/probability_calculator/probability_calculator.py new file mode 100644 index 0000000..27c542f --- /dev/null +++ b/projects/003-probability-calculator/python/probability_calculator/probability_calculator.py @@ -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}%") \ No newline at end of file diff --git a/projects/003-probability-calculator/test_module.py b/projects/003-probability-calculator/test_module.py index a92a116..841b017 100644 --- a/projects/003-probability-calculator/test_module.py +++ b/projects/003-probability-calculator/test_module.py @@ -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.') @@ -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.')