Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.venv
141 changes: 141 additions & 0 deletions laptop_allocation/laptop_allocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from dataclasses import dataclass
from enum import Enum
from typing import List
from typing import Dict

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

# ============================================================================
# DATA CLASSES
# ============================================================================
@dataclass(frozen=True)
class Person:
name: str
age: int
# Sorted in order of preference, most preferred is first.
preferred_operating_system: tuple

Choose a reason for hiding this comment

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

Why a tuple rather than a list?

Copy link
Author

Choose a reason for hiding this comment

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

I used a tuple because Person needs to be hashable to serve as a dictionary key. If I used a list, Python would raise a TypeError: unhashable type: list

Choose a reason for hiding this comment

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

My version of python doesn't complain. What Python version are you using?

Choose a reason for hiding this comment

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

Ah, OK, I now understand it. The data being passed in is Tuples, so when I have preferred_operating_system as a list, Python ignores the type hint and stores the Tuple instead. I'm too used to strongly typed languages!

Choose a reason for hiding this comment

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

Take a gold star. Your Python knowledge exceeds mine in this respect (and probably many others, I'm guessing)



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

# ============================================================================
# HELPER FUNCTION
# ============================================================================
"""
Calculates how "sad" a person would be with a given laptop allocation.

Sadness is defined as the position (0-indexed) of the laptop's OS
in the person's preference list. For example:
- If preferences are [UBUNTU, ARCH, MACOS] and they get UBUNTU: sadness = 0
- If preferences are [UBUNTU, ARCH, MACOS] and they get ARCH: sadness = 1
- If preferences are [UBUNTU, ARCH, MACOS] and they get MACOS: sadness = 2
- If they get an OS not in their preferences: sadness = 100

Args:
person: The person receiving the laptop
laptop: The laptop being allocated

Returns:
An integer representing sadness (0 = most happy, 100 = not in preferences)
"""

def calculate_sadness(person:Person, laptop:Laptop)-> int:
if laptop.operating_system in person.preferred_operating_system:
# Find the index (position) of this OS in their preference list
sadness = person.preferred_operating_system.index(laptop.operating_system)
return sadness
else:
# OS not in preferences = very sad
return 100

# ============================================================================
# TEST DATA
# ============================================================================

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),
Laptop(id=5, manufacturer="Apple", model="macBook Pro", screen_size_in_inches=16,
operating_system=OperatingSystem.MACOS),
Laptop(id=6, manufacturer="Lenovo", model="ThinkPad", screen_size_in_inches=14,
operating_system=OperatingSystem.UBUNTU),
Laptop(id=7, manufacturer="System76", model="Lemur Pro", screen_size_in_inches=15,
operating_system=OperatingSystem.ARCH),
Laptop(id=8, manufacturer="Framework", model="Framework 13", screen_size_in_inches=13,
operating_system=OperatingSystem.UBUNTU),
]

people = [
Person(name="Imran", age=22, preferred_operating_system=(OperatingSystem.UBUNTU, OperatingSystem.MACOS)),
Person(name="Eliza", age=34, preferred_operating_system=(OperatingSystem.ARCH, OperatingSystem.UBUNTU)),
Person(name="Marcus", age=28, preferred_operating_system=(OperatingSystem.MACOS, OperatingSystem.UBUNTU)),
Person(name="Sofia", age=31, preferred_operating_system=(OperatingSystem.UBUNTU,)),
Person(name="James", age=25, preferred_operating_system=(OperatingSystem.ARCH, OperatingSystem.MACOS)),
Person(name="Nina", age=29, preferred_operating_system=(OperatingSystem.MACOS, OperatingSystem.ARCH, OperatingSystem.UBUNTU)),
]



# ============================================================================
# TESTING
# ============================================================================

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")


people_sorted = sorted(people, key = lambda p: len(p.preferred_operating_system))

Choose a reason for hiding this comment

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

Nice touch - haven't seen anyone else do this, but it is a clever tweak to the algorithm

Copy link
Author

Choose a reason for hiding this comment

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

Thank you! I looked into more complex approaches like the Hungarian Algorithm, but found it a bit overwhelming at this stage for me. I decided to stick with greedy approach to ensure the pickest user get a compatible laptop before the more flexible users. With this we can minimise the overall sadness score for the group.


result={}
available_laptops = laptops.copy()

Choose a reason for hiding this comment

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

Good spot that you need the copy if you don't want to change the original


for person in people_sorted:
smallest_sadness = float("inf")
best_laptop = None

for laptop in available_laptops:
sadness = calculate_sadness(person, laptop)
if sadness < smallest_sadness:
smallest_sadness = sadness
best_laptop = laptop

Choose a reason for hiding this comment

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

You could of course break out of the loop if sadness is 0

Copy link
Author

Choose a reason for hiding this comment

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

That is a very good point, thanks for mentioning it. I've added a break when sadness == 0 to avoid unnecessary iterations.



if smallest_sadness == 0:
break

result[person]= best_laptop
available_laptops.remove(best_laptop)

return result


allocation = allocate_laptops(people, laptops)

print("\n" + "="*50)
print("FINAL ALLOCATION:")
print("="*50)
for person, laptop in allocation.items():
print(f"{person.name}: {laptop.manufacturer} {laptop.model} ({laptop.operating_system.value})")