import json
import csv
import os
import re
from datetime import datetime
from pathlib import Path
# ============================================================
# CONFIGURATION
# ============================================================
DATA_FILE = "contacts.json"
BACKUP_FILE = "contacts_backup.json"
# ============================================================
# LOAD & SAVE
# ============================================================
def load_contacts():
if Path(DATA_FILE).exists():
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except:
pass
return {"contacts": [], "groups": []}
def save_contacts(data):
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def backup_contacts(data):
with open(BACKUP_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f" Backup saved: {BACKUP_FILE}")
# ============================================================
# VALIDATION
# ============================================================
def validate_phone(phone):
"""Accept digits, spaces, +, -, (, )"""
return bool(re.match(r"^[\d\s\+\-\(\)]{7,20}$", phone.strip()))
def validate_email(email):
return bool(re.match(r"^[\w\.\+\-]+@[\w\-]+\.[a-zA-Z]{2,}$", email.strip()))
# ============================================================
# ADD CONTACT
# ============================================================
def add_contact(data):
print("\n" + "="*50)
print(" ADD NEW CONTACT")
print("="*50)
# Required
name = input(" Full Name* : ").strip()
if not name:
print(" Name is required.")
return
# Check duplicate
if any(c["name"].lower() == name.lower() for c in data["contacts"]):
overwrite = input(f" '{name}' already exists. Add anyway? (y/n): ").strip().lower()
if overwrite != "y":
return
# Optional fields
phone = input(" Phone Number : ").strip()
if phone and not validate_phone(phone):
print(" Invalid phone format. Saving as-is.")
email = input(" Email : ").strip()
if email and not validate_email(email):
print(" Invalid email format. Saving as-is.")
address = input(" Address : ").strip()
birthday = input(" Birthday (DD-MM-YYYY): ").strip()
company = input(" Company : ").strip()
notes = input(" Notes : ").strip()
# Groups
if data["groups"]:
print(f"\n Available groups: {', '.join(data['groups'])}")
group = input(" Group (or new group name): ").strip()
if group and group not in data["groups"]:
data["groups"].append(group)
contact = {
"id": len(data["contacts"]) + 1,
"name": name,
"phone": phone,
"email": email,
"address": address,
"birthday": birthday,
"company": company,
"notes": notes,
"group": group,
"favourite": False,
"created_at": datetime.now().strftime("%d-%m-%Y %H:%M"),
"updated_at": datetime.now().strftime("%d-%m-%Y %H:%M"),
}
data["contacts"].append(contact)
save_contacts(data)
print(f"\n Contact saved: {name}")
# ============================================================
# VIEW CONTACT
# ============================================================
def view_contact(contact):
w = 50
fav = " ★" if contact.get("favourite") else ""
print("\n" + "="*w)
print(f" {contact['name']}{fav}")
print("="*w)
fields = [
("Phone", contact.get("phone", "")),
("Email", contact.get("email", "")),
("Company", contact.get("company", "")),
("Address", contact.get("address", "")),
("Birthday", contact.get("birthday", "")),
("Group", contact.get("group", "")),
("Notes", contact.get("notes", "")),
("Added", contact.get("created_at", "")),
("Updated", contact.get("updated_at", "")),
]
for label, value in fields:
if value:
print(f" {label:<10}: {value}")
print("="*w)
# ============================================================
# LIST CONTACTS
# ============================================================
def list_contacts(data, group_filter=None, favourites_only=False,
sort_by="name"):
contacts = data["contacts"]
if group_filter:
contacts = [c for c in contacts
if c.get("group", "").lower() == group_filter.lower()]
if favourites_only:
contacts = [c for c in contacts if c.get("favourite")]
if not contacts:
print("\n No contacts found.")
return []
# Sort
if sort_by == "name":
contacts = sorted(contacts, key=lambda c: c["name"].lower())
elif sort_by == "company":
contacts = sorted(contacts, key=lambda c: c.get("company", "").lower())
elif sort_by == "group":
contacts = sorted(contacts, key=lambda c: c.get("group", "").lower())
elif sort_by == "recent":
contacts = sorted(contacts, key=lambda c: c.get("updated_at", ""),
reverse=True)
label = ""
if group_filter:
label = f" Group: {group_filter}"
elif favourites_only:
label = " Favourites"
print("\n" + "="*65)
print(f" CONTACTS ({len(contacts)} total){label}")
print("="*65)
print(f" {'#':<5} {'NAME':<22} {'PHONE':<16} "
f"{'EMAIL':<22} {'GRP'}")
print(" " + "-"*60)
for i, c in enumerate(contacts, 1):
fav = "★" if c.get("favourite") else " "
name = c["name"][:20]
phone = c.get("phone", "")[:14]
email = c.get("email", "")[:20]
group = c.get("group", "")[:8]
print(f" {fav}{i:<4} {name:<22} {phone:<16} "
f"{email:<22} {group}")
print("="*65)
return contacts
# ============================================================
# SEARCH CONTACTS
# ============================================================
def search_contacts(data, query):
query = query.lower().strip()
results = []
for c in data["contacts"]:
if (query in c["name"].lower()
or query in c.get("phone", "").lower()
or query in c.get("email", "").lower()
or query in c.get("company", "").lower()
or query in c.get("address", "").lower()
or query in c.get("notes", "").lower()):
results.append(c)
if not results:
print(f"\n No contacts found for: '{query}'")
return []
print(f"\n Found {len(results)} result(s) for '{query}':")
for i, c in enumerate(results, 1):
fav = "★" if c.get("favourite") else " "
phone = c.get("phone", "N/A")
email = c.get("email", "")
print(f" {fav}[{i}] {c['name']} | {phone} | {email}")
return results
# ============================================================
# EDIT CONTACT
# ============================================================
def edit_contact(data, contact):
print(f"\n Editing: {contact['name']}")
print(" (Press Enter to keep current value)\n")
fields = [
("name", "Full Name", contact.get("name", "")),
("phone", "Phone", contact.get("phone", "")),
("email", "Email", contact.get("email", "")),
("company", "Company", contact.get("company", "")),
("address", "Address", contact.get("address", "")),
("birthday", "Birthday", contact.get("birthday", "")),
("notes", "Notes", contact.get("notes", "")),
("group", "Group", contact.get("group", "")),
]
for key, label, current in fields:
new_val = input(f" {label:<12} [{current}]: ").strip()
if new_val:
contact[key] = new_val
# Favourite toggle
fav = input(f" Favourite? (y/n) [{'+' if contact.get('favourite') else '-'}]: ").strip().lower()
if fav == "y":
contact["favourite"] = True
elif fav == "n":
contact["favourite"] = False
contact["updated_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")
# Update in data
for i, c in enumerate(data["contacts"]):
if c["id"] == contact["id"]:
data["contacts"][i] = contact
break
# Add new group if needed
if contact["group"] and contact["group"] not in data["groups"]:
data["groups"].append(contact["group"])
save_contacts(data)
print(f"\n Contact updated: {contact['name']}")
# ============================================================
# DELETE CONTACT
# ============================================================
def delete_contact(data, contact):
confirm = input(f"\n Delete '{contact['name']}'? (yes/no): ").strip().lower()
if confirm == "yes":
data["contacts"] = [c for c in data["contacts"]
if c["id"] != contact["id"]]
save_contacts(data)
print(f" Deleted: {contact['name']}")
else:
print(" Cancelled.")
# ============================================================
# BIRTHDAY REMINDERS
# ============================================================
def birthday_reminders(data):
today = datetime.today()
upcoming = []
for c in data["contacts"]:
bday = c.get("birthday", "")
if not bday:
continue
try:
bday_dt = datetime.strptime(bday, "%d-%m-%Y")
# This year's birthday
this_year = bday_dt.replace(year=today.year)
if this_year < today:
this_year = bday_dt.replace(year=today.year + 1)
days_left = (this_year - today).days
age = today.year - bday_dt.year
if this_year.year > today.year:
age -= 1
upcoming.append((days_left, c["name"], bday, age + 1))
except:
continue
upcoming.sort(key=lambda x: x[0])
print("\n" + "="*52)
print(" UPCOMING BIRTHDAYS")
print("="*52)
if not upcoming:
print(" No birthdays found. Add birthdays when creating contacts.")
return
for days, name, bday, age in upcoming[:15]:
if days == 0:
when = "TODAY!"
elif days == 1:
when = "Tomorrow"
elif days <= 7:
when = f"In {days} days"
elif days <= 30:
when = f"In {days} days"
else:
when = f"In {days} days"
print(f" {when:<15} {name:<25} {bday} (turns {age})")
print("="*52)
# ============================================================
# EXPORT TO CSV
# ============================================================
def export_csv(data, filename=None):
contacts = data["contacts"]
if not contacts:
print(" No contacts to export.")
return
if not filename:
filename = f"contacts_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
fields = ["name", "phone", "email", "company",
"address", "birthday", "group", "notes"]
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
for c in contacts:
writer.writerow({k: c.get(k, "") for k in fields})
print(f"\n Exported {len(contacts)} contact(s) to: {filename}")
# ============================================================
# IMPORT FROM CSV
# ============================================================
def import_csv(data, filename):
if not Path(filename).exists():
print(f" File not found: {filename}")
return
imported = 0
skipped = 0
with open(filename, "r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
name = row.get("name", "").strip()
if not name:
skipped += 1
continue
# Skip if already exists
if any(c["name"].lower() == name.lower()
for c in data["contacts"]):
skipped += 1
continue
contact = {
"id": len(data["contacts"]) + 1,
"name": name,
"phone": row.get("phone", ""),
"email": row.get("email", ""),
"address": row.get("address", ""),
"birthday": row.get("birthday", ""),
"company": row.get("company", ""),
"notes": row.get("notes", ""),
"group": row.get("group", ""),
"favourite": False,
"created_at": datetime.now().strftime("%d-%m-%Y %H:%M"),
"updated_at": datetime.now().strftime("%d-%m-%Y %H:%M"),
}
data["contacts"].append(contact)
# Add group
if contact["group"] and contact["group"] not in data["groups"]:
data["groups"].append(contact["group"])
imported += 1
save_contacts(data)
print(f"\n Imported: {imported} | Skipped (duplicate/empty): {skipped}")
# ============================================================
# GROUPS
# ============================================================
def manage_groups(data):
print("\n" + "="*45)
print(" GROUPS")
print("="*45)
if not data["groups"]:
print(" No groups yet.")
else:
for i, g in enumerate(data["groups"], 1):
count = sum(1 for c in data["contacts"]
if c.get("group") == g)
print(f" [{i}] {g} ({count} contacts)")
print("\n 1. View contacts in a group")
print(" 2. Rename a group")
print(" 3. Delete a group")
print(" 4. Back")
sub = input("\n > ").strip()
if sub == "1":
group = input(" Group name: ").strip()
list_contacts(data, group_filter=group)
elif sub == "2":
old = input(" Current group name: ").strip()
new = input(" New group name : ").strip()
if old and new:
for c in data["contacts"]:
if c.get("group") == old:
c["group"] = new
if old in data["groups"]:
data["groups"].remove(old)
if new not in data["groups"]:
data["groups"].append(new)
save_contacts(data)
print(f" Renamed '{old}' to '{new}'")
elif sub == "3":
group = input(" Group to delete: ").strip()
confirm = input(f" Delete group '{group}'? Contacts will be ungrouped. (yes/no): ").strip().lower()
if confirm == "yes":
for c in data["contacts"]:
if c.get("group") == group:
c["group"] = ""
if group in data["groups"]:
data["groups"].remove(group)
save_contacts(data)
print(f" Group deleted: {group}")
# ============================================================
# STATS
# ============================================================
def show_stats(data):
contacts = data["contacts"]
total = len(contacts)
if not total:
print("\n No contacts yet.")
return
with_phone = sum(1 for c in contacts if c.get("phone"))
with_email = sum(1 for c in contacts if c.get("email"))
with_bday = sum(1 for c in contacts if c.get("birthday"))
favourites = sum(1 for c in contacts if c.get("favourite"))
group_counts = {}
for c in contacts:
g = c.get("group") or "Ungrouped"
group_counts[g] = group_counts.get(g, 0) + 1
print("\n" + "="*48)
print(" CONTACT BOOK STATISTICS")
print("="*48)
print(f" Total contacts : {total}")
print(f" With phone : {with_phone}")
print(f" With email : {with_email}")
print(f" With birthday : {with_bday}")
print(f" Favourites : {favourites}")
print(f"\n By group:")
for g, count in sorted(group_counts.items(), key=lambda x: -x[1]):
bar = "█" * min(count, 20)
print(f" {g:<20} {count:>4} {bar}")
print("="*48)
# ============================================================
# SELECT CONTACT HELPER
# ============================================================
def select_contact(data, prompt=" Search contact: "):
query = input(prompt).strip()
if not query:
return None
results = search_contacts(data, query)
if not results:
return None
if len(results) == 1:
return results[0]
try:
idx = int(input(" Select number: ").strip()) - 1
if 0 <= idx < len(results):
return results[idx]
except ValueError:
pass
return None
# ============================================================
# MAIN MENU
# ============================================================
def print_menu(data):
total = len(data["contacts"])
favs = sum(1 for c in data["contacts"] if c.get("favourite"))
print("\n" + "-"*50)
print(f" CONTACT BOOK [{total} contacts ★{favs} favourites]")
print("-"*50)
print(" 1. Add new contact")
print(" 2. View all contacts")
print(" 3. Search contact")
print(" 4. View contact details")
print(" 5. Edit contact")
print(" 6. Delete contact")
print(" 7. View favourites")
print(" 8. Birthday reminders")
print(" 9. Manage groups")
print(" 10. Export to CSV")
print(" 11. Import from CSV")
print(" 12. Statistics")
print(" 13. Backup contacts")
print(" 0. Exit")
print("-"*50)
def main():
print("\n" + "="*55)
print(" CONTACT BOOK MANAGER")
print("="*55)
data = load_contacts()
if data["contacts"]:
print(f"\n Loaded {len(data['contacts'])} contact(s) from {DATA_FILE}")
else:
print("\n No contacts yet. Start by adding one!")
while True:
print_menu(data)
choice = input(" > ").strip()
if choice == "1":
add_contact(data)
elif choice == "2":
print("\n Sort by: 1=Name 2=Company 3=Group 4=Recent")
sort_opt = input(" Sort (default Name): ").strip()
sort_map = {"1": "name", "2": "company",
"3": "group", "4": "recent"}
sort_by = sort_map.get(sort_opt, "name")
list_contacts(data, sort_by=sort_by)
elif choice == "3":
query = input("\n Search: ").strip()
search_contacts(data, query)
elif choice == "4":
contact = select_contact(data)
if contact:
view_contact(contact)
elif choice == "5":
contact = select_contact(data)
if contact:
edit_contact(data, contact)
elif choice == "6":
contact = select_contact(data)
if contact:
view_contact(contact)
delete_contact(data, contact)
elif choice == "7":
list_contacts(data, favourites_only=True)
elif choice == "8":
birthday_reminders(data)
elif choice == "9":
manage_groups(data)
elif choice == "10":
fname = input(" Output filename (Enter=auto): ").strip() or None
export_csv(data, fname)
elif choice == "11":
fname = input(" CSV file path: ").strip()
import_csv(data, fname)
elif choice == "12":
show_stats(data)
elif choice == "13":
backup_contacts(data)
elif choice == "0":
print("\n Goodbye!\n")
break
else:
print(" Invalid choice.")
# ============================================================
# RUN
# ============================================================
if __name__ == "__main__":
main()