diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index 1a4bfe52f20..79557a4c2d0 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -4661,9 +4661,12 @@ "title": "Understanding Abstraction", "intro": ["Learn about Understanding Abstraction in these lessons."] }, - "workshop-placeholder-oop-3": { - "title": "Placeholder - Waiting for title", - "intro": [""] + "workshop-discount-calculator": { + "title": "Build a Discount Calculator", + "intro": [ + "Build a Discount Calculator", + "In this workshop you will build a flexible discount pricing calculator through abstract base classes, allowing multiple discount algorithms to be applied interchangeably without modifying the core logic." + ] }, "lab-player-interface": { "title": "Build a Player Interface", diff --git a/client/src/pages/learn/full-stack-developer/workshop-discount-calculator/index.md b/client/src/pages/learn/full-stack-developer/workshop-discount-calculator/index.md new file mode 100644 index 00000000000..4a20cf492df --- /dev/null +++ b/client/src/pages/learn/full-stack-developer/workshop-discount-calculator/index.md @@ -0,0 +1,9 @@ +--- +title: Introduction to the Build a Discount Calculator +block: workshop-discount-calculator +superBlock: full-stack-developer +--- + +## Introduction to the Build a Discount Calculator + +In this workshop you will build a flexible discount pricing calculator through abstract base classes, allowing multiple discount algorithms to be applied interchangeably without modifying the core logic. diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e524c94e2e4ab7b4bfc7e2.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e524c94e2e4ab7b4bfc7e2.md new file mode 100644 index 00000000000..86fa7dd843b --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e524c94e2e4ab7b4bfc7e2.md @@ -0,0 +1,64 @@ +--- +id: 68e524c94e2e4ab7b4bfc7e2 +title: Step 1 +challengeType: 20 +dashedName: step-1 +--- + +# --description-- + +In this workshop, you are going to build a discount calculator that can apply different discount strategies to products. The system will determine the best price for a customer based on multiple discount rules. + +Start by defining a class named `Product`. Give your class an `__init__` method with the following parameters: `self`, `name`, and `price`. Inside the `__init__` method, assign `name` and `price` parameters to instance attributes with the same name. + +# --hints-- + +You should create a class named `Product`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_class("Product")`) +}) +``` + +Your `Product` class should have an `__init__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").has_function("__init__")`) +}) +``` + +Your `__init__` method should have the following parameters: `self`, `name`, and `price`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").find_function("__init__").has_args("self, name, price")`) +}) +``` + +You should assign `name` to `self.name` inside your `__init__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").find_function("__init__").has_stmt("self.name = name")`) +}) +``` + +You should assign `price` to `self.price` inside your `__init__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").find_function("__init__").has_stmt("self.price = price")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +--fcc-editable-region-- + +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a89.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a89.md new file mode 100644 index 00000000000..9606d051054 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a89.md @@ -0,0 +1,47 @@ +--- +id: 68e529226b0defdff26c4a89 +title: Step 3 +challengeType: 20 +dashedName: step-3 +--- + +# --description-- + +Right now, if you create an instance of `Product` and print it, you'll see a default object representation that isn't very informative. For example, it might look something like `<__main__.Product object at 0x7f9b8c2d1d30>`. + +To convert the default object representation into a readable string, create a `__str__` method in your `Product` class that returns a string with the format `name - $price` (where `name` and `price` should be replaced with the values of the corresponding attributes). + +# --hints-- + +Your `Product` class should have a `__str__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").has_function("__str__")`) +}) +``` + +Your `__str__` method should return a string in the format `name - $price`. + +```js +({ + test: () => runPython(` + test_product = Product("Test Item", 25.5) + assert str(test_product) == "Test Item - $25.5" + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +class Product: + def __init__(self, name: str, price: float): + self.name = name + self.price = price +--fcc-editable-region-- + +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a8a.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a8a.md new file mode 100644 index 00000000000..b110a435cc2 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a8a.md @@ -0,0 +1,85 @@ +--- +id: 68e529226b0defdff26c4a8a +title: Step 6 +challengeType: 20 +dashedName: step-6 +--- + +# --description-- + +Now, create an instance of `Product` with the name `Wireless Mouse` and a price of `50.0`. Assign it to a variable named `product`. Then, print `product` to the console to see what it looks like. + +# --hints-- + +You should have a variable named `product`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_variable("product")`) +}) +``` + +You should assign an instance of `Product` to the `product` variable. + +```js +({ + test: () => runPython(`assert isinstance(product, Product)`) +}) +``` + +Your `Product` instance should have the name `Wireless Mouse` and price `50.0`. + +```js +({ + test: () => runPython(` + assert product.name == 'Wireless Mouse' + assert product.price == 50.0 + `) +}) +``` + +You should print `product` to the console. + +```js +({ + test: () => runPython(`assert _Node(_code).has_call("print(product)")`) +}) +``` + +When you print a `Product` with name `'Wireless Mouse'` and price `50.0`, it should display `Wireless Mouse - $50.0`. + +```js +({ + test: () => runPython(` + import io + import sys + captured_output = io.StringIO() + sys.stdout = captured_output + + test_product = Product('Wireless Mouse', 50.0) + print(test_product) + + sys.stdout = sys.__stdout__ + output = captured_output.getvalue() + + assert "Wireless Mouse - $50.0" in output + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' +--fcc-editable-region-- + +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a8b.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a8b.md new file mode 100644 index 00000000000..705e830d1c9 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e529226b0defdff26c4a8b.md @@ -0,0 +1,48 @@ +--- +id: 68e529226b0defdff26c4a8b +title: Step 7 +challengeType: 20 +dashedName: step-7 +--- + +# --description-- + +Now you'll start building the discount system interface. Python provides a way to define abstract classes that serve as blueprints for other classes. This is done using the `ABC` class (Abstract Base Class) of the `abc` module. + +At the top of your code, add the following import statement: + +```py +from abc import ABC, abstractmethod +``` + +This imports the `ABC` class and the `abstractmethod` decorator, which you'll use to define methods that must be implemented by any class that inherits from your abstract class. + +# --hints-- + +You should have the mentioned import statement at the top of your code. + +```js +({ + test: () => runPython(`assert _Node(_code).find_imports()[0].is_equivalent("from abc import ABC, abstractmethod")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +--fcc-editable-region-- + +--fcc-editable-region-- +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58981.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58981.md new file mode 100644 index 00000000000..813e1c7c165 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58981.md @@ -0,0 +1,53 @@ +--- +id: 68e5293bd00d2fe134f58981 +title: Step 8 +challengeType: 20 +dashedName: step-8 +--- + +# --description-- + +Create a class named `DiscountStrategy` that inherits from `ABC`. This will be an abstract base class that defines the interface for all discount strategies. + +For now, just use `pass` in the class body to create an empty class. + +# --hints-- + +You should create a class named `DiscountStrategy`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_class("DiscountStrategy")`) +}) +``` + +Your `DiscountStrategy` class should inherit from `ABC`. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountStrategy").inherits_from("ABC") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58982.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58982.md new file mode 100644 index 00000000000..8f0af8b153e --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58982.md @@ -0,0 +1,64 @@ +--- +id: 68e5293bd00d2fe134f58982 +title: Step 9 +challengeType: 20 +dashedName: step-9 +--- + +# --description-- + +As discussed in the previous lesson, abstract methods are methods that must be implemented by any class that inherits from the abstract base class. They serve as a contract, ensuring that all discount strategies will have certain required methods. + +Inside your `DiscountStrategy` class, define a method named `is_applicable`. Use the `@abstractmethod` decorator above the method definition. The method should have the following parameters: `self`, `product`, and `user_tier`. Use the `pass` statement in the method body. + +Add type hints to make the method signature clear: `product` should be of type `Product`, `user_tier` should be of type `str`. + + +# --hints-- + +Your `DiscountStrategy` class should have a method named `is_applicable`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountStrategy").has_function("is_applicable")`) +}) +``` + +Your `is_applicable` method should have the `@abstractmethod` decorator. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountStrategy").find_function("is_applicable").has_decorators("abstractmethod")`) +}) +``` + +Your `is_applicable` method should have parameters `self`, `product: Product`, and `user_tier: str`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountStrategy").find_function("is_applicable").has_args("self,product:Product,user_tier:str")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +--fcc-editable-region-- +class DiscountStrategy(ABC): + pass +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58983.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58983.md new file mode 100644 index 00000000000..c05b806ba03 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58983.md @@ -0,0 +1,74 @@ +--- +id: 68e5293bd00d2fe134f58983 +title: Step 11 +challengeType: 20 +dashedName: step-11 +--- + +# --description-- + +Now add a second abstract method named `apply_discount` to your `DiscountStrategy` class. Use the `@abstractmethod` decorator and give it parameters `self` and `product`. + +Add type hints: `product` should be of type `Product`, and the method should return a `float`. Use `pass` in the method body. + +# --hints-- + +Your `DiscountStrategy` class should have a method named `apply_discount`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountStrategy").has_function("apply_discount")`) +}) +``` + +Your `apply_discount` method should have the `@abstractmethod` decorator. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountStrategy").find_function("apply_discount").has_decorators("abstractmethod")`) +}) +``` + +Your `apply_discount` method should have parameters `self` and `product: Product`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountStrategy").find_function("apply_discount").has_args("self, product:Product")`) +}) +``` + +Your `apply_discount` method should have a return type hint of `-> float`. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountStrategy").find_function("apply_discount").has_returns("float") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58984.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58984.md new file mode 100644 index 00000000000..c031227cf0c --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58984.md @@ -0,0 +1,95 @@ +--- +id: 68e5293bd00d2fe134f58984 +title: Step 12 +challengeType: 20 +dashedName: step-12 +--- + +# --description-- + +Now it's time to create a concrete implementation of your abstract class. Create a class named `PercentageDiscount` that inherits from `DiscountStrategy`. + +Give it an `__init__` method that takes `self` and `percent` as parameters. Give a type hint of `int` to `percent` and set the return type of the method to `None`. Inside the `__init__` method, store `percent` as an instance attribute. + +# --hints-- + +You should create a class named `PercentageDiscount`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_class("PercentageDiscount")`) +}) +``` + +Your `PercentageDiscount` class should inherit from `DiscountStrategy`. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("PercentageDiscount").inherits_from("DiscountStrategy") + `) +}) +``` + +Your `PercentageDiscount` class should have an `__init__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").has_function("__init__")`) +}) +``` + +Your `__init__` method should have parameters `self` and `percent: int`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").find_function("__init__").has_args("self, percent: int")`) +}) +``` + +Your `__init__` method should have a return type hint of `None`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").find_function("__init__").has_returns("None")`) +}) +``` + +You should assign `percent` to `self.percent` inside your `__init__` method. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("PercentageDiscount").find_function("__init__").has_stmt("self.percent = percent")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58985.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58985.md new file mode 100644 index 00000000000..0336b9d9f74 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58985.md @@ -0,0 +1,108 @@ +--- +id: 68e5293bd00d2fe134f58985 +title: Step 13 +challengeType: 20 +dashedName: step-13 +--- + +# --description-- + +Since `PercentageDiscount` inherits from an abstract class, you must implement all abstract methods. Start by implementing the `is_applicable` method. The `is_applicable` method should take `self`, `product: Product`, and `user_tier: str` as parameters. It should have a return type hint of `bool`. + +This method should determine whether a percentage discount is valid. Return `True` if `self.percent` is less than or equal to `70`, otherwise return `False`. This prevents unrealistic discounts like 90% off. + +# --hints-- + +Your `PercentageDiscount` class should have an `is_applicable` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").has_function("is_applicable")`) +}) +``` + +Your `is_applicable` method should have parameters `self`, `product: Product`, and `user_tier: str`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").find_function("is_applicable").has_args("self,product:Product,user_tier:str")`) +}) +``` + +Your `is_applicable` method should return `True` when `self.percent` is less than or equal to `70`. + +```js +({ + test: () => runPython(` + # Add stub method to make class instantiable for testing + class TestPercentageDiscount(PercentageDiscount): + def apply_discount(self, product): + return 0 + + test_product = Product("Test", 100.0) + discount1 = TestPercentageDiscount(50) + discount2 = TestPercentageDiscount(70) + assert discount1.is_applicable(test_product, "regular") == True + assert discount2.is_applicable(test_product, "regular") == True + `) +}) +``` + +Your `is_applicable` method should return `False` when `self.percent` is greater than `70`. + +```js +({ + test: () => runPython(` + # Add stub method to make class instantiable for testing + class TestPercentageDiscount(PercentageDiscount): + def apply_discount(self, product): + return 0 + + test_product = Product("Test", 100.0) + discount = TestPercentageDiscount(75) + assert discount.is_applicable(test_product, "regular") == False + `) +}) +``` + +Your `is_applicable` method should have a return type hint of `bool`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").find_function("is_applicable").has_returns("bool")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58986.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58986.md new file mode 100644 index 00000000000..d7b96391332 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58986.md @@ -0,0 +1,90 @@ +--- +id: 68e5293bd00d2fe134f58986 +title: Step 14 +challengeType: 20 +dashedName: step-14 +--- + +# --description-- + +Now implement the `apply_discount` method in your `PercentageDiscount`. + +This method should calculate and return the discounted price. + +The formula is: `product.price * (1 - self.percent / 100)` + +# --hints-- + +Your `PercentageDiscount` class should have an `apply_discount` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").has_function("apply_discount")`) +}) +``` + +Your `apply_discount` method should have parameters `self` and `product: Product`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").find_function("apply_discount").has_args("self, product: Product")`) +}) +``` + +Your `apply_discount` method should have a return type hint of `float`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PercentageDiscount").find_function("apply_discount").has_returns("float")`) +}) +``` + +Your `apply_discount` method should calculate the discounted price using the formula `product.price * (1 - self.percent / 100)`. + +```js +({ + test: () => runPython(` + test_product = Product("Test", 100.0) + discount = PercentageDiscount(20) + result = discount.apply_discount(test_product) + assert result == 80.0 + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58987.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58987.md new file mode 100644 index 00000000000..b8a905ca00c --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58987.md @@ -0,0 +1,107 @@ +--- +id: 68e5293bd00d2fe134f58987 +title: Step 16 +challengeType: 20 +dashedName: step-16 +--- + +# --description-- + +Now you'll create another discount strategy. Create a class named `FixedAmountDiscount` that inherits from `DiscountStrategy`. + +Give it an `__init__` method that takes `self` and `amount` as parameters. Give `amount` parameter a type hint of `int` and a return type hint of `None` for the method. Inside the `__init__` method, store `amount` as an instance attribute. + +# --hints-- + +You should create a class named `FixedAmountDiscount`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_class("FixedAmountDiscount")`) +}) +``` + +Your `FixedAmountDiscount` class should inherit from `DiscountStrategy`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").inherits_from("DiscountStrategy")`) +}) +``` + +Your `FixedAmountDiscount` class should have an `__init__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").has_function("__init__")`) +}) +``` + +Your `__init__` method should have parameters `self` and `amount: int`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").find_function("__init__").has_args("self, amount: int")`) +}) +``` + +Your `__init__` method should have a return type of `None`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").find_function("__init__").has_returns("None")`) +}) +``` + +You should assign `amount` to `self.amount` inside your `__init__` method. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("FixedAmountDiscount").find_function("__init__").has_stmt("self.amount = amount") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58988.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58988.md new file mode 100644 index 00000000000..c9525ba94f9 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58988.md @@ -0,0 +1,119 @@ +--- +id: 68e5293bd00d2fe134f58988 +title: Step 17 +challengeType: 20 +dashedName: step-17 +--- + +# --description-- + +Implement the `is_applicable` method for your `FixedAmountDiscount` class. A fixed amount discount should only be applied if it makes sense economically. + +Return `True` if `product.price * 0.9` is greater than `self.amount`, otherwise return `False`. This ensures the discount doesn't exceed 10% of the original price, which would be unrealistic. + +# --hints-- + +Your `FixedAmountDiscount` class should have an `is_applicable` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").has_function("is_applicable")`) +}) +``` + +Your `is_applicable` method should have parameters `self`, `product: Product`, and `user_tier: str`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").find_function("is_applicable").has_args("self, product:Product, user_tier:str")`) +}) +``` + +Your `is_applicable` method should have a return type hint of `-> bool`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").find_function("is_applicable").has_returns("bool")`) +}) +``` + +Your `is_applicable` method should return `True` when `product.price * 0.9` is greater than `self.amount`. + +```js +({ + test: () => runPython(` + # Add stub method to make class instantiable for testing + class TestFixedAmountDiscount(FixedAmountDiscount): + def apply_discount(self, product): + return 0 + + test_product = Product("Test", 100.0) + discount = TestFixedAmountDiscount(50) + assert discount.is_applicable(test_product, "regular") == True + `) +}) +``` + +Your `is_applicable` method should return `False` when `product.price * 0.9` is less than or equal to `self.amount`. + +```js +({ + test: () => runPython(` + # Add stub method to make class instantiable for testing + class TestFixedAmountDiscount(FixedAmountDiscount): + def apply_discount(self, product): + return 0 + + test_product = Product("Test", 100.0) + discount = TestFixedAmountDiscount(95) + assert discount.is_applicable(test_product, "regular") == False + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58989.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58989.md new file mode 100644 index 00000000000..8e24c55bb37 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f58989.md @@ -0,0 +1,99 @@ +--- +id: 68e5293bd00d2fe134f58989 +title: Step 18 +challengeType: 20 +dashedName: step-18 +--- + +# --description-- + +Now implement the `apply_discount` method for `FixedAmountDiscount`. This method should subtract the fixed amount from the product price and return the new price. + +# --hints-- + +Your `FixedAmountDiscount` class should have an `apply_discount` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").has_function("apply_discount")`) +}) +``` + +Your `apply_discount` method should have parameters `self` and `product: Product`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").find_function("apply_discount").has_args("self, product: Product")`) +}) +``` + +Your `apply_discount` method should have a return type hint of `-> float`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("FixedAmountDiscount").find_function("apply_discount").has_returns("float")`) +}) +``` + +Your `apply_discount` method should return `product.price - self.amount`. + +```js +({ + test: () => runPython(` + test_product = Product("Test", 100.0) + discount = FixedAmountDiscount(15) + result = discount.apply_discount(test_product) + assert result == 85.0 + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f5898a.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f5898a.md new file mode 100644 index 00000000000..00dc82bc452 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e5293bd00d2fe134f5898a.md @@ -0,0 +1,95 @@ +--- +id: 68e5293bd00d2fe134f5898a +title: Step 19 +challengeType: 20 +dashedName: step-19 +--- + +# --description-- + +Let's test your new discount strategy. After the existing code, create an instance of `FixedAmountDiscount` with an amount of `5` and assign it to a variable named `fixed_discount`. Then apply this discount to the product and print the result. + +# --hints-- + +You should create a variable named `fixed_discount` with a `FixedAmountDiscount` instance. + +```js +({ + test: () => runPython(` + assert _Node(_code).has_variable("fixed_discount") + assert isinstance(fixed_discount, FixedAmountDiscount) + `) +}) +``` + +Your `FixedAmountDiscount` instance should be initialized with amount `5`. + +```js +({ + test: () => runPython(`assert fixed_discount.amount == 5`) +}) +``` + +You should call `fixed_discount.apply_discount(product)` and print the result. + +```js +({ + test: () => runPython(` + assert _Node(_code).has_call("print(fixed_discount.apply_discount(product))") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) +--fcc-editable-region-- + +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309d.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309d.md new file mode 100644 index 00000000000..3c87607c09b --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309d.md @@ -0,0 +1,88 @@ +--- +id: 68e52994bc7ea2e3dec5309d +title: Step 20 +challengeType: 20 +dashedName: step-20 +--- + +# --description-- + +Now let's create a third discount strategy for premium users. Create a class named `PremiumUserDiscount` that inherits from `DiscountStrategy`. + +Unlike the previous strategies, this one doesn't need an `__init__` method because it doesn't store any configuration. It will apply a fixed 20% discount to all premium users. + +For now, just create the empty class that inherits from `DiscountStrategy` using `pass`. + +# --hints-- + +You should create a class named `PremiumUserDiscount`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_class("PremiumUserDiscount")`) +}) +``` + +Your `PremiumUserDiscount` class should inherit from `DiscountStrategy`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").inherits_from("DiscountStrategy")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309e.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309e.md new file mode 100644 index 00000000000..c9a01c52ae6 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309e.md @@ -0,0 +1,133 @@ +--- +id: 68e52994bc7ea2e3dec5309e +title: Step 21 +challengeType: 20 +dashedName: step-21 +--- + +# --description-- + +Implement the `is_applicable` method for `PremiumUserDiscount`. This method should check if the user is a premium member. + +Return `True` if `user_tier.lower()` equals `'premium'`, otherwise return `False`. Using `.lower()` ensures the check is case-insensitive. + +# --hints-- + +Your `PremiumUserDiscount` class should have an `is_applicable` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").has_function("is_applicable")`) +}) +``` + +Your `is_applicable` method should have parameters `self`, `product: Product`, and `user_tier: str`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").find_function("is_applicable").has_args("self, product:Product, user_tier:str")`) +}) +``` + +Your `is_applicable` method should have a return type hint of `-> bool`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").find_function("is_applicable").has_returns("bool")`) +}) +``` + +Your `is_applicable` method should return `True` when `user_tier.lower()` equals `'premium'`. + +```js +({ + test: () => runPython(` + # Add stub method to make class instantiable for testing + class TestPremiumUserDiscount(PremiumUserDiscount): + def apply_discount(self, product): + return 0 + + test_product = Product("Test", 100.0) + discount = TestPremiumUserDiscount() + assert discount.is_applicable(test_product, "Premium") == True + assert discount.is_applicable(test_product, "premium") == True + assert discount.is_applicable(test_product, "PREMIUM") == True + `) +}) +``` + +Your `is_applicable` method should return `False` when `user_tier` is not `'premium'`. + +```js +({ + test: () => runPython(` + # Add stub method to make class instantiable for testing + class TestPremiumUserDiscount(PremiumUserDiscount): + def apply_discount(self, product): + return 0 + + test_product = Product("Test", 100.0) + discount = TestPremiumUserDiscount() + assert discount.is_applicable(test_product, "regular") == False + assert discount.is_applicable(test_product, "standard") == False + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +--fcc-editable-region-- +class PremiumUserDiscount(DiscountStrategy): + pass +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309f.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309f.md new file mode 100644 index 00000000000..276c32bbd77 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec5309f.md @@ -0,0 +1,124 @@ +--- +id: 68e52994bc7ea2e3dec5309f +title: Step 22 +challengeType: 20 +dashedName: step-22 +--- + +# --description-- + +Now implement the `apply_discount` method for `PremiumUserDiscount`. This should apply a 20% discount to premium users. + +Return `product.price * 0.8` (which represents 80% of the original price, i.e., 20% off). + +# --hints-- + +Your `PremiumUserDiscount` class should have an `apply_discount` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").has_function("apply_discount")`) +}) +``` + +Your `apply_discount` method should have parameters `self` and `product: Product`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").find_function("apply_discount").has_args("self, product: Product")`) +}) +``` + +Your `apply_discount` method should have a return type hint of `-> float`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("PremiumUserDiscount").find_function("apply_discount").has_returns("float")`) +}) +``` + +Your `apply_discount` method should return `product.price * 0.8`. + +```js +({ + test: () => runPython(` + test_product = Product("Test", 100.0) + discount = PremiumUserDiscount() + result = discount.apply_discount(test_product) + assert result == 80.0 + `) +}) +``` + +Your `apply_discount` method should apply a 20% discount (80% of original price). + +```js +({ + test: () => runPython(` + test_product = Product("Test", 50.0) + discount = PremiumUserDiscount() + result = discount.apply_discount(test_product) + assert result == 40.0 + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a0.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a0.md new file mode 100644 index 00000000000..4808ed5376c --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a0.md @@ -0,0 +1,89 @@ +--- +id: 68e52994bc7ea2e3dec530a0 +title: Step 23 +challengeType: 20 +dashedName: step-23 +--- + +# --description-- + +Now you need a way to manage multiple discount strategies and find the best price. At the top of your file, after the existing import, add an import for the `List` type from the `typing` module: + +```py +from typing import List +``` + +This allows you to specify that a function parameter is a list containing specific types of objects. + +# --hints-- + +You should import `List` from the `typing` module as mentioned in the instructions after the existing imports. + +```js +({ + test: () => runPython(`assert _Node(_code).find_imports()[1].is_equivalent("from typing import List")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +--fcc-editable-region-- + +--fcc-editable-region-- +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a1.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a1.md new file mode 100644 index 00000000000..f19a0ca41ad --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a1.md @@ -0,0 +1,120 @@ +--- +id: 68e52994bc7ea2e3dec530a1 +title: Step 24 +challengeType: 20 +dashedName: step-24 +--- + +# --description-- + +Create a class named `DiscountEngine`. This class will manage all discount strategies and calculate the best price for a product. + +Give it an `__init__` method that takes `self` and `strategies` as parameters, where `strategies` has a type hint of `List[DiscountStrategy]`. The return type of the method should be `None`. Inside the `__init__` method, store `strategies` as an instance attribute. + +# --hints-- + +You should create a class named `DiscountEngine`. + +```js +({ + test: () => runPython(`assert _Node(_code).has_class("DiscountEngine")`) +}) +``` + +Your `DiscountEngine` class should have an `__init__` method. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").has_function("__init__")`) +}) +``` + +Your `__init__` method should have parameters `self` and `strategies: List[DiscountStrategy]`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").find_function("__init__").has_args("self, strategies: List[DiscountStrategy]")`) +}) +``` + +Your `__init__` method should have a return type of `None`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").find_function("__init__").has_returns("None")`) +}) +``` + +You should assign `strategies` to `self.strategies` inside your `__init__` method. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountEngine").find_function("__init__").has_stmt("self.strategies = strategies") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a2.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a2.md new file mode 100644 index 00000000000..397b2eaa5fb --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a2.md @@ -0,0 +1,106 @@ +--- +id: 68e52994bc7ea2e3dec530a2 +title: Step 25 +challengeType: 20 +dashedName: step-25 +--- + +# --description-- + +Now create a method named `calculate_best_price` in your `DiscountEngine` class. This method should take `self`, `product`, and `user_tier` as parameters, and return a `float`. + +For now, just use `pass` in the method body. You'll implement the logic in the next steps. + +# --hints-- + +Your `DiscountEngine` class should have a method named `calculate_best_price`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").has_function("calculate_best_price")`) +}) +``` + +Your `calculate_best_price` method should have parameters `self`, `product: Product`, and `user_tier: str`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").has_args("self, product:Product, user_tier:str")`) +}) +``` + +Your `calculate_best_price` method should have a return type hint of `-> float`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").has_returns("float")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a3.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a3.md new file mode 100644 index 00000000000..71bd5fef01e --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a3.md @@ -0,0 +1,90 @@ +--- +id: 68e52994bc7ea2e3dec530a3 +title: Step 26 +challengeType: 20 +dashedName: step-26 +--- + +# --description-- + +Inside the `calculate_best_price` method, create a list named `prices` containing `product.price`. This ensures that if no discounts apply, the customer pays the full price. + +# --hints-- + +Inside `calculate_best_price`, you should create a list named `prices` containing `product.price`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").has_stmt("prices = [product.price]")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + +--fcc-editable-region-- + def calculate_best_price(self, product: Product, user_tier: str) -> float: + pass +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a4.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a4.md new file mode 100644 index 00000000000..1d24251f553 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a4.md @@ -0,0 +1,114 @@ +--- +id: 68e52994bc7ea2e3dec530a4 +title: Step 27 +challengeType: 20 +dashedName: step-27 +--- + +# --description-- + +Now create a `for` loop that iterates over `self.strategies`. For each `strategy`, check if it's applicable using the `is_applicable` method. If it is, calculate the discounted price using `apply_discount` and store it in a variable named `discounted`. Then, append the discounted price to the `prices` list. + +# --hints-- + +You should create a `for` loop that iterates over `self.strategies`. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").find_for_loops()[0].find_for_iter().is_equivalent("self.strategies") + `) +}) +``` + +Inside the loop, your `if` statement should be `if strategy.is_applicable(product, user_tier):`. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").find_for_loops()[0].find_ifs()[0].find_conditions()[0].is_equivalent("strategy.is_applicable(product, user_tier)") + `) +}) +``` + +If a strategy is applicable, you should call `apply_discount` and append the result to `prices`. + +```js +({ + test: () => runPython(` +assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").find_for_loops()[0].find_ifs()[0].find_body().is_ordered("discounted = strategy.apply_discount(product)","prices.append(discounted)") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + +--fcc-editable-region-- + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a5.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a5.md new file mode 100644 index 00000000000..071101153a3 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a5.md @@ -0,0 +1,98 @@ +--- +id: 68e52994bc7ea2e3dec530a5 +title: Step 28 +challengeType: 20 +dashedName: step-28 +--- + +# --description-- + +Finally, return the minimum value from the `prices` list using the `min()` function. This will give you the best (lowest) price after considering all applicable discounts. + +# --hints-- + +Your `calculate_best_price` method should return the minimum value from the `prices` list using the `min` function. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountEngine").find_function("calculate_best_price").find_return().is_equivalent("return min(prices)") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + + for strategy in self.strategies: + if strategy.is_applicable(product, user_tier): + discounted = strategy.apply_discount(product) + prices.append(discounted) +--fcc-editable-region-- + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a6.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a6.md new file mode 100644 index 00000000000..9a84ab2666a --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a6.md @@ -0,0 +1,116 @@ +--- +id: 68e52994bc7ea2e3dec530a6 +title: Step 30 +challengeType: 20 +dashedName: step-30 +--- + +# --description-- + +Now let's test the complete discount system! Add an `if` statement checking if `__name__ == '__main__'` and move `product = Product('Wireless Mouse', 50.0)` inside it. + +After that, inside the `if` block, create a variable `user_tier` and set it to the string `Premium`. Then create a variable `strategies` set to a list of strategies containing `PercentageDiscount(10)`, `FixedAmountDiscount(5)`, and `PremiumUserDiscount()`. + +# --hints-- + +You should have an `if` statement checking if `__name__ == '__main__'`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_ifs()[0].find_conditions()[0].is_equivalent('__name__ == "__main__"')`) +}) +``` + +You should move `product = Product('Wireless Mouse', 50.0)` inside the `if` block. + +```js +({ + test: () => runPython(`assert _Node(_code).find_ifs()[0].has_stmt("product = Product('Wireless Mouse', 50.0)")`) +}) +``` + +You should set `user_tier` to the string `Premium` within the `if` block. + +```js +({ + test: () => runPython(`assert _Node(_code).find_ifs()[0].has_stmt('user_tier = "Premium"')`) +}) +``` + +You should create a list named `strategies` containing `PercentageDiscount(10)`, `FixedAmountDiscount(5)`, and `PremiumUserDiscount()`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_ifs()[0].has_stmt('strategies = [PercentageDiscount(10), FixedAmountDiscount(5), PremiumUserDiscount()]')`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + + for strategy in self.strategies: + if strategy.is_applicable(product, user_tier): + discounted = strategy.apply_discount(product) + prices.append(discounted) + + return min(prices) +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a7.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a7.md new file mode 100644 index 00000000000..653d3162dd6 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e52994bc7ea2e3dec530a7.md @@ -0,0 +1,193 @@ +--- +id: 68e52994bc7ea2e3dec530a7 +title: Step 32 +challengeType: 20 +dashedName: step-32 +--- + +# --description-- + +To complete the workshop, add a final print statement to display the best price with two decimal places using an f-string and this format: `Best price for [product.name] for [user_tier] user: $[best_price]` where `[product.name]`, `[user_tier]`, and `[best_price]` should be replaced with the actual variable values. + +An example output could be: `Best price for Wireless Buds for Premium user: $93.00`. Note the two decimal places for the price. + +With that, the discount calculator workshop is complete! + +# --hints-- + +Your output should match the format: `Best price for {product.name} for {user_tier} user: ${best_price:.2f}`. + +```js +({ + test: () => runPython(` + import io + import sys + old_stdout = sys.stdout + sys.stdout = buffer = io.StringIO() + + exec(_code) + + output = buffer.getvalue() + sys.stdout = old_stdout + + assert "Best price for Wireless Mouse for Premium user: $40.00" in output + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + + for strategy in self.strategies: + if strategy.is_applicable(product, user_tier): + discounted = strategy.apply_discount(product) + prices.append(discounted) + + return min(prices) + +if __name__ == '__main__': + product = Product('Wireless Mouse', 50.0) + user_tier = 'Premium' + + strategies = [ + PercentageDiscount(10), + FixedAmountDiscount(5), + PremiumUserDiscount() + ] + + engine = DiscountEngine(strategies) + best_price = engine.calculate_best_price(product, user_tier) +--fcc-editable-region-- + +--fcc-editable-region-- +``` + +# --solutions-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price*0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 # 20% off for premium users + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + for strategy in self.strategies: + if strategy.is_applicable(product, user_tier): + discounted = strategy.apply_discount(product) + prices.append(discounted) + return min(prices) + +if __name__ == '__main__': + product = Product('Wireless Mouse', 50.0) + user_tier = 'Premium' + strategies = [ + PercentageDiscount(10), + FixedAmountDiscount(5), + PremiumUserDiscount() + ] + engine = DiscountEngine(strategies) + best_price = engine.calculate_best_price(product, user_tier) + print(f'Best price for {product.name} for {user_tier} user: ${best_price:.2f}') +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e79d91daf8fb4808f93c47.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e79d91daf8fb4808f93c47.md new file mode 100644 index 00000000000..36d3e1b2805 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e79d91daf8fb4808f93c47.md @@ -0,0 +1,80 @@ +--- +id: 68e79d91daf8fb4808f93c47 +title: Step 15 +challengeType: 20 +dashedName: step-15 +--- + +# --description-- + +After implementing the `apply_discount` method, test your discount strategy. Create an instance of `PercentageDiscount` with `10` percent and assign it to a variable named `discount`. Then apply the discount using `discount.apply_discount(product)`, and print the result to see the discount in action. + +# --hints-- + +You should create a variable named `discount` with a `PercentageDiscount` instance. + +```js +({ + test: () => runPython(` + assert _Node(_code).has_variable("discount") + assert isinstance(discount, PercentageDiscount) + `) +}) +``` + +Your `PercentageDiscount` instance should be initialized with `10` percent. + +```js +({ + test: () => runPython(`assert discount.percent == 10`) +}) +``` + +You should call `discount.apply_discount(product)` and print the result. + +```js +({ + test: () => runPython(`assert _Node(_code).has_call("print(discount.apply_discount(product))")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) + +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e7a24b2482bd5fa88774fa.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e7a24b2482bd5fa88774fa.md new file mode 100644 index 00000000000..13c2a5a2023 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e7a24b2482bd5fa88774fa.md @@ -0,0 +1,138 @@ +--- +id: 68e7a24b2482bd5fa88774fa +title: Step 29 +challengeType: 20 +dashedName: step-29 +--- + +# --description-- + +Now, remove the following lines from the bottom of your code: + +- `print(product)` +- `discount = PercentageDiscount(10)` +- `print(discount.apply_discount(product))` +- `fixed_discount = FixedAmountDiscount(5)` +- `print(fixed_discount.apply_discount(product))` + +# --hints-- + +You should not have the line `print(product)` in your code. + +```js +({ test: () => assert.isFalse(runPython(`_Node(_code).has_call('print(product)')` +)) +}) +``` + +You should not have the line `discount = PercentageDiscount(10)` in your code. + +```js +({ test: () => assert.isFalse(runPython(`_Node(_code).has_stmt('discount = PercentageDiscount(10)')` +)) +}) +``` + +You should not have the line `print(discount.apply_discount(product))` in your code. + +```js +({ test: () => assert.isFalse(runPython(`_Node(_code).has_call('print(discount.apply_discount(product))')` +)) +}) +``` + +You should not have the line `fixed_discount = FixedAmountDiscount(5)` in your code. + +```js + +({ test: () => assert.isFalse(runPython(`_Node(_code).has_stmt('fixed_discount = FixedAmountDiscount(5)')` +)) +}) +``` + +You should not have the line `print(fixed_discount.apply_discount(product))` in your code. + +```js +({ + test: () => assert.isFalse(runPython(`_Node(_code).has_call('print(fixed_discount.apply_discount(product))')` +)) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + + for strategy in self.strategies: + if strategy.is_applicable(product, user_tier): + discounted = strategy.apply_discount(product) + prices.append(discounted) + return min(prices) + +product = Product('Wireless Mouse', 50.0) + +--fcc-editable-region-- +print(product) + +discount = PercentageDiscount(10) +print(discount.apply_discount(product)) + +fixed_discount = FixedAmountDiscount(5) +print(fixed_discount.apply_discount(product)) +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68e9207ddcba053c5cb24413.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e9207ddcba053c5cb24413.md new file mode 100644 index 00000000000..c7f4ef47a9d --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68e9207ddcba053c5cb24413.md @@ -0,0 +1,40 @@ +--- +id: 68e9207ddcba053c5cb24413 +title: Step 2 +challengeType: 20 +dashedName: step-2 +--- + +# --description-- + +In Python, type hints are optional signals that tell other developers what the data type of a variable or function is expected to be. Here is an example that adds the type hint `str` to the `name` parameter. + +```py +def greet(name: str): + return 'Hello, ' + name +``` + +For the `__init__` method parameters, update the `name` parameter to have the type hint `str` and the `price` parameter to have the type hint `float`. + +# --hints-- + +You should add the type hint `str` to the `name` parameter and the type hint `float` to the `price` parameter in the `__init__` method of the `Product` class. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").find_function("__init__").has_args("self, name: str, price: float")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +--fcc-editable-region-- +class Product: + def __init__(self, name, price): + self.name = name + self.price = price +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68ef53ef276acfa40c83cb12.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68ef53ef276acfa40c83cb12.md new file mode 100644 index 00000000000..dbefaadda71 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68ef53ef276acfa40c83cb12.md @@ -0,0 +1,47 @@ +--- +id: 68ef53ef276acfa40c83cb12 +title: Step 10 +challengeType: 20 +dashedName: step-10 +--- + +# --description-- + +Add a return type hint of `bool` to the `is_applicable` method so that it clearly indicates it returns a `bool`. + +# --hints-- + +Your `is_applicable` method should have a return type hint of `-> bool`. + +```js +({ + test: () => runPython(` + assert _Node(_code).find_class("DiscountStrategy").find_function("is_applicable").has_returns("bool") + `) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +--fcc-editable-region-- +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str): + pass +--fcc-editable-region-- +product = Product('Wireless Mouse', 50.0) +print(product) +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68efb04d5ea87cf267192ed4.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68efb04d5ea87cf267192ed4.md new file mode 100644 index 00000000000..53894d07a10 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68efb04d5ea87cf267192ed4.md @@ -0,0 +1,107 @@ +--- +id: 68efb04d5ea87cf267192ed4 +title: Step 31 +challengeType: 20 +dashedName: step-31 +--- + +# --description-- + +Create a `DiscountEngine` instance and store it in the variable `engine`. After that, calculate the best price using `calculate_best_price` method and store it in a variable named `best_price`. + +# --hints-- + +You should create a `DiscountEngine` instance with the `strategies` list. + +```js +({ + test: () => runPython(`assert _Node(_code).find_ifs()[0].has_stmt('engine = DiscountEngine(strategies)')`) +}) +``` + +You should call `engine.calculate_best_price(product, user_tier)` and store the result in `best_price`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_ifs()[0].has_stmt('best_price = engine.calculate_best_price(product, user_tier)') +`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +from abc import ABC, abstractmethod +from typing import List + +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price + + def __str__(self) -> str: + return f'{self.name} - ${self.price}' + +class DiscountStrategy(ABC): + @abstractmethod + def is_applicable(self, product: Product, user_tier: str) -> bool: + pass + + @abstractmethod + def apply_discount(self, product: Product) -> float: + pass + +class PercentageDiscount(DiscountStrategy): + def __init__(self, percent: int) -> None: + self.percent = percent + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return self.percent <= 70 + + def apply_discount(self, product: Product) -> float: + return product.price * (1 - self.percent / 100) + +class FixedAmountDiscount(DiscountStrategy): + def __init__(self, amount: int) -> None: + self.amount = amount + + def is_applicable(self, product: Product, user_tier: str) -> bool: + return product.price * 0.9 > self.amount + + def apply_discount(self, product: Product) -> float: + return product.price - self.amount + +class PremiumUserDiscount(DiscountStrategy): + def is_applicable(self, product: Product, user_tier: str) -> bool: + return user_tier.lower() == 'premium' + + def apply_discount(self, product: Product) -> float: + return product.price * 0.8 + +class DiscountEngine: + def __init__(self, strategies: List[DiscountStrategy]) -> None: + self.strategies = strategies + + def calculate_best_price(self, product: Product, user_tier: str) -> float: + prices = [product.price] + + for strategy in self.strategies: + if strategy.is_applicable(product, user_tier): + discounted = strategy.apply_discount(product) + prices.append(discounted) + + return min(prices) +--fcc-editable-region-- +if __name__ == '__main__': + product = Product('Wireless Mouse', 50.0) + user_tier = 'Premium' + + strategies = [ + PercentageDiscount(10), + FixedAmountDiscount(5), + PremiumUserDiscount() + ] +--fcc-editable-region-- +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68f1196f0fedc6f6ecc9aba6.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68f1196f0fedc6f6ecc9aba6.md new file mode 100644 index 00000000000..d84351b4de3 --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68f1196f0fedc6f6ecc9aba6.md @@ -0,0 +1,46 @@ +--- +id: 68f1196f0fedc6f6ecc9aba6 +title: Step 4 +challengeType: 20 +dashedName: step-4 +--- + +# --description-- + +In Python, a return type hint indicates the expected return type of a function or method. You do this by adding `-> return_type` after the parameter list in the method definition. + +Here is an example of a method with both parameter and return type hints whose return type is `bool`: + +```py +def example_method(self, value: int) -> bool: + pass +``` + +Other return type hints you might use include `str`, `None`, `float` and more. + +In the existing `__init__` method, add a return type hint of `None` since constructors do not return a value. + +# --hints-- + +Your `__init__` method should have a return type hint of `None`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").find_function("__init__").has_returns("None")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +--fcc-editable-region-- +class Product: + def __init__(self, name: str, price: float): + self.name = name + self.price = price +--fcc-editable-region-- + def __str__(self): + return f'{self.name} - ${self.price}' +``` diff --git a/curriculum/challenges/english/blocks/workshop-discount-calculator/68f119ad905b29ff59d858c8.md b/curriculum/challenges/english/blocks/workshop-discount-calculator/68f119ad905b29ff59d858c8.md new file mode 100644 index 00000000000..f13c96b8cdf --- /dev/null +++ b/curriculum/challenges/english/blocks/workshop-discount-calculator/68f119ad905b29ff59d858c8.md @@ -0,0 +1,35 @@ +--- +id: 68f119ad905b29ff59d858c8 +title: Step 5 +challengeType: 20 +dashedName: step-5 +--- + +# --description-- + +Just like the previous step, in your `__str__` method, add a return type hint of `str` since this method returns a string representation of the object. + +# --hints-- + +Your `__str__` method should have a return type hint of `str`. + +```js +({ + test: () => runPython(`assert _Node(_code).find_class("Product").find_function("__str__").has_returns("str")`) +}) +``` + +# --seed-- + +## --seed-contents-- + +```py +class Product: + def __init__(self, name: str, price: float) -> None: + self.name = name + self.price = price +--fcc-editable-region-- + def __str__(self): + return f'{self.name} - ${self.price}' +--fcc-editable-region-- +``` diff --git a/curriculum/structure/blocks/workshop-discount-calculator.json b/curriculum/structure/blocks/workshop-discount-calculator.json new file mode 100644 index 00000000000..2a245be5c91 --- /dev/null +++ b/curriculum/structure/blocks/workshop-discount-calculator.json @@ -0,0 +1,44 @@ +{ + "name": "Build a Discount Calculator", + "isUpcomingChange": true, + "dashedName": "workshop-discount-calculator", + "helpCategory": "Python", + "blockLayout": "challenge-grid", + "challengeOrder": [ + { "id": "68e524c94e2e4ab7b4bfc7e2", "title": "Step 1" }, + { "id": "68e9207ddcba053c5cb24413", "title": "Step 2" }, + { "id": "68e529226b0defdff26c4a89", "title": "Step 3" }, + { "id": "68f1196f0fedc6f6ecc9aba6", "title": "Step 4" }, + { "id": "68f119ad905b29ff59d858c8", "title": "Step 5" }, + { "id": "68e529226b0defdff26c4a8a", "title": "Step 6" }, + { "id": "68e529226b0defdff26c4a8b", "title": "Step 7" }, + { "id": "68e5293bd00d2fe134f58981", "title": "Step 8" }, + { "id": "68e5293bd00d2fe134f58982", "title": "Step 9" }, + { "id": "68ef53ef276acfa40c83cb12", "title": "Step 10" }, + { "id": "68e5293bd00d2fe134f58983", "title": "Step 11" }, + { "id": "68e5293bd00d2fe134f58984", "title": "Step 12" }, + { "id": "68e5293bd00d2fe134f58985", "title": "Step 13" }, + { "id": "68e5293bd00d2fe134f58986", "title": "Step 14" }, + { "id": "68e79d91daf8fb4808f93c47", "title": "Step 15" }, + { "id": "68e5293bd00d2fe134f58987", "title": "Step 16" }, + { "id": "68e5293bd00d2fe134f58988", "title": "Step 17" }, + { "id": "68e5293bd00d2fe134f58989", "title": "Step 18" }, + { "id": "68e5293bd00d2fe134f5898a", "title": "Step 19" }, + { "id": "68e52994bc7ea2e3dec5309d", "title": "Step 20" }, + { "id": "68e52994bc7ea2e3dec5309e", "title": "Step 21" }, + { "id": "68e52994bc7ea2e3dec5309f", "title": "Step 22" }, + { "id": "68e52994bc7ea2e3dec530a0", "title": "Step 23" }, + { "id": "68e52994bc7ea2e3dec530a1", "title": "Step 24" }, + { "id": "68e52994bc7ea2e3dec530a2", "title": "Step 25" }, + { "id": "68e52994bc7ea2e3dec530a3", "title": "Step 26" }, + { "id": "68e52994bc7ea2e3dec530a4", "title": "Step 27" }, + { "id": "68e52994bc7ea2e3dec530a5", "title": "Step 28" }, + { "id": "68e7a24b2482bd5fa88774fa", "title": "Step 29" }, + { "id": "68e52994bc7ea2e3dec530a6", "title": "Step 30" }, + { "id": "68efb04d5ea87cf267192ed4", "title": "Step 31" }, + { "id": "68e52994bc7ea2e3dec530a7", "title": "Step 32" } + ], + "blockLabel": "workshop", + "usesMultifileEditor": true, + "hasEditableBoundaries": true +} diff --git a/curriculum/structure/superblocks/full-stack-developer.json b/curriculum/structure/superblocks/full-stack-developer.json index 53b0707685c..2c6ac82a434 100644 --- a/curriculum/structure/superblocks/full-stack-developer.json +++ b/curriculum/structure/superblocks/full-stack-developer.json @@ -746,6 +746,7 @@ "workshop-media-catalogue", "lab-polygon-area-calculator", "lecture-understanding-abstraction", + "workshop-discount-calculator", "lab-player-interface", "review-object-oriented-programming", "quiz-object-oriented-programming"