Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions sprint-5-laptop-allocation/laptop_allocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from dataclasses import dataclass
from enum import Enum
from typing import List, Dict, Optional, Tuple

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect you started using a Tuple and changed it to a List? It's good practice to remove unwanted imports.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out. I've removed unwanted imports.

import itertools
import sys

class OperatingSystem(Enum):
MACOS = "macOS"
ARCH = "Arch Linux"
UBUNTU = "Ubuntu"

@dataclass(frozen=True)
class Person:
name: str
age: int
preferred_operating_system: List[OperatingSystem]

# custom hash to hash the immutable version of the list
def __hash__(self) -> int:
return hash((self.name, self.age, tuple(self.preferred_operating_system)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! The non-hashability of List caught many people out (including me, initially - I'm more C# than Python). This is a nice workaround.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OracPrime Thanks for reviewing my PR and for the feedback. I really appreciate it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was good code - you may notice I took the liberty of quoting it in the Nov SDC slack channel!



@dataclass(frozen=True)
class Laptop:
id: int
manufacturer: str
model: str
screen_size_in_inches: float
operating_system: OperatingSystem

# =====define parse operating system======
def parse_operating_system(raw: str) -> OperatingSystem:
normalized = raw.strip().lower()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good!


aliases = {
"macos": OperatingSystem.MACOS,
"mac": OperatingSystem.MACOS,
"arch": OperatingSystem.ARCH,
"arch linux": OperatingSystem.ARCH,
"ubuntu": OperatingSystem.UBUNTU,
}

if normalized not in aliases:
valid = ", ".join(sorted(aliases.keys()))
print(f"Error: invalid operating system. Try one of: {valid}", file=sys.stderr)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the inclusion of allowed values in the error message. Like the fact that you're using stderr

sys.exit(1)

return aliases[normalized]

# ======= sadness helper ============
def sadness(person: Person, laptop: Laptop) -> int:
laptop_os = laptop.operating_system
preferences = person.preferred_operating_system
if laptop_os not in preferences:
return 100
return preferences.index(laptop_os)

# ======= Laptop allocation ============
def allocate_laptops(people: List[Person], laptops: List[Laptop]) -> Dict[Person, Laptop]:
if len(laptops) < len(people):
raise ValueError("Not enough laptops to allocate one per person.")

best_assignment: Optional[Dict[Person, Laptop]] = None
best_total_sadness: Optional[int] = None

for chosen_laptops in itertools.combinations(laptops, len(people)):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will work for 5 laptops. What happens if you've got 20 laptops?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @OracPrime , you’re absolutely right. The original brute-force approach I implemented only works for very small number of laptops as you pointed out, because it explores all possible permutations and does not scale well.

I’ve refactored my code to use a greedy approach, which allows it to scale to much larger inputs (for example, 20 or more laptops). This approach doesn’t always find the perfect solution, but it gives a reasonable result and runs efficiently even as the number of laptops increases.

Thanks for taking the time to review my code and for your helpful feedback.

for permuted_laptops in itertools.permutations(chosen_laptops):
total = 0
for i in range(len(people)):
person = people[i]
laptop = permuted_laptops[i]
total += sadness(person, laptop)

if best_total_sadness is None or total < best_total_sadness:
best_total_sadness = total
best_assignment = {people[i]: permuted_laptops[i] for i in range(len(people))}

if best_assignment is None:
raise RuntimeError("Allocation failed unexpectedly.")

return best_assignment

# ======define main ================
def main() -> None:
laptops = [
Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13.0, operating_system=OperatingSystem.ARCH),
Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15.0, operating_system=OperatingSystem.UBUNTU),
Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15.0, operating_system=OperatingSystem.UBUNTU),
Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13.0, operating_system=OperatingSystem.MACOS),
]

people = [
Person(name="Imran", age=22, preferred_operating_system=[OperatingSystem.UBUNTU, OperatingSystem.ARCH, OperatingSystem.MACOS]),
Person(name="Eliza", age=34, preferred_operating_system=[OperatingSystem.ARCH, OperatingSystem.UBUNTU])
]

allocation = allocate_laptops(people, laptops)

# Print results
for person, laptop in allocation.items():
print(f"{person.name}: Laptop {laptop.id} ({laptop.operating_system.value}) sadness={sadness(person, laptop)}")

if __name__ == "__main__":
main()