diff --git a/.gitignore b/.gitignore index 3c3629e6..d6298e71 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.venv/ +prep \ No newline at end of file diff --git a/exercises-sprint5/bank.py b/exercises-sprint5/bank.py new file mode 100644 index 00000000..52553ce7 --- /dev/null +++ b/exercises-sprint5/bank.py @@ -0,0 +1,53 @@ +def open_account(balances: dict, name: str, amount: int) -> None: + balances[name] = amount + +def sum_balances(accounts: dict) -> int: + total = 0 + for name, pence in accounts.items(): + print(f"{name} had balance {pence}") + total += pence + return total + +def format_pence_as_string(total_pence: int) -> str: + if total_pence < 100: + return f"{total_pence}p" + pounds = int(total_pence / 100) + pence = total_pence % 100 + return f"£{pounds}.{pence:02d}" + +balances = { + "Sima": 700, + "Linn": 545, + "Georg": 831, +} + +open_account(balances, "Tobi", 913) +open_account(balances, "Olya", 713) + +total_pence = sum_balances(balances) +total_string = format_pence_as_string(total_pence) + +print(f"The bank accounts total {total_string}") + + + + +# When I ran mypy I got these errors: + +# Error 1: bank.py:24: error: Missing positional argument "amount" in call to "open_account" [call-arg] +# The function open_account expects three arguments but only two have been passed. We need to add one more argument. + +# Error 2: Argument 1 has incompatible type "str"; expected "dict" +# By adding balances as the first argument, this will be solved as well. + +# Error 3: Argument 2 has incompatible type "float"; expected "str" +# 9.13 is in the wrong position, and it's a float not an int. + +# Error 4: Missing positional argument "amount" in call to "open_account" +# Same problem as Error 1 missing balances and wrong types. + +# Error 5: bank.py:25: error: Argument 1 to "open_account" has incompatible type "str"; expected "dict[Any, Any]" +# Line 25 has two bugs: balances should be passed as the first argument, and the third argument is passed as a string which should be an int. + +# Error 6: bank.py:28: error: Name "format_pence_as_str" is not defined [name-defined] +# Typo! Should be format_pence_as_string. \ No newline at end of file diff --git a/exercises-sprint5/double.py b/exercises-sprint5/double.py new file mode 100644 index 00000000..df19254a --- /dev/null +++ b/exercises-sprint5/double.py @@ -0,0 +1,21 @@ +def double(value): + return value * 2 + + +double("22") +print(double("22")) + + +# Coming from JS, I predicted that Python might behave the same way. +# Under the hood, without throwing an error, Python would concatenate +# the string "22" with itself, and the result would be "2222". + +# correction: later on I realised JavaScript and Python behave differently JS coerces so "22" * 2 returns 44 in JS whereas Python repeats the string according to the number, so "22" * 2 returns "2222". + + +def double(number): + return number * 3 + +print(double(10)) + +# As mentioned in prep section either the name should be triple or the code should be *2 \ No newline at end of file diff --git a/exercises-sprint5/enums.py b/exercises-sprint5/enums.py new file mode 100644 index 00000000..90cbd965 --- /dev/null +++ b/exercises-sprint5/enums.py @@ -0,0 +1,175 @@ +""" +Laptop Library System +This program helps library users find available laptops matching their +preferred operating system. It demonstrates enum usage, type safety, +and input validation. +""" + + +from dataclasses import dataclass +from enum import Enum +from typing import List +import sys + + +# -------------------------------------------------------------------- +# ENUM DEFINITIONS +# -------------------------------------------------------------------- +""" + Represents valid operating systems for laptops. + Using enums prevents string comparison issues (case, typos, spaces). +""" + +class OperatingSystem(Enum): + UBUNTU = "Ubuntu" + MACOS = "macOS" + ARCH = "Arch Linux" + +# -------------------------------------------------------------------- +# DATA CLASSES +# -------------------------------------------------------------------- +#Represents a library user with their preferences. + +@dataclass(frozen=True) +class Person: + name: str + age: int + preferred_operating_systems: OperatingSystem + + +#Represents a laptop available in the library. +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + +# -------------------------------------------------------------------- +# BUSINESS LOGIC +# -------------------------------------------------------------------- +""" + Filters laptops to find those matching the user's preferred OS. + + Args: + laptops: List of available laptops + person: User with OS preference + + Returns: + List of laptops matching the user's preferred OS +""" +def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: + possible_laptops = [] + for laptop in laptops: + if laptop.operating_system == person.preferred_operating_systems: + possible_laptops.append(laptop) + return possible_laptops + + +# -------------------------------------------------------------------- +# MAIN PROGRAM - USER INPUT AND PROCESSING +# -------------------------------------------------------------------- +# Get user input as strings first (raw input) +name = input("Enter your name: ") +age_str = input("Enter your age: ") +os_str = input("Enter preferred OS: ") + +# -------------------------------------------------------------------- +# INPUT VALIDATION AND CONVERSION +# -------------------------------------------------------------------- + +# Convert age from string to integer with error handling +# Output to stderr as per requirements, exit with non-zero code +try: + age = int(age_str) +except ValueError: + + print(f"Error: {age_str} is not a valid age", file=sys.stderr) + sys.exit(1) + +# Convert OS string to enum with error handling +try: + os = OperatingSystem(os_str) +except ValueError: + print(f"Error: '{os_str}' is not a valid OS. Choose: Ubuntu, macOS, Arch Linux", file=sys.stderr) + sys.exit(1) + + +# Create Person object from validated input +# Now we know age is a valid int and os is a valid OperatingSystem +person = Person(name=name, age=age, preferred_operating_systems=os) + +# -------------------------------------------------------------------- +# DATA - AVAILABLE LAPTOPS +# -------------------------------------------------------------------- +# Pre-defined list of laptops in the library (as per exercise requirements) +laptops = [ + Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system=OperatingSystem.ARCH), + Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), + Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), + Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), +] + + +# -------------------------------------------------------------------- +# PROCESSING - FIND MATCHING LAPTOPS +# -------------------------------------------------------------------- +# Find laptops matching user's preferred OS +possible_laptops = find_possible_laptops(laptops, person) + +# Requirement: Tell user how many laptops have their OS +print(f"we have {len(possible_laptops)} laptops with {os.value}") + + +# -------------------------------------------------------------------- +# COUNTING LAPTOPS PER OPERATING SYSTEM +# -------------------------------------------------------------------- +# Count laptops for each OS to find which has most +arch_count = 0 +ubuntu_count = 0 +macos_count = 0 +for laptop in laptops: + if laptop.operating_system == OperatingSystem.ARCH: + arch_count += 1 + elif laptop.operating_system == OperatingSystem.UBUNTU: + ubuntu_count += 1 + else: + macos_count += 1 + + + + +# -------------------------------------------------------------------- +# FINDING THE OPERATING SYSTEM WITH MOST LAPTOPS +# -------------------------------------------------------------------- +# Store counts in dictionary for easy max calculation +os_counts = { + OperatingSystem.ARCH: arch_count, + OperatingSystem.UBUNTU: ubuntu_count, + OperatingSystem.MACOS: macos_count +} + +# Find OS with maximum laptops +max_os = max(os_counts, key=os_counts.get) # Gets the OS with highest count +max_count = os_counts[max_os] # The actual count + + +# -------------------------------------------------------------------- +# COMPARISON AND SUGGESTION LOGIC +# -------------------------------------------------------------------- +# Get user's OS choice and count +os_user= person.preferred_operating_systems +user_count = os_counts[os_user] + + + +# Requirement: Suggest alternative if another OS has MORE laptops +# Check: 1) Different OS, AND 2) Has strictly more laptops (> not >=) +if os_user != max_os and max_count > user_count: + print(f" if you're willing to accept {max_os.value} " + + f"you'd have {max_count} laptops available " + + f"(vs {user_count} for {os_user.value})") + + + diff --git a/exercises-sprint5/generics.py b/exercises-sprint5/generics.py new file mode 100644 index 00000000..ab9606fc --- /dev/null +++ b/exercises-sprint5/generics.py @@ -0,0 +1,32 @@ +#✍️exercise +#Fix the above code so that it works. You must not change the print on line 17 - we do want to print the children’s ages. (Feel free to invent the ages of Imran’s children.) + + + +from dataclasses import dataclass + +@dataclass(frozen=True) +class Person: + name: str + children: list["Person"] + age: int + +fatma = Person(name="Fatma", age=7, children=[]) +aisha = Person(name="Aisha", age=10, children=[]) + +imran = Person(name="Imran",age=50, children=[fatma, aisha]) + +def print_family_tree(person: Person) -> None: + print(person.name) + for child in person.children: + print(f"- {child.name} ({child.age})") + +print_family_tree(imran) + + +# When I first ran mypy with `children: list`, it found no errors. +# This is because mypy didn't know what type of items were in the list. +# After changing to `children: List["Person"]` (using generics), +# mypy could identify that each child is a Person object. +# Now it caught the bug: Person has no "age" attribute. +# I fixed this by adding `age: int` to the Person class. \ No newline at end of file diff --git a/exercises-sprint5/inheritance.py b/exercises-sprint5/inheritance.py new file mode 100644 index 00000000..1664ab2f --- /dev/null +++ b/exercises-sprint5/inheritance.py @@ -0,0 +1,62 @@ +""" ✍️exercise +Play computer with this code. Predict what you expect each line will do. Then run the code and check your predictions. (If any lines cause errors, you may need to comment them out to check later lines). +""" + +# PREDICTION: This defines a base class called Parent +class Parent: + # PREDICTION: Constructor that sets first_name and last_name attributes + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + +# PREDICTION: Method that returns full name as "first last" + def get_name(self) -> str: + return f"{self.first_name} {self.last_name}" + + +# PREDICTION: Child class inherits everything from Parent class +class Child(Parent): + # PREDICTION: Constructor calls parent's constructor then adds new attribute + def __init__(self, first_name: str, last_name: str): + super().__init__(first_name, last_name) # PREDICTION: Calls Parent.__init__ + self.previous_last_names = [] # PREDICTION: Creates empty list for this instance + +# PREDICTION: Method to change last name and track previous names + def change_last_name(self, last_name) -> None: + self.previous_last_names.append(self.last_name) + self.last_name = last_name + +# PREDICTION: Method that returns full name with maiden name note if changed + def get_full_name(self) -> str: + suffix = "" + if len(self.previous_last_names) > 0: + suffix = f" (née {self.previous_last_names[0]})" + return f"{self.first_name} {self.last_name}{suffix}" + + +# PREDICTION: Creates Child instance with names "Elizaveta" "Alekseeva" +person1 = Child("Elizaveta", "Alekseeva") +# PREDICTION: Prints "Elizaveta Alekseeva" (calls inherited get_name() from Parent) +print(person1.get_name()) +# PREDICTION: Prints "Elizaveta Alekseeva" (no suffix since no name change yet) +print(person1.get_full_name()) +# PREDICTION: Changes last name to "Tyurina", adds "Alekseeva" to previous_last_names +person1.change_last_name("Tyurina") +# PREDICTION: Prints "Elizaveta Tyurina" (updated last name) +print(person1.get_name()) +# PREDICTION: Prints "Elizaveta Tyurina (née Alekseeva)" (shows maiden name) +print(person1.get_full_name()) + + +# PREDICTION: Creates Parent instance (NOT Child) with same names +person2 = Parent("Elizaveta", "Alekseeva") +# PREDICTION: Prints "Elizaveta Alekseeva" (Parent has get_name() method) +print(person2.get_name()) +# PREDICTION: ERROR! Parent class doesn't have get_full_name() method +print(person2.get_full_name()) +# PREDICTION: ERROR! Parent class doesn't have change_last_name() method +person2.change_last_name("Tyurina") +# PREDICTION: Won't reach this line due to previous error +print(person2.get_name()) +# PREDICTION: Won't reach this line due to previous error +print(person2.get_full_name()) \ No newline at end of file diff --git a/exercises-sprint5/person.py b/exercises-sprint5/person.py new file mode 100644 index 00000000..91d4d091 --- /dev/null +++ b/exercises-sprint5/person.py @@ -0,0 +1,38 @@ +from datetime import date + +class Person: + def __init__(self, name: str, date_of_birth: date, preferred_operating_system: str): + self.name = name + self.date_of_birth = date_of_birth + self.preferred_operating_system = preferred_operating_system + + def is_adult(self) -> bool: + today_date = date.today().year + birth_year = self.date_of_birth.year + age = today_date - birth_year + + + return age >= 18 + +imran = Person("Imran", date(2000, 6, 20), "Ubuntu") +print(imran.name) + + +eliza = Person("Eliza", date(1987, 12, 10), "Arch Linux") +print(eliza.name) + + + +# when I ran mypy I got this errors: +# person.py:9: error: "Person" has no attribute "address" [attr-defined] +# person.py:13: error: "Person" has no attribute "address" [attr-defined] +# So I will remove .address line as mypy caught rightly that Person class does not have address property. It shows the benefit of using classes as earlier without defining a class mypy could not catch the same bug. + + +#def get_address(person: Person) -> str: + #return person.address + +#print(is_adult(imran)) +# When I ran mypy with is_adult function I got no error as age is a property of Person class + +# however when I added get_address function and ran mypy again I got this error: person.py:27: error: "Person" has no attribute "address" [attr-defined]Found 1 error in 1 file (checked 1 source file) which was expected as Person class has no address attribute \ No newline at end of file diff --git a/exercises-sprint5/person2.py b/exercises-sprint5/person2.py new file mode 100644 index 00000000..98b5ae3b --- /dev/null +++ b/exercises-sprint5/person2.py @@ -0,0 +1,24 @@ +#✍️exercise +#Write a Person class using @datatype which uses a datetime.date for date of birth, rather than an int for age. + +from dataclasses import dataclass +from datetime import date + + +@dataclass(frozen=True) +class Person: + name: str + date_of_birth: date + preferred_operating_system: str + + + def is_adult(self) -> bool: + today_date = date.today().year + birth_year = self.date_of_birth.year + age = today_date - birth_year + return age >= 18 + + +imran = Person("Imran", date(2000, 6, 20), "Ubuntu") +print(imran) +print(imran.is_adult()) \ No newline at end of file diff --git a/exercises-sprint5/refactoring.py b/exercises-sprint5/refactoring.py new file mode 100644 index 00000000..38230b38 --- /dev/null +++ b/exercises-sprint5/refactoring.py @@ -0,0 +1,68 @@ +#✍️exercise +#Try changing the type annotation of Person.preferred_operating_system from str to List[str]. +#Run mypy on the code. +#It tells us different places that our code is now wrong, because we’re passing values of the wrong type. +#We probably also want to rename our field - lists are plural. Rename the field to preferred_operating_systems. +#Run mypy again. +#Fix all of the places that mypy tells you need changing. Make sure the program works as you’d expect. + +from dataclasses import dataclass +from typing import List + +@dataclass(frozen=True) +class Person: + name: str + age: int + preferred_operating_systems: List[str] + + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: str + + +def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: + possible_laptops = [] + for laptop in laptops: + if laptop.operating_system in person.preferred_operating_systems: + possible_laptops.append(laptop) + return possible_laptops + + +people = [ + Person(name="Imran", age=22, preferred_operating_systems=["Ubuntu"]), + Person(name="Eliza", age=34, preferred_operating_systems=["Arch Linux"]), +] + +laptops = [ + Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system="Arch Linux"), + Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="Ubuntu"), + Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="ubuntu"), + Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system="macOS"), +] + +for person in people: + possible_laptops = find_possible_laptops(laptops, person) + print(f"Possible laptops for {person.name}: {possible_laptops}") + + + +# Original code had: preferred_operating_system: str +# Changed to: preferred_operating_systems: List[str] +# +# When I ran mypy after the change, it found 3 errors: +# 1. Line 23: Function was using old field name (preferred_operating_system) +# 2. Line 29: Creating Imran with old field name and string instead of list +# 3. Line 30: Creating Eliza with old field name and string instead of list +# +# Fixes needed: +# - Rename the field everywhere +# - Change == to 'in' (because now it's a list) +# - Change strings to lists: "Ubuntu" → ["Ubuntu"] +# +# The point: mypy acts as a safety net when refactoring. +# It tells you exactly where to update your code. \ No newline at end of file