import user
import book
import csv
import datetime
from datetime import timedelta
import typing
class Loan:
def __init__(self, user, book, borrow_str, due_str, return_str):
self.user = user
self.book = book
self.borrow_date = parse_date(borrow_str)
self.due_date = parse_date(due_str)
self.return_date = parse_date(return_str)
def calculate_fine(self):
TODAY = datetime.datetime.strptime("15/09/2025", "%d/%m/%Y").date()
if self.book.book_type == "online":
return 0.0
if TODAY <= self.due_date:
return 0.0
grace = {"Student": 0, "Staff": 2, "Others": 0}[self.user.role]
effective_due = self.due_date + timedelta(days = grace)
overdue_days = max(0, (TODAY - effective_due).days)
rate = 0.50 if self.user.role in ['Student', 'Staff'] else 1.00
return overdue_days * rate
def parse_date(date_str: str) -> typing.Optional[datetime.date]:
if not date_str or date_str.strip() == "None":
return None
day, month, year = map(int, date_str.split('/'))
return datetime.date(year, month, day)
def load_users(file_path: str) -> dict:
users = {}
with open(file_path, newline='') as csvfile:
reader = csv.reader(csvfile)
next(reader)
for row in reader:
user_id, password, name, role, department = row
role = row[3].capitalize()
if user_id.startswith('s'):
u = user.Student(user_id, password, name, role, None)
elif user_id.startswith('e'):
u = user.Staff(user_id, password, name, role, department)
else:
role = "Others"
u = user.Other(user_id, password, name, role, None)
users[user_id] = u
return users
def load_books(file_path: str) -> dict:
books = {}
with open(file_path, newline='') as csvfile:
reader = csv.reader(csvfile)
next(reader)
for row in reader:
book_id, book_type, copies, title, author, year, keywords = row
b = book.Book(book_id, book_type, int(copies), title, author, int(year), keywords)
books[book_id] = b
b.loans = []
return books
def load_loans(file_path: str, users: dict, books: dict):
loans = []
with open(file_path, newline='') as csvfile:
reader = csv.reader(csvfile)
next(reader)
for row in reader:
user_id, book_id, borrow_date, due_date, return_date = row
if user_id in users and book_id in books:
loan = Loan(users[user_id], books[book_id], borrow_date, due_date, return_date)
users[user_id].add_loan(loan)
books[book_id].loans.append(loan)
loans.append(loan)
return loans
def borrow_return_console(user, books, users):
TODAY = datetime.datetime.strptime("15/09/2025", "%d/%m/%Y").date()
while True:
cmd = input("> ").strip()
if cmd == "quit":
return
part = cmd.split(' ', 1)
if len(part) < 2:
continue
action, query = part[0].lower(), part[1]
if action == "borrow":
matched_books = []
for b in books.values():
if query.lower() in b.title.lower() or query == b.book_id:
matched_books.append(b)
matched_books.sort(key=lambda x: x.book_id)
if not matched_books:
print(f"No books match '{query}'.")
continue
physical_match = [b for b in matched_books if b.book_type == "physical"]
online_match = [b for b in matched_books if b.book_type == "online"]
available_str = sum(b.available_copies() for b in physical_match)
print(f"Found {len(matched_books)} book(s).")
all_matches = physical_match + online_match
all_matches.sort(key=lambda x: x.book_id)
for b in all_matches:
copies = f"Available copies: {available_str}/{b.total_copies}" if b.book_type == "physical" else "Available copies: 0/0"
print(f"- {b.book_id} ({b.book_type}) '{b.title}' by {b.authors} ({b.year}). {copies}.")
valid_ids = [b.book_id for b in all_matches]
while True:
borrowid = input(f"Confirm the Book ID you'd like to borrow: ").strip()
if borrowid == "quit":
break
if borrowid not in valid_ids:
continue
book = books[borrowid]
unpaid_fine = sum(loan.calculate_fine() for loan in user.get_active_loans())
if unpaid_fine > 0:
print("Borrowing unavailable: unpaid fines. Review your loan details for more info.")
break
current_physical = sum(1 for loan in user.get_active_loans() if loan.book.book_type == "physical")
if current_physical >= user.get_loan_policy()["quota"]:
print("Borrowing unavailable: quota reached. Review your loan details for more info.")
break
if book.book_type == "physical" and book.available_copies() <= 0:
print("No copies available.")
break
due_days = user.get_loan_policy()["days"]
due_date = TODAY + timedelta(days = due_days)
borrow_str = TODAY.strftime("%d/%m/%Y")
due_str = due_date.strftime("%d/%m/%Y")
return_str = "None"
loan = Loan(user, book, borrow_str, due_str, return_str)
user.loans.append(loan)
book.loans.append(loan)
print(f"You have borrowed '{book.title}' by {book.authors} ({book.year}). Due: {due_date.strftime('%d/%m/%Y')}.")
break
elif action == "return":
borrowid = query.strip()
if borrowid not in books:
print("No loan record for {}.".format(borrowid))
continue
book = books[borrowid]
active_loans = [loan for loan in user.get_active_loans() if loan.book.book_id == borrowid]
if not active_loans:
print(f"No loan record for {borrowid}.")
continue
loan_to_return = min(active_loans, key=lambda x: x.due_date)
user.loans.remove(loan_to_return)
return_date = TODAY
fine_amount = 0.0
overdue_days = 0
if book.book_type == "physical" and return_date > loan_to_return.due_date:
grace = {"Student": 0, "Staff": 2, "Others": 0}[user.role]
effective_due = loan_to_return.due_date + timedelta(days = grace)
overdue_days = max(0, (TODAY - effective_due).days)
rate = 0.50 if user.role in ['Student', 'Staff'] else 1.00
fine_amount = overdue_days * rate
status = f"Returned '{book.title}' by {book.authors} ({book.year})."
if overdue_days > 0:
status += f" Overdue by {overdue_days} day(s). Fine: $ {fine_amount:.2f}"
print(status)
elif action == "renew":
borrowid = query.strip()
due_days = user.get_loan_policy()["days"]
due_date = TODAY + timedelta(days = due_days)
for loan in user.get_active_loans():
if int(loan.calculate_fine()) > 0:
print("Renewal denied: You have unpaid fines")
continue
if TODAY > due_date:
print("Renewal denied: This book is already overdue")
book = books[borrowid]
active_loans = [loan for loan in user.get_active_loans() if loan.book.book_id == borrowid]
loan_to_renew = min(active_loans, key=lambda x: x.due_date)
if loan_to_renew.renewed:
print("Renewal unavailable: Each book can only be renewed once.")
continue
loan_to_renew.due_date = loan_to_renew.due_date + timedelta(days=5)
loan_to_renew.renewed = True
print(f"Renew '{book.title}' by {book.authors} (book.year) successfully. New due date: {loan_to_renew.due_date.strftime('%d/%m/%Y')}")
else:
continue
def search_by_keywords(books):
keywords = input("Enter search keywords (separated by comma): ").strip().lower().split(',')
if not keywords or keywords[0] == '':
print("Found 0 book(s).")
return
result = []
for b in book.values():
matched = sum(1 for keyword in keywords if keywords in book.title.lower() or keywords in ' '.join(book.keywords).lower())
if matched > 0:
result.append(book, matched)
result.sort(key=lambda x: (-x[1], -x[0].year, x[0].book_id))
print(f"Found {len(result)} book(s).")
for i, (book, _) in enumerate(result):
print(f"{i+1}. {book.book_id} '{book.title}' by {book.authors} ({book.year}).")
def manage_library(books, existing_keywords):
while True:
cmd = input("> ").strip()
if cmd == "quit":
break
elif cmd == "report":
total_user = len(users)
students = sum(1 for u in users.values() if u.user_id.startswith('s'))
staffs = sum(1 for u in users.values() if u.user_id.startswith('e'))
others = sum(1 for u in users.values() if u.user_id.startswith('o'))
total_books = len(books)
physical_books = sum(1 for b in books.values() if b.book_type == "physical")
available_physical = sum(1 for b in books.values() if b.book_type == "physical" and b.is_available())
online_books = sum(1 for b in books.values() if b.book_type == "online")
print("Library report")
print(f"- {total_user} users, including {students} student(s), {staffs} staff, and {others} others.")
print(f"- {total_books} books, including {physical_books} physical book(s) ({available_physical} currently available) and {online_books} online book(s).")
elif cmd.startswith("add "):
book_type = cmd.split(" ")[1]
if book_type not in ["physical", "online"]:
print("Invalid cmd.")
continue
title = input("Title: ")
authors = input("Authors: ")
year = input("Year: ")
if book_type == "physical":
copies = input("Copies: ")
else:
copies = 0
all_keywords = set()
for b in books.value():
all_keywords.update(books.keywords)
detected_keywords = [word for word in title.lower().split() if word in all_keywords]
detected_keywords.sort()
keywords_str = ":".join(detected_keywords)
if book_type == "physical":
physical_ids = [int(book.book_id[1:]) for book in books.values() if book.book_id.startswith('P')]
new_id = f"P{str(max(physical_ids) + 1)}"
else:
online_ids = [int(book.book_id[1:]) for book in books.values() if book.book_id.startswith('E')]
new_id = f"E{str(max(online_ids) + 1)}"
new_book = book(new_id, book_type, copies, title, authors, year, keywords_str)
book[new_id] = new_book
print(f"Detected keywords: {keywords_str}")
print(f"Adding {new_id} '{title}' by {authors} ({year}).")
def main(user_file: str, book_file:str, loan_file:str) -> None:
"""
This is the entry of your program. Please DO NOT modify this function signature, i.e. function name, parameters
Parameteres:
- user_file (str): path the `users.csv` which stores user information
- book_file (str): path the `books.csv` which stores book information
- loan_file (str): path the `loans.csv` which stores loan information
"""
# Your implemetation goes here
users = load_users(user_file)
books = load_books(book_file)
load_loans(loan_file, users, books)
while True:
print("Welcome to Library")
user_input = input("Login as: ").strip()
if user_input == "quit":
print("Goodbye!")
return
else:
password = input("Password: ").strip()
if user_input not in users or users[user_input].password != password:
print("Invalid credentials. 2 attempt(s) remaining.")
user_input = input("Login as: ").strip()
password = input("Password: ").strip()
if user_input not in users or users[user_input].password != password:
print("Invalid credentials. 1 attempt(s) remaining.")
user_input = input("Login as: ").strip()
password = input("Password: ").strip()
if user_input not in users or users[user_input].password != password:
print("Sorry you're out of attempts. Please contact your librarian for assistance.")
continue
current_user = users[user_input]
print(f"Logged in as {current_user.name} ({current_user.role})")
while True:
menu = [
"My Library Account",
"0. Quit",
"1. Log out",
"2. View account policies",
"3. View my loans",
"4. Borrow and Return",
"5. Search by Keywords"
]
if current_user.is_library_staff():
menu.append("6. Manage Library")
print("=" * 34)
for item in menu:
print(item)
print("=" * 34)
while True:
try:
choice = input("Enter your choice: ").strip()
if not choice.isdigit() or choice not in ['0', '1', '2', '3', '4', '5', '6']:
continue
choice = int(choice)
break
except:
continue
if choice == 0:
print("Goodbye!")
return
elif choice == 1:
break
elif choice == 2:
policy = current_user.get_loan_policy()
active_loans = current_user.get_active_loans()
num_loans = len(active_loans)
phys, online = current_user.get_physical_online_count()
total_fine = sum(loan.calculate_fine() for loan in active_loans)
print(f"{current_user.role} {current_user.name}. Policies: maximum of {policy['days']} days, {policy['quota']} items. Current loans: {num_loans} ({phys} physical / {online} online). Fines: $ {total_fine:.2f}")
elif choice == 3:
active_loans = sorted(current_user.get_active_loans(), key=lambda x: x.due_date)
count = len(active_loans)
print(f"You are currently have {count} loan(s).")
for i, loan in enumerate(active_loans, 1):
print(f"{i}. {loan.book.book_id} '{loan.book.title}' by {loan.book.authors} ({loan.book.year}). Due date: {loan.due_date.strftime('%d/%m/%Y')}.")
elif choice == 4:
borrow_return_console(current_user, books, users)
elif choice == 5:
total_user = len(users)
students = sum(1 for u in users.values() if u.user_id.startswith('s'))
staffs = sum(1 for u in users.values() if u.user_id.startswith('e'))
others = sum(1 for u in users.values() if u.user_id.startswith('o'))
total_books = len(books)
physical_books = sum(1 for b in books.values() if b.book_type == "physical")
available_physical = sum(1 for b in books.values() if b.book_type == "physical" and b.is_available())
online_books = sum(1 for b in books.values() if b.book_type == "online")
print("Library report")
print(f"- {total_user} users, including {students} student(s), {staffs} staff, and {others} others.")
print(f"- {total_books} books, including {physical_books} physical book(s) ({available_physical} currently available) and {online_books} online book(s).")
else:
continue
if __name__ == "__main__":
main('data/users.csv', 'data/books.csv', 'data/loans.csv')
最新发布