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