adding Data Structures and Algorithms II C950
BIN
D682_AIOFCS/.DS_Store
vendored
BIN
Data Structures and Algorithms II — C950/.DS_Store
vendored
Normal file
BIN
Data Structures and Algorithms II — C950/SLC downtown map.docx
Normal file
BIN
Data Structures and Algorithms II — C950/Task 2 - Python files + Screenshots/.DS_Store
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.14" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.14" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Task 2 - Python files.iml" filepath="$PROJECT_DIR$/.idea/Task 2 - Python files.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 557 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 557 KiB |
@ -0,0 +1,96 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
import csv
|
||||
from package import Package
|
||||
from hash_table import ChainingHashTable
|
||||
|
||||
|
||||
def load_packages(filepath):
|
||||
# Let's read packages.csv and load all package records into a hash table.
|
||||
table = ChainingHashTable()
|
||||
|
||||
with open(filepath, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
for row in reader:
|
||||
# Build a Package object from each CSV row
|
||||
pkg = Package(
|
||||
package_id=row["PackageID"],
|
||||
address=row["Address"],
|
||||
city=row["City"],
|
||||
state=row["State"],
|
||||
zip_code=row["Zip"],
|
||||
deadline=row["Deadline"],
|
||||
weight=row["Weight"],
|
||||
notes=row["Notes"],
|
||||
)
|
||||
# Insert into the hash table using package ID as the key
|
||||
table.insert(pkg.package_id, pkg)
|
||||
|
||||
# Let's return the hash table keyed by package ID
|
||||
return table
|
||||
|
||||
|
||||
def load_distances(filepath):
|
||||
# Let's read distances.csv and build a symmetric 2-D distance matrix.
|
||||
address_list = []
|
||||
raw_rows = []
|
||||
|
||||
with open(filepath, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
|
||||
# First row contains the column headers (location name + address)
|
||||
header = next(reader)
|
||||
|
||||
# Extract the short street address from each header cell.
|
||||
# Header cells may contain a full location name followed by the
|
||||
# street address separated by whitespace; we take the last part.
|
||||
for cell in header[1:]: # skip the first "Address" label column
|
||||
parts = [p.strip() for p in cell.replace("\n", " ").split(" ") if p.strip()]
|
||||
address_list.append(parts[-1] if parts else cell.strip())
|
||||
|
||||
# Read the remaining rows — each row is one location's distances
|
||||
for row in reader:
|
||||
raw_rows.append(row[1:]) # drop the first column (location label)
|
||||
|
||||
m = len(address_list)
|
||||
|
||||
# Create an m×m matrix filled with 0.0
|
||||
matrix = [[0.0] * m for _ in range(m)]
|
||||
|
||||
# Fill in the matrix - the CSV only has the lower triangle
|
||||
for i, row in enumerate(raw_rows):
|
||||
for j, cell in enumerate(row):
|
||||
if cell.strip() == "" or cell is None:
|
||||
continue
|
||||
dist = float(cell)
|
||||
matrix[i][j] = dist
|
||||
matrix[j][i] = dist
|
||||
|
||||
return address_list, matrix
|
||||
|
||||
|
||||
def get_distance(address_list, distance_matrix, addr_a, addr_b):
|
||||
|
||||
# Let's find the index of an address using flexible matching.
|
||||
def find_index(addr):
|
||||
addr_clean = addr.strip().lower()
|
||||
|
||||
# Pass 1: exact match (fastest, covers most cases)
|
||||
for i, a in enumerate(address_list):
|
||||
if a.strip().lower() == addr_clean:
|
||||
return i
|
||||
|
||||
# Pass 2: substring match (handles minor formatting differences)
|
||||
for i, a in enumerate(address_list):
|
||||
if addr_clean in a.strip().lower() or a.strip().lower() in addr_clean:
|
||||
return i
|
||||
|
||||
# Raise an error if the address cannot be found.
|
||||
raise ValueError(f"Address not found in distance table: '{addr}'")
|
||||
|
||||
i = find_index(addr_a)
|
||||
j = find_index(addr_b)
|
||||
return distance_matrix[i][j]
|
||||
@ -0,0 +1,28 @@
|
||||
Address,"Western Governors University 4001 South 700 East, Salt Lake City, UT 84107",International Peace Gardens 1060 Dalton Ave S,Sugar House Park 1330 2100 S,Taylorsville-Bennion Heritage City Gov Off 1488 4800 S,Salt Lake City Division of Health Services 177 W Price Ave,South Salt Lake Public Works 195 W Oakland Ave,Salt Lake City Streets and Sanitation 2010 W 500 S,Deker Lake 2300 Parkway Blvd,Salt Lake City Ottinger Hall 233 Canyon Rd,Columbus Library 2530 S 500 E,Taylorsville City Hall 2600 Taylorsville Blvd,South Salt Lake Police 2835 Main St,Council Hall 300 State St,Redwood Park 3060 Lester St,Salt Lake County Mental Health 3148 S 1100 W,Salt Lake County/United Police Dept 3365 S 900 W,West Valley Prosecutor 3575 W Valley Central Sta bus Loop,Housing Auth. of Salt Lake County 3595 Main St,Utah DMV Administrative Office 380 W 2880 S,Third District Juvenile Court 410 S State St,Cottonwood Regional Softball Complex 4300 S 1300 E,Holiday City Office 4580 S 2300 E,Murray City Museum 5025 State St,Valley Regional Softball Complex 5100 South 2700 West,City Center of Rock Springs 5383 South 900 East #104,Rice Terrace Pavilion Park 600 E 900 South,Wheeler Historic Farm 6351 South 900 East
|
||||
"Western Governors University 4001 South 700 East, Salt Lake City, UT 84107",0,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
International Peace Gardens 1060 Dalton Ave S,7.2,0,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Sugar House Park 1330 2100 S,3.8,7.1,0,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Taylorsville-Bennion Heritage City Gov Off 1488 4800 S,11,6.4,9.2,0,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Salt Lake City Division of Health Services 177 W Price Ave,2.2,6,4.4,5.6,0,,,,,,,,,,,,,,,,,,,,,,
|
||||
South Salt Lake Public Works 195 W Oakland Ave,3.5,4.8,2.8,6.9,1.9,0,,,,,,,,,,,,,,,,,,,,,
|
||||
Salt Lake City Streets and Sanitation 2010 W 500 S,10.9,1.6,8.6,8.6,7.9,6.3,0,,,,,,,,,,,,,,,,,,,,
|
||||
Deker Lake 2300 Parkway Blvd,8.6,2.8,6.3,4,5.1,4.3,4,0,,,,,,,,,,,,,,,,,,,
|
||||
Salt Lake City Ottinger Hall 233 Canyon Rd,7.6,4.8,5.3,11.1,7.5,4.5,4.2,7.7,0,,,,,,,,,,,,,,,,,,
|
||||
Columbus Library 2530 S 500 E,2.8,6.3,1.6,7.3,2.6,1.5,8,9.3,4.8,0,,,,,,,,,,,,,,,,,
|
||||
Taylorsville City Hall 2600 Taylorsville Blvd,6.4,7.3,10.4,1,6.5,8.7,8.6,4.6,11.9,9.4,0,,,,,,,,,,,,,,,,
|
||||
South Salt Lake Police 2835 Main St,3.2,5.3,3,6.4,1.5,0.8,6.9,4.8,4.7,1.1,7.3,0,,,,,,,,,,,,,,,
|
||||
Council Hall 300 State St,7.6,4.8,5.3,11.1,7.5,4.5,4.2,7.7,0.6,5.1,12,4.7,0,,,,,,,,,,,,,,
|
||||
Redwood Park 3060 Lester St,5.2,3,6.5,3.9,3.2,3.9,4.2,1.6,7.6,4.6,4.9,3.5,7.3,0,,,,,,,,,,,,,
|
||||
Salt Lake County Mental Health 3148 S 1100 W,4.4,4.6,5.6,4.3,2.4,3,8,3.3,7.8,3.7,5.2,2.6,7.8,1.3,0,,,,,,,,,,,,
|
||||
Salt Lake County/United Police Dept 3365 S 900 W,3.66112816434,4.5,5.8,4.4,2.7,3.8,5.8,3.4,6.6,4,5.4,2.9,6.6,1.5,0.6,0,,,,,,,,,,,
|
||||
West Valley Prosecutor 3575 W Valley Central Sta bus Loop,7.6,7.4,5.7,7.2,1.4,5.7,7.2,3.1,7.2,6.7,8.1,6.3,7.2,4,6.4,5.6,0,,,,,,,,,,
|
||||
Housing Auth. of Salt Lake County 3595 Main St,2,6,4.1,5.3,0.5,1.9,7.7,5.1,5.9,2.3,6.2,1.2,5.9,3.2,2.4,1.6,7.1,0,,,,,,,,,
|
||||
Utah DMV Administrative Office 380 W 2880 S,3.6,5,3.6,6,1.7,1.1,6.6,4.6,5.4,1.8,6.9,1,5.4,3,2.2,1.7,6.1,1.6,0,,,,,,,,
|
||||
Third District Juvenile Court 410 S State St,6.5,4.8,4.3,10.6,6.5,3.5,3.2,6.7,1,4.1,11.5,3.7,1,6.9,6.8,6.4,7.2,4.9,4.4,0,,,,,,,
|
||||
Cottonwood Regional Softball Complex 4300 S 1300 E,1.9,9.5,3.3,5.9,3.2,4.9,11.2,8.1,8.5,3.8,6.9,4.1,8.5,6.2,5.3,4.9,10.6,3,4.6,7.5,0,,,,,,
|
||||
Holiday City Office 4580 S 2300 E,3.4,10.9,5,7.4,5.2,6.9,12.7,10.4,10.3,5.8,8.3,6.2,10.3,8.2,7.4,6.9,12,5,6.6,9.3,2,0,,,,,
|
||||
Murray City Museum 5025 State St,2.4,8.3,6.1,4.7,2.5,4.2,10,7.8,7.8,4.3,4.1,3.4,7.8,5.5,4.6,4.2,9.4,2.3,3.9,6.8,2.9,4.4,0,,,,
|
||||
Valley Regional Softball Complex 5100 South 2700 West,6.4,6.9,9.7,0.6,6,9,8.2,4.2,11.5,7.8,0.4,6.9,11.5,4.4,4.8,5.6,7.5,5.5,6.5,11.4,6.4,7.9,4.5,0,,,
|
||||
City Center of Rock Springs 5383 South 900 East #104,2.4,10,6.1,6.4,4.2,5.9,11.7,9.5,9.5,4.8,4.9,5.2,9.5,7.2,6.3,5.9,11.1,4,5.6,8.5,2.8,3.4,1.7,5.4,0,,
|
||||
Rice Terrace Pavilion Park 600 E 900 South,5,4.4,2.8,10.1,5.4,3.5,5.1,6.2,2.8,3.2,11,3.7,2.8,6.4,6.5,5.7,6.2,5.1,4.3,1.8,6,7.9,6.8,10.6,7,0,
|
||||
Wheeler Historic Farm 6351 South 900 East,3.6,13,7.4,10.1,5.5,7.2,14.2,10.7,14.1,6,6.8,6.4,14.1,10.5,8.8,8.4,13.6,5.2,6.9,13.1,4.1,4.7,3.1,7.8,1.3,8.3,0
|
||||
|
@ -0,0 +1,66 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
|
||||
class ChainingHashTable:
|
||||
|
||||
# We are going to assigns all buckets with an empty list.
|
||||
def __init__(self, initial_capacity=40):
|
||||
# Let's initialize the hash table with empty bucket list entries.
|
||||
self.table = []
|
||||
for i in range(initial_capacity):
|
||||
self.table.append([])
|
||||
|
||||
# This func inserts a new item into the hash table.
|
||||
def insert(self, key, item):
|
||||
# Get the bucket list where this item will go.
|
||||
bucket = hash(key) % len(self.table)
|
||||
bucket_list = self.table[bucket]
|
||||
|
||||
# Let's check if the key already exists and update it if found.
|
||||
for kv in bucket_list:
|
||||
if kv[0] == key:
|
||||
kv[1] = item
|
||||
return True
|
||||
|
||||
# otherwise, we insert the item to the end of list.
|
||||
key_value = [key, item]
|
||||
bucket_list.append(key_value)
|
||||
return True
|
||||
|
||||
# This func returns the item if found, or None if not found.
|
||||
def lookup(self, key):
|
||||
# Let's find the correct bucket for this key.
|
||||
bucket = hash(key) % len(self.table)
|
||||
bucket_list = self.table[bucket]
|
||||
|
||||
# Let's search through the bucket for the matching key.
|
||||
for kv in bucket_list:
|
||||
if kv[0] == key:
|
||||
return kv[1]
|
||||
return None
|
||||
|
||||
# This func removes an item with matching key from the hash table.
|
||||
def remove(self, key):
|
||||
# Let's find the bucket where this key should exist.
|
||||
bucket = hash(key) % len(self.table)
|
||||
bucket_list = self.table[bucket]
|
||||
|
||||
# Let's search for and remove the matching key-value pair.
|
||||
for kv in bucket_list:
|
||||
if kv[0] == key:
|
||||
bucket_list.remove([kv[0], kv[1]])
|
||||
return print('Package ', key, 'successfully deleted.')
|
||||
|
||||
# Let's return None if the key is not found.
|
||||
return None
|
||||
|
||||
# This func returns a list of all (key, value) tuples stored in the hash table.
|
||||
# Let's use this to iterate through all packages when needed.
|
||||
def get_all(self):
|
||||
result = []
|
||||
for bucket_list in self.table:
|
||||
for kv in bucket_list:
|
||||
result.append((kv[0], kv[1]))
|
||||
return result
|
||||
@ -0,0 +1,238 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
# Algorithm : Nearest-Neighbor (greedy self-adjusting)
|
||||
# Data Structure: Custom Chaining Hash Table
|
||||
|
||||
# Let's import the required libraries and modules
|
||||
import os
|
||||
from datetime import datetime
|
||||
from data_loader import load_packages, load_distances
|
||||
from truck import Truck
|
||||
|
||||
|
||||
|
||||
# Let's define file paths for package and distance data (relative to this script)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PACKAGES_CSV = os.path.join(BASE_DIR, "packages.csv")
|
||||
DISTANCES_CSV = os.path.join(BASE_DIR, "distances.csv")
|
||||
|
||||
|
||||
# Let's load all necessary data for the program
|
||||
def initialize():
|
||||
package_table = load_packages(PACKAGES_CSV)
|
||||
address_list, distance_matrix = load_distances(DISTANCES_CSV)
|
||||
return package_table, address_list, distance_matrix
|
||||
|
||||
|
||||
|
||||
|
||||
# Let's assign packages to trucks based on delivery constraints
|
||||
# Manual truck loading (satisfies all constraints)
|
||||
def load_trucks(package_table, address_list, distance_matrix):
|
||||
"""Assign packages to trucks and run the delivery algorithm."""
|
||||
|
||||
# Let's set the start of the delivery day (8:00 AM)
|
||||
start_of_day = datetime(2024, 1, 1, 8, 0, 0) # 8:00 AM
|
||||
|
||||
# ---- Truck 1 ----
|
||||
# Let's load priority packages and grouped deliveries
|
||||
truck1_packages = [
|
||||
1, 13, 14, 15, 16, 19, 20, 29, 30, 31, 34, 37, 40,
|
||||
2, 4, 5, 7, 10,
|
||||
]
|
||||
|
||||
# ---- Truck 2 ----
|
||||
# Let's load truck-specific and delayed packages
|
||||
truck2_packages = [
|
||||
3, 18, 36, 38,
|
||||
6, 28, 32,
|
||||
9,
|
||||
8, 11, 12, 17, 21, 23, 24,
|
||||
]
|
||||
|
||||
# ---- Truck 3 ----
|
||||
# Let's assign any remaining packages to Truck 3
|
||||
all_assigned = set(truck1_packages + truck2_packages)
|
||||
all_packages = [pid for pid, _ in package_table.get_all()]
|
||||
truck3_packages = [pid for pid in all_packages if pid not in all_assigned]
|
||||
|
||||
# Let's initialize Truck 1 (departs at 8:00 AM)
|
||||
truck1 = Truck(
|
||||
truck_id=1,
|
||||
packages=truck1_packages,
|
||||
depart_time=start_of_day,
|
||||
package_table=package_table,
|
||||
address_list=address_list,
|
||||
distance_matrix=distance_matrix,
|
||||
)
|
||||
|
||||
# Let's delay Truck 2 departure until 9:05 AM for late packages
|
||||
truck2_depart = datetime(2024, 1, 1, 9, 5, 0)
|
||||
truck2 = Truck(
|
||||
truck_id=2,
|
||||
packages=truck2_packages,
|
||||
depart_time=truck2_depart,
|
||||
package_table=package_table,
|
||||
address_list=address_list,
|
||||
distance_matrix=distance_matrix,
|
||||
)
|
||||
|
||||
# Let's run deliveries for Truck 1 and Truck 2
|
||||
truck1.deliver_all(return_to_hub=True)
|
||||
truck2.deliver_all(return_to_hub=True)
|
||||
|
||||
# Let's assign Truck 3 to depart when Truck 1 returns
|
||||
truck3_depart = truck1.current_time
|
||||
truck3 = Truck(
|
||||
truck_id=3,
|
||||
packages=truck3_packages,
|
||||
depart_time=truck3_depart,
|
||||
package_table=package_table,
|
||||
address_list=address_list,
|
||||
distance_matrix=distance_matrix,
|
||||
)
|
||||
|
||||
# Let's complete deliveries for Truck 3
|
||||
truck3.deliver_all(return_to_hub=True)
|
||||
|
||||
return truck1, truck2, truck3
|
||||
|
||||
|
||||
# CLI INTERFACE
|
||||
# Let's display the status of all packages at a given time
|
||||
def print_all_status(package_table, query_time):
|
||||
"""Print the delivery status of all 40 packages at a given time."""
|
||||
print(f"\n{'='*75}")
|
||||
print(f" Package Status at {query_time.strftime('%I:%M %p')}")
|
||||
print(f"{'='*75}")
|
||||
print(f"{'ID':<4} {'Address':<38} {'Deadline':<10} {'Truck':<6} {'Status':<12} {'Delivered'}")
|
||||
print(f"{'-'*75}")
|
||||
|
||||
# Sort packages by ID so output is easy to read
|
||||
all_packages = sorted(package_table.get_all(), key=lambda x: x[0])
|
||||
for pid, pkg in all_packages:
|
||||
# status_at() compares query_time to departure and delivery times
|
||||
status = pkg.status_at(query_time)
|
||||
delivered_str = (
|
||||
pkg.delivery_time.strftime("%I:%M %p")
|
||||
if status == "Delivered" and pkg.delivery_time
|
||||
else "—"
|
||||
)
|
||||
truck_str = str(pkg.truck_id) if pkg.truck_id else "—"
|
||||
print(
|
||||
f"{pid:<4} {pkg.address:<38} {pkg.deadline:<10} "
|
||||
f"{truck_str:<6} {status:<12} {delivered_str}"
|
||||
)
|
||||
|
||||
|
||||
# Let's display the status of a single package
|
||||
def print_single_status(package_table, pkg_id, query_time):
|
||||
"""Print full details for a single package at a given time."""
|
||||
# Use the hash table lookup function to retrieve the package by ID
|
||||
pkg = package_table.lookup(pkg_id)
|
||||
if pkg is None:
|
||||
print(f"Package {pkg_id} not found.")
|
||||
return
|
||||
|
||||
status = pkg.status_at(query_time)
|
||||
delivered_str = (
|
||||
pkg.delivery_time.strftime("%I:%M %p")
|
||||
if status == "Delivered" and pkg.delivery_time
|
||||
else "Not yet delivered"
|
||||
)
|
||||
|
||||
# Display all data components associated with this package ID
|
||||
print(f"\n{'='*50}")
|
||||
print(f" Package {pkg.package_id} — Status at {query_time.strftime('%I:%M %p')}")
|
||||
print(f"{'='*50}")
|
||||
print(f" Address : {pkg.address}, {pkg.city}, {pkg.state} {pkg.zip_code}")
|
||||
print(f" Deadline : {pkg.deadline}")
|
||||
print(f" Weight : {pkg.weight} kg")
|
||||
print(f" Truck : {pkg.truck_id if pkg.truck_id else '—'}")
|
||||
print(f" Notes : {pkg.notes if pkg.notes else 'None'}")
|
||||
print(f" Status : {status}")
|
||||
print(f" Delivered: {delivered_str}")
|
||||
|
||||
|
||||
# Let's convert user input into a valid time format
|
||||
def parse_time(time_str):
|
||||
"""Parse a user-entered time string into a datetime object."""
|
||||
reference_date = datetime(2024, 1, 1)
|
||||
for fmt in ["%I:%M %p", "%H:%M", "%I:%M%p"]:
|
||||
try:
|
||||
t = datetime.strptime(time_str.strip(), fmt)
|
||||
return reference_date.replace(hour=t.hour, minute=t.minute, second=0)
|
||||
except ValueError:
|
||||
continue
|
||||
return None # Return None if no format matched
|
||||
|
||||
|
||||
# Let's create the main user interface loop
|
||||
def main(package_table, truck1, truck2, truck3):
|
||||
# Calculate and display total combined mileage at startup
|
||||
total_miles = truck1.total_miles + truck2.total_miles + truck3.total_miles
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(" WGUPS Package Routing Program")
|
||||
print("="*60)
|
||||
print(f" Truck 1 mileage : {truck1.total_miles:.2f} miles")
|
||||
print(f" Truck 2 mileage : {truck2.total_miles:.2f} miles")
|
||||
print(f" Truck 3 mileage : {truck3.total_miles:.2f} miles")
|
||||
print(f" Total mileage : {total_miles:.2f} miles")
|
||||
print("="*60)
|
||||
|
||||
# Menu loop — continues until the user enters 4 to exit
|
||||
while True:
|
||||
print("\nOptions:")
|
||||
print(" 1 - View status of ALL packages at a specific time")
|
||||
print(" 2 - View status of a SINGLE package at a specific time")
|
||||
print(" 3 - Print all truck routes")
|
||||
print(" 4 - Exit")
|
||||
choice = input("\nEnter choice (1/2/3/4): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
# Prompt for a time and display all 40 package statuses
|
||||
time_str = input("Enter time (e.g. 9:00 AM or 13:00): ")
|
||||
query_time = parse_time(time_str)
|
||||
if query_time is None:
|
||||
print("Invalid time format. Try '9:00 AM' or '09:00'.")
|
||||
else:
|
||||
print_all_status(package_table, query_time)
|
||||
|
||||
elif choice == "2":
|
||||
# Prompt for a package ID and time, then display that package
|
||||
try:
|
||||
pkg_id = int(input("Enter package ID (1-40): "))
|
||||
except ValueError:
|
||||
print("Invalid package ID.")
|
||||
continue
|
||||
time_str = input("Enter time (e.g. 9:00 AM or 13:00): ")
|
||||
query_time = parse_time(time_str)
|
||||
if query_time is None:
|
||||
print("Invalid time format.")
|
||||
else:
|
||||
print_single_status(package_table, pkg_id, query_time)
|
||||
|
||||
elif choice == "3":
|
||||
# Print the full ordered route for each truck with delivery times
|
||||
truck1.print_route()
|
||||
truck2.print_route()
|
||||
truck3.print_route()
|
||||
total = truck1.total_miles + truck2.total_miles + truck3.total_miles
|
||||
print(f"\nCombined total: {total:.2f} miles")
|
||||
|
||||
elif choice == "4":
|
||||
print("Thank you for using WGUPS Routing Program, Goodbye.")
|
||||
break
|
||||
|
||||
else:
|
||||
print("Invalid choice. Please enter 1, 2, 3, or 4.")
|
||||
|
||||
|
||||
# Let's start the program execution
|
||||
if __name__ == "__main__":
|
||||
package_table, address_list, distance_matrix = initialize()
|
||||
truck1, truck2, truck3 = load_trucks(package_table, address_list, distance_matrix)
|
||||
main(package_table, truck1, truck2, truck3)
|
||||
@ -0,0 +1,79 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
|
||||
class Package:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package_id,
|
||||
address,
|
||||
city,
|
||||
state,
|
||||
zip_code,
|
||||
deadline,
|
||||
weight,
|
||||
notes="",
|
||||
):
|
||||
# Let's store the core delivery data from the Package File
|
||||
self.package_id = int(package_id)
|
||||
self.address = address.strip()
|
||||
self.city = city.strip()
|
||||
self.state = state.strip()
|
||||
self.zip_code = str(zip_code).strip()
|
||||
self.deadline = deadline.strip()
|
||||
self.weight = weight
|
||||
self.notes = notes.strip()
|
||||
|
||||
# Let's initialize the status tracking fields for the simulation
|
||||
self.status = "At Hub"
|
||||
self.departure_time = None
|
||||
self.delivery_time = None
|
||||
self.truck_id = None
|
||||
|
||||
|
||||
# STATUS UPDATES
|
||||
|
||||
def mark_en_route(self, departure_time, truck_id):
|
||||
|
||||
# Called when a truck departs the hub carrying this package.
|
||||
self.status = "En Route"
|
||||
self.departure_time = departure_time
|
||||
self.truck_id = truck_id
|
||||
|
||||
def mark_delivered(self, delivery_time):
|
||||
|
||||
# Called when the truck arrives at this package's delivery address.
|
||||
|
||||
self.status = "Delivered"
|
||||
self.delivery_time = delivery_time
|
||||
|
||||
# TIME-BASED STATUS QUERY
|
||||
|
||||
def status_at(self, query_time):
|
||||
|
||||
# Return the status this package had at a specific point in time.
|
||||
|
||||
# Package is still at the hub if no truck has departed with it yet,
|
||||
# or if the query time is before the truck's departure.
|
||||
if self.departure_time is None or query_time < self.departure_time:
|
||||
return "At Hub"
|
||||
|
||||
# Package is en route if the truck has left but delivery hasn't happened yet
|
||||
if self.delivery_time is None or query_time < self.delivery_time:
|
||||
return "En Route"
|
||||
|
||||
# Otherwise the package was already delivered by query_time
|
||||
return "Delivered"
|
||||
|
||||
# DISPLAY
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"Package({self.package_id:>2}, "
|
||||
f"{self.address:<38} "
|
||||
f"Deadline: {self.deadline:<8} "
|
||||
f"Wt: {self.weight:<4} "
|
||||
f"Status: {self.status})"
|
||||
)
|
||||
@ -0,0 +1,41 @@
|
||||
PackageID,Address,City,State,Zip,Deadline,Weight,Notes
|
||||
1,195 W Oakland Ave,Salt Lake City,UT,84115,10:30:00,21,
|
||||
2,2530 S 500 E,Salt Lake City,UT,84106,EOD,44,
|
||||
3,233 Canyon Rd,Salt Lake City,UT,84103,EOD,2,Can only be on truck 2
|
||||
4,380 W 2880 S,Salt Lake City,UT,84115,EOD,4,
|
||||
5,410 S State St,Salt Lake City,UT,84111,EOD,5,
|
||||
6,3060 Lester St,West Valley City,UT,84119,10:30:00,88,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
7,1330 2100 S,Salt Lake City,UT,84106,EOD,8,
|
||||
8,300 State St,Salt Lake City,UT,84103,EOD,9,
|
||||
9,300 State St,Salt Lake City,UT,84103,EOD,2,Wrong address listed
|
||||
10,600 E 900 South,Salt Lake City,UT,84105,EOD,1,
|
||||
11,2600 Taylorsville Blvd,Salt Lake City,UT,84118,EOD,1,
|
||||
12,3575 W Valley Central Station bus Loop,West Valley City,UT,84119,EOD,1,
|
||||
13,2010 W 500 S,Salt Lake City,UT,84104,10:30:00,2,
|
||||
14,4300 S 1300 E,Millcreek,UT,84117,10:30:00,88,"Must be delivered with 15, 19"
|
||||
15,4580 S 2300 E,Holladay,UT,84117,09:00:00,4,
|
||||
16,4580 S 2300 E,Holladay,UT,84117,10:30:00,88,"Must be delivered with 13, 19"
|
||||
17,3148 S 1100 W,Salt Lake City,UT,84119,EOD,2,
|
||||
18,1488 4800 S,Salt Lake City,UT,84123,EOD,6,Can only be on truck 2
|
||||
19,177 W Price Ave,Salt Lake City,UT,84115,EOD,37,
|
||||
20,3595 Main St,Salt Lake City,UT,84115,10:30:00,37,"Must be delivered with 13, 15"
|
||||
21,3595 Main St,Salt Lake City,UT,84115,EOD,3,
|
||||
22,6351 South 900 East,Murray,UT,84121,EOD,2,
|
||||
23,5100 South 2700 West,Salt Lake City,UT,84118,EOD,5,
|
||||
24,5025 State St,Murray,UT,84107,EOD,7,
|
||||
25,5383 South 900 East #104,Salt Lake City,UT,84117,10:30:00,7,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
26,5383 South 900 East #104,Salt Lake City,UT,84117,EOD,25,
|
||||
27,1060 Dalton Ave S,Salt Lake City,UT,84104,EOD,5,
|
||||
28,2835 Main St,Salt Lake City,UT,84115,EOD,7,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
29,1330 2100 S,Salt Lake City,UT,84106,10:30:00,2,
|
||||
30,300 State St,Salt Lake City,UT,84103,10:30:00,1,
|
||||
31,3365 S 900 W,Salt Lake City,UT,84119,10:30:00,1,
|
||||
32,3365 S 900 W,Salt Lake City,UT,84119,EOD,1,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
33,2530 S 500 E,Salt Lake City,UT,84106,EOD,1,
|
||||
34,4580 S 2300 E,Holladay,UT,84117,10:30:00,2,
|
||||
35,1060 Dalton Ave S,Salt Lake City,UT,84104,EOD,88,
|
||||
36,2300 Parkway Blvd,West Valley City,UT,84119,EOD,88,Can only be on truck 2
|
||||
37,410 S State St,Salt Lake City,UT,84111,10:30:00,2,
|
||||
38,410 S State St,Salt Lake City,UT,84111,EOD,9,Can only be on truck 2
|
||||
39,2010 W 500 S,Salt Lake City,UT,84104,EOD,9,
|
||||
40,380 W 2880 S,Salt Lake City,UT,84115,10:30:00,45,
|
||||
|
@ -0,0 +1,160 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
SPEED_MPH = 18 # all trucks travel at 18 mph
|
||||
HUB_ADDRESS = "Salt Lake City, UT 84107" # WGU hub — matches the distance table
|
||||
|
||||
|
||||
class Truck:
|
||||
# Let's initialize the truck with assigned packages and starting conditions.
|
||||
def __init__(self, truck_id, packages, depart_time, package_table,
|
||||
address_list, distance_matrix):
|
||||
|
||||
self.truck_id = truck_id
|
||||
self.package_ids = list(packages) # copy so original list is not modified
|
||||
self.depart_time = depart_time
|
||||
self.package_table = package_table
|
||||
self.address_list = address_list
|
||||
self.distance_matrix = distance_matrix
|
||||
|
||||
# Simulation state — updated as deliver_all() runs
|
||||
self.current_address = HUB_ADDRESS # truck starts at the hub
|
||||
self.current_time = depart_time # clock starts at departure time
|
||||
self.total_miles = 0.0 # accumulates mileage during delivery
|
||||
self.route = [] # list of (package_id, address, arrival_time)
|
||||
|
||||
|
||||
# Distance and time helpers
|
||||
def _distance(self, addr_a, addr_b):
|
||||
|
||||
def find_index(addr):
|
||||
addr_clean = addr.strip().lower()
|
||||
|
||||
# Pass 1: exact match
|
||||
for i, a in enumerate(self.address_list):
|
||||
if a.strip().lower() == addr_clean:
|
||||
return i
|
||||
|
||||
# Pass 2: substring match (handles abbreviations and extra words)
|
||||
for i, a in enumerate(self.address_list):
|
||||
if addr_clean in a.strip().lower() or a.strip().lower() in addr_clean:
|
||||
return i
|
||||
|
||||
# Pass 3: match on the first two tokens (street number + street name)
|
||||
tokens = addr_clean.split()
|
||||
if tokens:
|
||||
prefix = " ".join(tokens[:2])
|
||||
for i, a in enumerate(self.address_list):
|
||||
if a.strip().lower().startswith(prefix):
|
||||
return i
|
||||
|
||||
raise ValueError(f"Address not found in distance table: '{addr}'")
|
||||
|
||||
i = find_index(addr_a)
|
||||
j = find_index(addr_b)
|
||||
return self.distance_matrix[i][j]
|
||||
|
||||
def _travel_time(self, miles):
|
||||
|
||||
hours = miles / SPEED_MPH
|
||||
return timedelta(hours=hours)
|
||||
|
||||
|
||||
# Core nearest-neighbor delivery algorithm
|
||||
def deliver_all(self, return_to_hub=False):
|
||||
|
||||
# Step 1 — Mark all packages on this truck as En Route at departure time
|
||||
for pid in self.package_ids:
|
||||
pkg = self.package_table.lookup(pid)
|
||||
if pkg:
|
||||
pkg.mark_en_route(self.depart_time, self.truck_id)
|
||||
|
||||
# Work from a copy so the original package_ids list is preserved
|
||||
remaining = list(self.package_ids)
|
||||
|
||||
# Step 2 — Nearest-neighbor delivery loop
|
||||
while remaining:
|
||||
|
||||
# Inner scan: find the package with the shortest distance
|
||||
# from the truck's current location
|
||||
nearest_pid = None
|
||||
nearest_dist = float("inf") # start with infinity so any real distance wins
|
||||
|
||||
for pid in remaining:
|
||||
pkg = self.package_table.lookup(pid)
|
||||
if pkg is None:
|
||||
continue
|
||||
|
||||
# Get the correct address for this package at this time
|
||||
# (handles Package 9's address correction at 10:20 AM)
|
||||
address = self._resolve_address(pid, self.current_time)
|
||||
dist = self._distance(self.current_address, address)
|
||||
|
||||
# Update nearest if this package is closer than the current best
|
||||
if dist < nearest_dist:
|
||||
nearest_dist = dist
|
||||
nearest_pid = pid
|
||||
|
||||
if nearest_pid is None:
|
||||
break # safety check — should not happen with valid data
|
||||
|
||||
# Step 2b — Drive to the nearest package's address
|
||||
pkg = self.package_table.lookup(nearest_pid)
|
||||
dest_address = self._resolve_address(nearest_pid, self.current_time)
|
||||
travel = self._travel_time(nearest_dist)
|
||||
|
||||
self.current_time += travel # advance the simulated clock
|
||||
self.total_miles += nearest_dist # accumulate mileage
|
||||
self.current_address = dest_address # move truck to new location
|
||||
|
||||
# Step 2c — Mark the package as delivered at the current time
|
||||
pkg.mark_delivered(self.current_time)
|
||||
self.route.append((nearest_pid, dest_address, self.current_time))
|
||||
|
||||
# Step 2d — Remove from remaining list
|
||||
remaining.remove(nearest_pid)
|
||||
|
||||
# Step 3 — Return to hub if needed (for driver reassignment to Truck 3)
|
||||
if return_to_hub:
|
||||
dist_home = self._distance(self.current_address, HUB_ADDRESS)
|
||||
self.total_miles += dist_home
|
||||
self.current_time += self._travel_time(dist_home)
|
||||
self.current_address = HUB_ADDRESS
|
||||
|
||||
# Package 9 address correction
|
||||
def _resolve_address(self, package_id, current_time):
|
||||
|
||||
pkg = self.package_table.lookup(package_id)
|
||||
|
||||
if package_id == 9:
|
||||
# Define the correction time as 10:20 AM on the simulation date
|
||||
correction_time = self.depart_time.replace(
|
||||
hour=10, minute=20, second=0, microsecond=0
|
||||
)
|
||||
if current_time >= correction_time:
|
||||
return "410 S State St" # corrected address after 10:20 AM
|
||||
else:
|
||||
return pkg.address # wrong address before 10:20 AM
|
||||
|
||||
return pkg.address # all other packages return their address unchanged
|
||||
|
||||
|
||||
# Route summary display
|
||||
def print_route(self):
|
||||
|
||||
print(f"\n=== Truck {self.truck_id} Route ===")
|
||||
print(f"Departed hub : {self.depart_time.strftime('%I:%M %p')}")
|
||||
for pid, addr, arrival in self.route:
|
||||
pkg = self.package_table.lookup(pid)
|
||||
deadline = pkg.deadline if pkg else "?"
|
||||
print(
|
||||
f" Pkg {pid:>2} -> {addr:<40} "
|
||||
f"Arrived: {arrival.strftime('%I:%M %p')} "
|
||||
f"Deadline: {deadline}"
|
||||
)
|
||||
print(f"Total miles : {self.total_miles:.2f}")
|
||||
print(f"Finished at : {self.current_time.strftime('%I:%M %p')}")
|
||||
@ -0,0 +1,96 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
import csv
|
||||
from package import Package
|
||||
from hash_table import ChainingHashTable
|
||||
|
||||
|
||||
def load_packages(filepath):
|
||||
# Let's read packages.csv and load all package records into a hash table.
|
||||
table = ChainingHashTable()
|
||||
|
||||
with open(filepath, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
for row in reader:
|
||||
# Build a Package object from each CSV row
|
||||
pkg = Package(
|
||||
package_id=row["PackageID"],
|
||||
address=row["Address"],
|
||||
city=row["City"],
|
||||
state=row["State"],
|
||||
zip_code=row["Zip"],
|
||||
deadline=row["Deadline"],
|
||||
weight=row["Weight"],
|
||||
notes=row["Notes"],
|
||||
)
|
||||
# Insert into the hash table using package ID as the key
|
||||
table.insert(pkg.package_id, pkg)
|
||||
|
||||
# Let's return the hash table keyed by package ID
|
||||
return table
|
||||
|
||||
|
||||
def load_distances(filepath):
|
||||
# Let's read distances.csv and build a symmetric 2-D distance matrix.
|
||||
address_list = []
|
||||
raw_rows = []
|
||||
|
||||
with open(filepath, newline="", encoding="utf-8") as f:
|
||||
reader = csv.reader(f)
|
||||
|
||||
# First row contains the column headers (location name + address)
|
||||
header = next(reader)
|
||||
|
||||
# Extract the short street address from each header cell.
|
||||
# Header cells may contain a full location name followed by the
|
||||
# street address separated by whitespace; we take the last part.
|
||||
for cell in header[1:]: # skip the first "Address" label column
|
||||
parts = [p.strip() for p in cell.replace("\n", " ").split(" ") if p.strip()]
|
||||
address_list.append(parts[-1] if parts else cell.strip())
|
||||
|
||||
# Read the remaining rows — each row is one location's distances
|
||||
for row in reader:
|
||||
raw_rows.append(row[1:]) # drop the first column (location label)
|
||||
|
||||
m = len(address_list)
|
||||
|
||||
# Create an m×m matrix filled with 0.0
|
||||
matrix = [[0.0] * m for _ in range(m)]
|
||||
|
||||
# Fill in the matrix - the CSV only has the lower triangle
|
||||
for i, row in enumerate(raw_rows):
|
||||
for j, cell in enumerate(row):
|
||||
if cell.strip() == "" or cell is None:
|
||||
continue
|
||||
dist = float(cell)
|
||||
matrix[i][j] = dist
|
||||
matrix[j][i] = dist
|
||||
|
||||
return address_list, matrix
|
||||
|
||||
|
||||
def get_distance(address_list, distance_matrix, addr_a, addr_b):
|
||||
|
||||
# Let's find the index of an address using flexible matching.
|
||||
def find_index(addr):
|
||||
addr_clean = addr.strip().lower()
|
||||
|
||||
# Pass 1: exact match (fastest, covers most cases)
|
||||
for i, a in enumerate(address_list):
|
||||
if a.strip().lower() == addr_clean:
|
||||
return i
|
||||
|
||||
# Pass 2: substring match (handles minor formatting differences)
|
||||
for i, a in enumerate(address_list):
|
||||
if addr_clean in a.strip().lower() or a.strip().lower() in addr_clean:
|
||||
return i
|
||||
|
||||
# Raise an error if the address cannot be found.
|
||||
raise ValueError(f"Address not found in distance table: '{addr}'")
|
||||
|
||||
i = find_index(addr_a)
|
||||
j = find_index(addr_b)
|
||||
return distance_matrix[i][j]
|
||||
@ -0,0 +1,28 @@
|
||||
Address,"Western Governors University 4001 South 700 East, Salt Lake City, UT 84107",International Peace Gardens 1060 Dalton Ave S,Sugar House Park 1330 2100 S,Taylorsville-Bennion Heritage City Gov Off 1488 4800 S,Salt Lake City Division of Health Services 177 W Price Ave,South Salt Lake Public Works 195 W Oakland Ave,Salt Lake City Streets and Sanitation 2010 W 500 S,Deker Lake 2300 Parkway Blvd,Salt Lake City Ottinger Hall 233 Canyon Rd,Columbus Library 2530 S 500 E,Taylorsville City Hall 2600 Taylorsville Blvd,South Salt Lake Police 2835 Main St,Council Hall 300 State St,Redwood Park 3060 Lester St,Salt Lake County Mental Health 3148 S 1100 W,Salt Lake County/United Police Dept 3365 S 900 W,West Valley Prosecutor 3575 W Valley Central Sta bus Loop,Housing Auth. of Salt Lake County 3595 Main St,Utah DMV Administrative Office 380 W 2880 S,Third District Juvenile Court 410 S State St,Cottonwood Regional Softball Complex 4300 S 1300 E,Holiday City Office 4580 S 2300 E,Murray City Museum 5025 State St,Valley Regional Softball Complex 5100 South 2700 West,City Center of Rock Springs 5383 South 900 East #104,Rice Terrace Pavilion Park 600 E 900 South,Wheeler Historic Farm 6351 South 900 East
|
||||
"Western Governors University 4001 South 700 East, Salt Lake City, UT 84107",0,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
International Peace Gardens 1060 Dalton Ave S,7.2,0,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Sugar House Park 1330 2100 S,3.8,7.1,0,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Taylorsville-Bennion Heritage City Gov Off 1488 4800 S,11,6.4,9.2,0,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Salt Lake City Division of Health Services 177 W Price Ave,2.2,6,4.4,5.6,0,,,,,,,,,,,,,,,,,,,,,,
|
||||
South Salt Lake Public Works 195 W Oakland Ave,3.5,4.8,2.8,6.9,1.9,0,,,,,,,,,,,,,,,,,,,,,
|
||||
Salt Lake City Streets and Sanitation 2010 W 500 S,10.9,1.6,8.6,8.6,7.9,6.3,0,,,,,,,,,,,,,,,,,,,,
|
||||
Deker Lake 2300 Parkway Blvd,8.6,2.8,6.3,4,5.1,4.3,4,0,,,,,,,,,,,,,,,,,,,
|
||||
Salt Lake City Ottinger Hall 233 Canyon Rd,7.6,4.8,5.3,11.1,7.5,4.5,4.2,7.7,0,,,,,,,,,,,,,,,,,,
|
||||
Columbus Library 2530 S 500 E,2.8,6.3,1.6,7.3,2.6,1.5,8,9.3,4.8,0,,,,,,,,,,,,,,,,,
|
||||
Taylorsville City Hall 2600 Taylorsville Blvd,6.4,7.3,10.4,1,6.5,8.7,8.6,4.6,11.9,9.4,0,,,,,,,,,,,,,,,,
|
||||
South Salt Lake Police 2835 Main St,3.2,5.3,3,6.4,1.5,0.8,6.9,4.8,4.7,1.1,7.3,0,,,,,,,,,,,,,,,
|
||||
Council Hall 300 State St,7.6,4.8,5.3,11.1,7.5,4.5,4.2,7.7,0.6,5.1,12,4.7,0,,,,,,,,,,,,,,
|
||||
Redwood Park 3060 Lester St,5.2,3,6.5,3.9,3.2,3.9,4.2,1.6,7.6,4.6,4.9,3.5,7.3,0,,,,,,,,,,,,,
|
||||
Salt Lake County Mental Health 3148 S 1100 W,4.4,4.6,5.6,4.3,2.4,3,8,3.3,7.8,3.7,5.2,2.6,7.8,1.3,0,,,,,,,,,,,,
|
||||
Salt Lake County/United Police Dept 3365 S 900 W,3.66112816434,4.5,5.8,4.4,2.7,3.8,5.8,3.4,6.6,4,5.4,2.9,6.6,1.5,0.6,0,,,,,,,,,,,
|
||||
West Valley Prosecutor 3575 W Valley Central Sta bus Loop,7.6,7.4,5.7,7.2,1.4,5.7,7.2,3.1,7.2,6.7,8.1,6.3,7.2,4,6.4,5.6,0,,,,,,,,,,
|
||||
Housing Auth. of Salt Lake County 3595 Main St,2,6,4.1,5.3,0.5,1.9,7.7,5.1,5.9,2.3,6.2,1.2,5.9,3.2,2.4,1.6,7.1,0,,,,,,,,,
|
||||
Utah DMV Administrative Office 380 W 2880 S,3.6,5,3.6,6,1.7,1.1,6.6,4.6,5.4,1.8,6.9,1,5.4,3,2.2,1.7,6.1,1.6,0,,,,,,,,
|
||||
Third District Juvenile Court 410 S State St,6.5,4.8,4.3,10.6,6.5,3.5,3.2,6.7,1,4.1,11.5,3.7,1,6.9,6.8,6.4,7.2,4.9,4.4,0,,,,,,,
|
||||
Cottonwood Regional Softball Complex 4300 S 1300 E,1.9,9.5,3.3,5.9,3.2,4.9,11.2,8.1,8.5,3.8,6.9,4.1,8.5,6.2,5.3,4.9,10.6,3,4.6,7.5,0,,,,,,
|
||||
Holiday City Office 4580 S 2300 E,3.4,10.9,5,7.4,5.2,6.9,12.7,10.4,10.3,5.8,8.3,6.2,10.3,8.2,7.4,6.9,12,5,6.6,9.3,2,0,,,,,
|
||||
Murray City Museum 5025 State St,2.4,8.3,6.1,4.7,2.5,4.2,10,7.8,7.8,4.3,4.1,3.4,7.8,5.5,4.6,4.2,9.4,2.3,3.9,6.8,2.9,4.4,0,,,,
|
||||
Valley Regional Softball Complex 5100 South 2700 West,6.4,6.9,9.7,0.6,6,9,8.2,4.2,11.5,7.8,0.4,6.9,11.5,4.4,4.8,5.6,7.5,5.5,6.5,11.4,6.4,7.9,4.5,0,,,
|
||||
City Center of Rock Springs 5383 South 900 East #104,2.4,10,6.1,6.4,4.2,5.9,11.7,9.5,9.5,4.8,4.9,5.2,9.5,7.2,6.3,5.9,11.1,4,5.6,8.5,2.8,3.4,1.7,5.4,0,,
|
||||
Rice Terrace Pavilion Park 600 E 900 South,5,4.4,2.8,10.1,5.4,3.5,5.1,6.2,2.8,3.2,11,3.7,2.8,6.4,6.5,5.7,6.2,5.1,4.3,1.8,6,7.9,6.8,10.6,7,0,
|
||||
Wheeler Historic Farm 6351 South 900 East,3.6,13,7.4,10.1,5.5,7.2,14.2,10.7,14.1,6,6.8,6.4,14.1,10.5,8.8,8.4,13.6,5.2,6.9,13.1,4.1,4.7,3.1,7.8,1.3,8.3,0
|
||||
|
@ -0,0 +1,66 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
|
||||
class ChainingHashTable:
|
||||
|
||||
# We are going to assigns all buckets with an empty list.
|
||||
def __init__(self, initial_capacity=40):
|
||||
# Let's initialize the hash table with empty bucket list entries.
|
||||
self.table = []
|
||||
for i in range(initial_capacity):
|
||||
self.table.append([])
|
||||
|
||||
# This func inserts a new item into the hash table.
|
||||
def insert(self, key, item):
|
||||
# Get the bucket list where this item will go.
|
||||
bucket = hash(key) % len(self.table)
|
||||
bucket_list = self.table[bucket]
|
||||
|
||||
# Let's check if the key already exists and update it if found.
|
||||
for kv in bucket_list:
|
||||
if kv[0] == key:
|
||||
kv[1] = item
|
||||
return True
|
||||
|
||||
# otherwise, we insert the item to the end of list.
|
||||
key_value = [key, item]
|
||||
bucket_list.append(key_value)
|
||||
return True
|
||||
|
||||
# This func returns the item if found, or None if not found.
|
||||
def lookup(self, key):
|
||||
# Let's find the correct bucket for this key.
|
||||
bucket = hash(key) % len(self.table)
|
||||
bucket_list = self.table[bucket]
|
||||
|
||||
# Let's search through the bucket for the matching key.
|
||||
for kv in bucket_list:
|
||||
if kv[0] == key:
|
||||
return kv[1]
|
||||
return None
|
||||
|
||||
# This func removes an item with matching key from the hash table.
|
||||
def remove(self, key):
|
||||
# Let's find the bucket where this key should exist.
|
||||
bucket = hash(key) % len(self.table)
|
||||
bucket_list = self.table[bucket]
|
||||
|
||||
# Let's search for and remove the matching key-value pair.
|
||||
for kv in bucket_list:
|
||||
if kv[0] == key:
|
||||
bucket_list.remove([kv[0], kv[1]])
|
||||
return print('Package ', key, 'successfully deleted.')
|
||||
|
||||
# Let's return None if the key is not found.
|
||||
return None
|
||||
|
||||
# This func returns a list of all (key, value) tuples stored in the hash table.
|
||||
# Let's use this to iterate through all packages when needed.
|
||||
def get_all(self):
|
||||
result = []
|
||||
for bucket_list in self.table:
|
||||
for kv in bucket_list:
|
||||
result.append((kv[0], kv[1]))
|
||||
return result
|
||||
@ -0,0 +1,238 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
# Algorithm : Nearest-Neighbor (greedy self-adjusting)
|
||||
# Data Structure: Custom Chaining Hash Table
|
||||
|
||||
# Let's import the required libraries and modules
|
||||
import os
|
||||
from datetime import datetime
|
||||
from data_loader import load_packages, load_distances
|
||||
from truck import Truck
|
||||
|
||||
|
||||
|
||||
# Let's define file paths for package and distance data (relative to this script)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PACKAGES_CSV = os.path.join(BASE_DIR, "packages.csv")
|
||||
DISTANCES_CSV = os.path.join(BASE_DIR, "distances.csv")
|
||||
|
||||
|
||||
# Let's load all necessary data for the program
|
||||
def initialize():
|
||||
package_table = load_packages(PACKAGES_CSV)
|
||||
address_list, distance_matrix = load_distances(DISTANCES_CSV)
|
||||
return package_table, address_list, distance_matrix
|
||||
|
||||
|
||||
|
||||
|
||||
# Let's assign packages to trucks based on delivery constraints
|
||||
# Manual truck loading (satisfies all constraints)
|
||||
def load_trucks(package_table, address_list, distance_matrix):
|
||||
"""Assign packages to trucks and run the delivery algorithm."""
|
||||
|
||||
# Let's set the start of the delivery day (8:00 AM)
|
||||
start_of_day = datetime(2024, 1, 1, 8, 0, 0) # 8:00 AM
|
||||
|
||||
# ---- Truck 1 ----
|
||||
# Let's load priority packages and grouped deliveries
|
||||
truck1_packages = [
|
||||
1, 13, 14, 15, 16, 19, 20, 29, 30, 31, 34, 37, 40,
|
||||
2, 4, 5, 7, 10,
|
||||
]
|
||||
|
||||
# ---- Truck 2 ----
|
||||
# Let's load truck-specific and delayed packages
|
||||
truck2_packages = [
|
||||
3, 18, 36, 38,
|
||||
6, 28, 32,
|
||||
9,
|
||||
8, 11, 12, 17, 21, 23, 24,
|
||||
]
|
||||
|
||||
# ---- Truck 3 ----
|
||||
# Let's assign any remaining packages to Truck 3
|
||||
all_assigned = set(truck1_packages + truck2_packages)
|
||||
all_packages = [pid for pid, _ in package_table.get_all()]
|
||||
truck3_packages = [pid for pid in all_packages if pid not in all_assigned]
|
||||
|
||||
# Let's initialize Truck 1 (departs at 8:00 AM)
|
||||
truck1 = Truck(
|
||||
truck_id=1,
|
||||
packages=truck1_packages,
|
||||
depart_time=start_of_day,
|
||||
package_table=package_table,
|
||||
address_list=address_list,
|
||||
distance_matrix=distance_matrix,
|
||||
)
|
||||
|
||||
# Let's delay Truck 2 departure until 9:05 AM for late packages
|
||||
truck2_depart = datetime(2024, 1, 1, 9, 5, 0)
|
||||
truck2 = Truck(
|
||||
truck_id=2,
|
||||
packages=truck2_packages,
|
||||
depart_time=truck2_depart,
|
||||
package_table=package_table,
|
||||
address_list=address_list,
|
||||
distance_matrix=distance_matrix,
|
||||
)
|
||||
|
||||
# Let's run deliveries for Truck 1 and Truck 2
|
||||
truck1.deliver_all(return_to_hub=True)
|
||||
truck2.deliver_all(return_to_hub=True)
|
||||
|
||||
# Let's assign Truck 3 to depart when Truck 1 returns
|
||||
truck3_depart = truck1.current_time
|
||||
truck3 = Truck(
|
||||
truck_id=3,
|
||||
packages=truck3_packages,
|
||||
depart_time=truck3_depart,
|
||||
package_table=package_table,
|
||||
address_list=address_list,
|
||||
distance_matrix=distance_matrix,
|
||||
)
|
||||
|
||||
# Let's complete deliveries for Truck 3
|
||||
truck3.deliver_all(return_to_hub=True)
|
||||
|
||||
return truck1, truck2, truck3
|
||||
|
||||
|
||||
# CLI INTERFACE
|
||||
# Let's display the status of all packages at a given time
|
||||
def print_all_status(package_table, query_time):
|
||||
"""Print the delivery status of all 40 packages at a given time."""
|
||||
print(f"\n{'='*75}")
|
||||
print(f" Package Status at {query_time.strftime('%I:%M %p')}")
|
||||
print(f"{'='*75}")
|
||||
print(f"{'ID':<4} {'Address':<38} {'Deadline':<10} {'Truck':<6} {'Status':<12} {'Delivered'}")
|
||||
print(f"{'-'*75}")
|
||||
|
||||
# Sort packages by ID so output is easy to read
|
||||
all_packages = sorted(package_table.get_all(), key=lambda x: x[0])
|
||||
for pid, pkg in all_packages:
|
||||
# status_at() compares query_time to departure and delivery times
|
||||
status = pkg.status_at(query_time)
|
||||
delivered_str = (
|
||||
pkg.delivery_time.strftime("%I:%M %p")
|
||||
if status == "Delivered" and pkg.delivery_time
|
||||
else "—"
|
||||
)
|
||||
truck_str = str(pkg.truck_id) if pkg.truck_id else "—"
|
||||
print(
|
||||
f"{pid:<4} {pkg.address:<38} {pkg.deadline:<10} "
|
||||
f"{truck_str:<6} {status:<12} {delivered_str}"
|
||||
)
|
||||
|
||||
|
||||
# Let's display the status of a single package
|
||||
def print_single_status(package_table, pkg_id, query_time):
|
||||
"""Print full details for a single package at a given time."""
|
||||
# Use the hash table lookup function to retrieve the package by ID
|
||||
pkg = package_table.lookup(pkg_id)
|
||||
if pkg is None:
|
||||
print(f"Package {pkg_id} not found.")
|
||||
return
|
||||
|
||||
status = pkg.status_at(query_time)
|
||||
delivered_str = (
|
||||
pkg.delivery_time.strftime("%I:%M %p")
|
||||
if status == "Delivered" and pkg.delivery_time
|
||||
else "Not yet delivered"
|
||||
)
|
||||
|
||||
# Display all data components associated with this package ID
|
||||
print(f"\n{'='*50}")
|
||||
print(f" Package {pkg.package_id} — Status at {query_time.strftime('%I:%M %p')}")
|
||||
print(f"{'='*50}")
|
||||
print(f" Address : {pkg.address}, {pkg.city}, {pkg.state} {pkg.zip_code}")
|
||||
print(f" Deadline : {pkg.deadline}")
|
||||
print(f" Weight : {pkg.weight} kg")
|
||||
print(f" Truck : {pkg.truck_id if pkg.truck_id else '—'}")
|
||||
print(f" Notes : {pkg.notes if pkg.notes else 'None'}")
|
||||
print(f" Status : {status}")
|
||||
print(f" Delivered: {delivered_str}")
|
||||
|
||||
|
||||
# Let's convert user input into a valid time format
|
||||
def parse_time(time_str):
|
||||
"""Parse a user-entered time string into a datetime object."""
|
||||
reference_date = datetime(2024, 1, 1)
|
||||
for fmt in ["%I:%M %p", "%H:%M", "%I:%M%p"]:
|
||||
try:
|
||||
t = datetime.strptime(time_str.strip(), fmt)
|
||||
return reference_date.replace(hour=t.hour, minute=t.minute, second=0)
|
||||
except ValueError:
|
||||
continue
|
||||
return None # Return None if no format matched
|
||||
|
||||
|
||||
# Let's create the main user interface loop
|
||||
def main(package_table, truck1, truck2, truck3):
|
||||
# Calculate and display total combined mileage at startup
|
||||
total_miles = truck1.total_miles + truck2.total_miles + truck3.total_miles
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(" WGUPS Package Routing Program")
|
||||
print("="*60)
|
||||
print(f" Truck 1 mileage : {truck1.total_miles:.2f} miles")
|
||||
print(f" Truck 2 mileage : {truck2.total_miles:.2f} miles")
|
||||
print(f" Truck 3 mileage : {truck3.total_miles:.2f} miles")
|
||||
print(f" Total mileage : {total_miles:.2f} miles")
|
||||
print("="*60)
|
||||
|
||||
# Menu loop — continues until the user enters 4 to exit
|
||||
while True:
|
||||
print("\nOptions:")
|
||||
print(" 1 - View status of ALL packages at a specific time")
|
||||
print(" 2 - View status of a SINGLE package at a specific time")
|
||||
print(" 3 - Print all truck routes")
|
||||
print(" 4 - Exit")
|
||||
choice = input("\nEnter choice (1/2/3/4): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
# Prompt for a time and display all 40 package statuses
|
||||
time_str = input("Enter time (e.g. 9:00 AM or 13:00): ")
|
||||
query_time = parse_time(time_str)
|
||||
if query_time is None:
|
||||
print("Invalid time format. Try '9:00 AM' or '09:00'.")
|
||||
else:
|
||||
print_all_status(package_table, query_time)
|
||||
|
||||
elif choice == "2":
|
||||
# Prompt for a package ID and time, then display that package
|
||||
try:
|
||||
pkg_id = int(input("Enter package ID (1-40): "))
|
||||
except ValueError:
|
||||
print("Invalid package ID.")
|
||||
continue
|
||||
time_str = input("Enter time (e.g. 9:00 AM or 13:00): ")
|
||||
query_time = parse_time(time_str)
|
||||
if query_time is None:
|
||||
print("Invalid time format.")
|
||||
else:
|
||||
print_single_status(package_table, pkg_id, query_time)
|
||||
|
||||
elif choice == "3":
|
||||
# Print the full ordered route for each truck with delivery times
|
||||
truck1.print_route()
|
||||
truck2.print_route()
|
||||
truck3.print_route()
|
||||
total = truck1.total_miles + truck2.total_miles + truck3.total_miles
|
||||
print(f"\nCombined total: {total:.2f} miles")
|
||||
|
||||
elif choice == "4":
|
||||
print("Thank you for using WGUPS Routing Program, Goodbye.")
|
||||
break
|
||||
|
||||
else:
|
||||
print("Invalid choice. Please enter 1, 2, 3, or 4.")
|
||||
|
||||
|
||||
# Let's start the program execution
|
||||
if __name__ == "__main__":
|
||||
package_table, address_list, distance_matrix = initialize()
|
||||
truck1, truck2, truck3 = load_trucks(package_table, address_list, distance_matrix)
|
||||
main(package_table, truck1, truck2, truck3)
|
||||
@ -0,0 +1,79 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
|
||||
class Package:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package_id,
|
||||
address,
|
||||
city,
|
||||
state,
|
||||
zip_code,
|
||||
deadline,
|
||||
weight,
|
||||
notes="",
|
||||
):
|
||||
# Let's store the core delivery data from the Package File
|
||||
self.package_id = int(package_id)
|
||||
self.address = address.strip()
|
||||
self.city = city.strip()
|
||||
self.state = state.strip()
|
||||
self.zip_code = str(zip_code).strip()
|
||||
self.deadline = deadline.strip()
|
||||
self.weight = weight
|
||||
self.notes = notes.strip()
|
||||
|
||||
# Let's initialize the status tracking fields for the simulation
|
||||
self.status = "At Hub"
|
||||
self.departure_time = None
|
||||
self.delivery_time = None
|
||||
self.truck_id = None
|
||||
|
||||
|
||||
# STATUS UPDATES
|
||||
|
||||
def mark_en_route(self, departure_time, truck_id):
|
||||
|
||||
# Called when a truck departs the hub carrying this package.
|
||||
self.status = "En Route"
|
||||
self.departure_time = departure_time
|
||||
self.truck_id = truck_id
|
||||
|
||||
def mark_delivered(self, delivery_time):
|
||||
|
||||
# Called when the truck arrives at this package's delivery address.
|
||||
|
||||
self.status = "Delivered"
|
||||
self.delivery_time = delivery_time
|
||||
|
||||
# TIME-BASED STATUS QUERY
|
||||
|
||||
def status_at(self, query_time):
|
||||
|
||||
# Return the status this package had at a specific point in time.
|
||||
|
||||
# Package is still at the hub if no truck has departed with it yet,
|
||||
# or if the query time is before the truck's departure.
|
||||
if self.departure_time is None or query_time < self.departure_time:
|
||||
return "At Hub"
|
||||
|
||||
# Package is en route if the truck has left but delivery hasn't happened yet
|
||||
if self.delivery_time is None or query_time < self.delivery_time:
|
||||
return "En Route"
|
||||
|
||||
# Otherwise the package was already delivered by query_time
|
||||
return "Delivered"
|
||||
|
||||
# DISPLAY
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"Package({self.package_id:>2}, "
|
||||
f"{self.address:<38} "
|
||||
f"Deadline: {self.deadline:<8} "
|
||||
f"Wt: {self.weight:<4} "
|
||||
f"Status: {self.status})"
|
||||
)
|
||||
@ -0,0 +1,41 @@
|
||||
PackageID,Address,City,State,Zip,Deadline,Weight,Notes
|
||||
1,195 W Oakland Ave,Salt Lake City,UT,84115,10:30:00,21,
|
||||
2,2530 S 500 E,Salt Lake City,UT,84106,EOD,44,
|
||||
3,233 Canyon Rd,Salt Lake City,UT,84103,EOD,2,Can only be on truck 2
|
||||
4,380 W 2880 S,Salt Lake City,UT,84115,EOD,4,
|
||||
5,410 S State St,Salt Lake City,UT,84111,EOD,5,
|
||||
6,3060 Lester St,West Valley City,UT,84119,10:30:00,88,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
7,1330 2100 S,Salt Lake City,UT,84106,EOD,8,
|
||||
8,300 State St,Salt Lake City,UT,84103,EOD,9,
|
||||
9,300 State St,Salt Lake City,UT,84103,EOD,2,Wrong address listed
|
||||
10,600 E 900 South,Salt Lake City,UT,84105,EOD,1,
|
||||
11,2600 Taylorsville Blvd,Salt Lake City,UT,84118,EOD,1,
|
||||
12,3575 W Valley Central Station bus Loop,West Valley City,UT,84119,EOD,1,
|
||||
13,2010 W 500 S,Salt Lake City,UT,84104,10:30:00,2,
|
||||
14,4300 S 1300 E,Millcreek,UT,84117,10:30:00,88,"Must be delivered with 15, 19"
|
||||
15,4580 S 2300 E,Holladay,UT,84117,09:00:00,4,
|
||||
16,4580 S 2300 E,Holladay,UT,84117,10:30:00,88,"Must be delivered with 13, 19"
|
||||
17,3148 S 1100 W,Salt Lake City,UT,84119,EOD,2,
|
||||
18,1488 4800 S,Salt Lake City,UT,84123,EOD,6,Can only be on truck 2
|
||||
19,177 W Price Ave,Salt Lake City,UT,84115,EOD,37,
|
||||
20,3595 Main St,Salt Lake City,UT,84115,10:30:00,37,"Must be delivered with 13, 15"
|
||||
21,3595 Main St,Salt Lake City,UT,84115,EOD,3,
|
||||
22,6351 South 900 East,Murray,UT,84121,EOD,2,
|
||||
23,5100 South 2700 West,Salt Lake City,UT,84118,EOD,5,
|
||||
24,5025 State St,Murray,UT,84107,EOD,7,
|
||||
25,5383 South 900 East #104,Salt Lake City,UT,84117,10:30:00,7,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
26,5383 South 900 East #104,Salt Lake City,UT,84117,EOD,25,
|
||||
27,1060 Dalton Ave S,Salt Lake City,UT,84104,EOD,5,
|
||||
28,2835 Main St,Salt Lake City,UT,84115,EOD,7,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
29,1330 2100 S,Salt Lake City,UT,84106,10:30:00,2,
|
||||
30,300 State St,Salt Lake City,UT,84103,10:30:00,1,
|
||||
31,3365 S 900 W,Salt Lake City,UT,84119,10:30:00,1,
|
||||
32,3365 S 900 W,Salt Lake City,UT,84119,EOD,1,Delayed on flight---will not arrive to depot until 9:05 am
|
||||
33,2530 S 500 E,Salt Lake City,UT,84106,EOD,1,
|
||||
34,4580 S 2300 E,Holladay,UT,84117,10:30:00,2,
|
||||
35,1060 Dalton Ave S,Salt Lake City,UT,84104,EOD,88,
|
||||
36,2300 Parkway Blvd,West Valley City,UT,84119,EOD,88,Can only be on truck 2
|
||||
37,410 S State St,Salt Lake City,UT,84111,10:30:00,2,
|
||||
38,410 S State St,Salt Lake City,UT,84111,EOD,9,Can only be on truck 2
|
||||
39,2010 W 500 S,Salt Lake City,UT,84104,EOD,9,
|
||||
40,380 W 2880 S,Salt Lake City,UT,84115,10:30:00,45,
|
||||
|
@ -0,0 +1,160 @@
|
||||
# Student ID: 012498637
|
||||
# Student Name: Zakaria Benmoulay
|
||||
# Course: C950 - Data Structures and Algorithms II
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
SPEED_MPH = 18 # all trucks travel at 18 mph
|
||||
HUB_ADDRESS = "Salt Lake City, UT 84107" # WGU hub — matches the distance table
|
||||
|
||||
|
||||
class Truck:
|
||||
# Let's initialize the truck with assigned packages and starting conditions.
|
||||
def __init__(self, truck_id, packages, depart_time, package_table,
|
||||
address_list, distance_matrix):
|
||||
|
||||
self.truck_id = truck_id
|
||||
self.package_ids = list(packages) # copy so original list is not modified
|
||||
self.depart_time = depart_time
|
||||
self.package_table = package_table
|
||||
self.address_list = address_list
|
||||
self.distance_matrix = distance_matrix
|
||||
|
||||
# Simulation state — updated as deliver_all() runs
|
||||
self.current_address = HUB_ADDRESS # truck starts at the hub
|
||||
self.current_time = depart_time # clock starts at departure time
|
||||
self.total_miles = 0.0 # accumulates mileage during delivery
|
||||
self.route = [] # list of (package_id, address, arrival_time)
|
||||
|
||||
|
||||
# Distance and time helpers
|
||||
def _distance(self, addr_a, addr_b):
|
||||
|
||||
def find_index(addr):
|
||||
addr_clean = addr.strip().lower()
|
||||
|
||||
# Pass 1: exact match
|
||||
for i, a in enumerate(self.address_list):
|
||||
if a.strip().lower() == addr_clean:
|
||||
return i
|
||||
|
||||
# Pass 2: substring match (handles abbreviations and extra words)
|
||||
for i, a in enumerate(self.address_list):
|
||||
if addr_clean in a.strip().lower() or a.strip().lower() in addr_clean:
|
||||
return i
|
||||
|
||||
# Pass 3: match on the first two tokens (street number + street name)
|
||||
tokens = addr_clean.split()
|
||||
if tokens:
|
||||
prefix = " ".join(tokens[:2])
|
||||
for i, a in enumerate(self.address_list):
|
||||
if a.strip().lower().startswith(prefix):
|
||||
return i
|
||||
|
||||
raise ValueError(f"Address not found in distance table: '{addr}'")
|
||||
|
||||
i = find_index(addr_a)
|
||||
j = find_index(addr_b)
|
||||
return self.distance_matrix[i][j]
|
||||
|
||||
def _travel_time(self, miles):
|
||||
|
||||
hours = miles / SPEED_MPH
|
||||
return timedelta(hours=hours)
|
||||
|
||||
|
||||
# Core nearest-neighbor delivery algorithm
|
||||
def deliver_all(self, return_to_hub=False):
|
||||
|
||||
# Step 1 — Mark all packages on this truck as En Route at departure time
|
||||
for pid in self.package_ids:
|
||||
pkg = self.package_table.lookup(pid)
|
||||
if pkg:
|
||||
pkg.mark_en_route(self.depart_time, self.truck_id)
|
||||
|
||||
# Work from a copy so the original package_ids list is preserved
|
||||
remaining = list(self.package_ids)
|
||||
|
||||
# Step 2 — Nearest-neighbor delivery loop
|
||||
while remaining:
|
||||
|
||||
# Inner scan: find the package with the shortest distance
|
||||
# from the truck's current location
|
||||
nearest_pid = None
|
||||
nearest_dist = float("inf") # start with infinity so any real distance wins
|
||||
|
||||
for pid in remaining:
|
||||
pkg = self.package_table.lookup(pid)
|
||||
if pkg is None:
|
||||
continue
|
||||
|
||||
# Get the correct address for this package at this time
|
||||
# (handles Package 9's address correction at 10:20 AM)
|
||||
address = self._resolve_address(pid, self.current_time)
|
||||
dist = self._distance(self.current_address, address)
|
||||
|
||||
# Update nearest if this package is closer than the current best
|
||||
if dist < nearest_dist:
|
||||
nearest_dist = dist
|
||||
nearest_pid = pid
|
||||
|
||||
if nearest_pid is None:
|
||||
break # safety check — should not happen with valid data
|
||||
|
||||
# Step 2b — Drive to the nearest package's address
|
||||
pkg = self.package_table.lookup(nearest_pid)
|
||||
dest_address = self._resolve_address(nearest_pid, self.current_time)
|
||||
travel = self._travel_time(nearest_dist)
|
||||
|
||||
self.current_time += travel # advance the simulated clock
|
||||
self.total_miles += nearest_dist # accumulate mileage
|
||||
self.current_address = dest_address # move truck to new location
|
||||
|
||||
# Step 2c — Mark the package as delivered at the current time
|
||||
pkg.mark_delivered(self.current_time)
|
||||
self.route.append((nearest_pid, dest_address, self.current_time))
|
||||
|
||||
# Step 2d — Remove from remaining list
|
||||
remaining.remove(nearest_pid)
|
||||
|
||||
# Step 3 — Return to hub if needed (for driver reassignment to Truck 3)
|
||||
if return_to_hub:
|
||||
dist_home = self._distance(self.current_address, HUB_ADDRESS)
|
||||
self.total_miles += dist_home
|
||||
self.current_time += self._travel_time(dist_home)
|
||||
self.current_address = HUB_ADDRESS
|
||||
|
||||
# Package 9 address correction
|
||||
def _resolve_address(self, package_id, current_time):
|
||||
|
||||
pkg = self.package_table.lookup(package_id)
|
||||
|
||||
if package_id == 9:
|
||||
# Define the correction time as 10:20 AM on the simulation date
|
||||
correction_time = self.depart_time.replace(
|
||||
hour=10, minute=20, second=0, microsecond=0
|
||||
)
|
||||
if current_time >= correction_time:
|
||||
return "410 S State St" # corrected address after 10:20 AM
|
||||
else:
|
||||
return pkg.address # wrong address before 10:20 AM
|
||||
|
||||
return pkg.address # all other packages return their address unchanged
|
||||
|
||||
|
||||
# Route summary display
|
||||
def print_route(self):
|
||||
|
||||
print(f"\n=== Truck {self.truck_id} Route ===")
|
||||
print(f"Departed hub : {self.depart_time.strftime('%I:%M %p')}")
|
||||
for pid, addr, arrival in self.route:
|
||||
pkg = self.package_table.lookup(pid)
|
||||
deadline = pkg.deadline if pkg else "?"
|
||||
print(
|
||||
f" Pkg {pid:>2} -> {addr:<40} "
|
||||
f"Arrived: {arrival.strftime('%I:%M %p')} "
|
||||
f"Deadline: {deadline}"
|
||||
)
|
||||
print(f"Total miles : {self.total_miles:.2f}")
|
||||
print(f"Finished at : {self.current_time.strftime('%I:%M %p')}")
|
||||