Loan EMI Calculator

import math

import json

import csv

import os

from datetime import datetime, date

from pathlib import Path

from dateutil.relativedelta import relativedelta


# Try importing dateutil

try:

    from dateutil.relativedelta import relativedelta

    DATEUTIL_OK = True

except ImportError:

    DATEUTIL_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


HISTORY_FILE  = "emi_history.json"

CURRENCY_SYM  = "₹"


# ============================================================

# FORMAT HELPERS

# ============================================================


def fmt(amount):

    return f"{CURRENCY_SYM}{amount:,.2f}"



def fmt_plain(amount):

    return f"{amount:,.2f}"



def draw_bar(value, max_val, width=30, fill="█", empty="░"):

    if max_val == 0:

        return ""

    filled = int((min(value, max_val) / max_val) * width)

    return fill * filled + empty * (width - filled)



# ============================================================

# CORE EMI CALCULATION

# ============================================================


def calculate_emi(principal, annual_rate, tenure_months):

    """

    EMI = P × r × (1+r)^n / ((1+r)^n - 1)

    Where:

        P = Principal

        r = Monthly interest rate (annual_rate / 12 / 100)

        n = Tenure in months

    """

    if annual_rate == 0:

        return round(principal / tenure_months, 2)


    r   = annual_rate / 12 / 100

    n   = tenure_months

    emi = principal * r * math.pow(1 + r, n) / (math.pow(1 + r, n) - 1)

    return round(emi, 2)



def calculate_summary(principal, annual_rate, tenure_months):

    """Return full summary of loan calculations."""

    emi           = calculate_emi(principal, annual_rate, tenure_months)

    total_payment = round(emi * tenure_months, 2)

    total_interest = round(total_payment - principal, 2)

    interest_pct  = round((total_interest / principal) * 100, 2)


    return {

        "principal":       principal,

        "annual_rate":     annual_rate,

        "tenure_months":   tenure_months,

        "tenure_years":    round(tenure_months / 12, 2),

        "emi":             emi,

        "total_payment":   total_payment,

        "total_interest":  total_interest,

        "interest_pct":    interest_pct,

    }



# ============================================================

# AMORTIZATION TABLE

# ============================================================


def generate_amortization(principal, annual_rate, tenure_months,

                          start_date=None):

    """

    Generate month-by-month amortization schedule.

    Returns list of dicts with payment breakdown per month.

    """

    emi       = calculate_emi(principal, annual_rate, tenure_months)

    r         = annual_rate / 12 / 100

    balance   = principal

    schedule  = []


    if start_date is None:

        start_date = date.today()


    for month in range(1, tenure_months + 1):

        interest_component  = round(balance * r, 2)

        principal_component = round(emi - interest_component, 2)


        # Last payment adjustment for rounding

        if month == tenure_months:

            principal_component = round(balance, 2)

            emi_actual          = round(principal_component + interest_component, 2)

        else:

            emi_actual = emi


        balance = round(balance - principal_component, 2)

        if balance < 0:

            balance = 0.0


        # Calculate payment date

        if DATEUTIL_OK:

            pay_date = (start_date + relativedelta(months=month)).strftime("%b %Y")

        else:

            m = (start_date.month + month - 1) % 12 + 1

            y = start_date.year + (start_date.month + month - 1) // 12

            pay_date = f"{date(y, m, 1).strftime('%b %Y')}"


        schedule.append({

            "month":     month,

            "pay_date":  pay_date,

            "emi":       emi_actual,

            "principal": principal_component,

            "interest":  interest_component,

            "balance":   balance,

        })


    return schedule



# ============================================================

# DISPLAY SUMMARY

# ============================================================


def display_summary(summary, label="LOAN SUMMARY"):

    p   = summary["principal"]

    ti  = summary["total_interest"]

    tp  = summary["total_payment"]


    # Pie-style ASCII breakdown

    p_bar  = draw_bar(p,  tp, width=25)

    ti_bar = draw_bar(ti, tp, width=25)


    print("\n" + "="*58)

    print(f"  {label}")

    print("="*58)

    print(f"  Principal Amount   : {fmt(summary['principal'])}")

    print(f"  Annual Interest    : {summary['annual_rate']}%")

    print(f"  Tenure             : {summary['tenure_months']} months "

          f"({summary['tenure_years']} years)")

    print("-"*58)

    print(f"  Monthly EMI        : {fmt(summary['emi'])}")

    print(f"  Total Payment      : {fmt(summary['total_payment'])}")

    print(f"  Total Interest     : {fmt(summary['total_interest'])}  "

          f"({summary['interest_pct']}% of principal)")

    print("-"*58)

    print(f"  Principal  {fmt_plain(p):>14}  {draw_bar(p,  tp, 20)}")

    print(f"  Interest   {fmt_plain(ti):>14}  {draw_bar(ti, tp, 20)}")

    print(f"  Total      {fmt_plain(tp):>14}")

    print("="*58)



# ============================================================

# DISPLAY AMORTIZATION TABLE

# ============================================================


def display_amortization(schedule, show_all=False):

    total_months = len(schedule)


    print("\n" + "="*72)

    print(f"  AMORTIZATION SCHEDULE  ({total_months} months)")

    print("="*72)

    print(f"  {'Mo':<4} {'Date':<10} {'EMI':>12} {'Principal':>12} "

          f"{'Interest':>12} {'Balance':>12}")

    print("  " + "-"*68)


    # Decide which months to show

    if show_all or total_months <= 24:

        rows = schedule

    else:

        # Show first 6, middle hint, last 6

        rows = schedule[:6]

        mid  = total_months // 2

        rows_display = schedule[:6]

        print_middle_hint = True

    

    if show_all or total_months <= 24:

        for row in schedule:

            print(f"  {row['month']:<4} {row['pay_date']:<10} "

                  f"{fmt_plain(row['emi']):>12} "

                  f"{fmt_plain(row['principal']):>12} "

                  f"{fmt_plain(row['interest']):>12} "

                  f"{fmt_plain(row['balance']):>12}")

    else:

        # First 6 months

        for row in schedule[:6]:

            print(f"  {row['month']:<4} {row['pay_date']:<10} "

                  f"{fmt_plain(row['emi']):>12} "

                  f"{fmt_plain(row['principal']):>12} "

                  f"{fmt_plain(row['interest']):>12} "

                  f"{fmt_plain(row['balance']):>12}")


        print(f"\n  ... {total_months - 12} more months ...\n")


        # Last 6 months

        for row in schedule[-6:]:

            print(f"  {row['month']:<4} {row['pay_date']:<10} "

                  f"{fmt_plain(row['emi']):>12} "

                  f"{fmt_plain(row['principal']):>12} "

                  f"{fmt_plain(row['interest']):>12} "

                  f"{fmt_plain(row['balance']):>12}")


    # Totals

    total_emi  = sum(r["emi"]       for r in schedule)

    total_prin = sum(r["principal"] for r in schedule)

    total_int  = sum(r["interest"]  for r in schedule)


    print("  " + "-"*68)

    print(f"  {'TOTAL':<15} {fmt_plain(total_emi):>12} "

          f"{fmt_plain(total_prin):>12} {fmt_plain(total_int):>12}")

    print("="*72)



# ============================================================

# YEARLY SUMMARY

# ============================================================


def display_yearly_summary(schedule):

    yearly = {}

    for row in schedule:

        year = row["pay_date"].split()[-1]

        if year not in yearly:

            yearly[year] = {"principal": 0, "interest": 0, "emi": 0}

        yearly[year]["principal"] += row["principal"]

        yearly[year]["interest"]  += row["interest"]

        yearly[year]["emi"]       += row["emi"]


    print("\n" + "="*58)

    print("  YEARLY SUMMARY")

    print("="*58)

    print(f"  {'YEAR':<8} {'EMI PAID':>12} {'PRINCIPAL':>12} {'INTEREST':>12}")

    print("  " + "-"*50)


    for year, vals in sorted(yearly.items()):

        print(f"  {year:<8} {fmt_plain(vals['emi']):>12} "

              f"{fmt_plain(vals['principal']):>12} "

              f"{fmt_plain(vals['interest']):>12}")


    print("="*58)



# ============================================================

# EMI COMPARISON (Multiple Rates / Tenures)

# ============================================================


def compare_options(principal):

    print(f"\n  EMI COMPARISON for {fmt(principal)}")

    print("  " + "="*70)


    rates   = [6, 7, 8, 9, 10, 11, 12, 14]

    tenures = [12, 24, 36, 60, 84, 120, 180, 240]


    # Header

    print(f"  {'RATE':<6}", end="")

    for t in tenures:

        y = t // 12

        print(f"  {str(y)+'yr':>9}", end="")

    print()

    print("  " + "-"*70)


    for rate in rates:

        print(f"  {rate}%   ", end="")

        for t in tenures:

            emi = calculate_emi(principal, rate, t)

            print(f"  {fmt_plain(emi):>9}", end="")

        print()


    print("  " + "="*70)

    print("  (Values show monthly EMI in same currency)")



# ============================================================

# PREPAYMENT IMPACT

# ============================================================


def prepayment_impact(principal, annual_rate, tenure_months, prepay_amount, prepay_month):

    """

    Calculate how a lump-sum prepayment affects

    total interest and remaining tenure.

    """

    # Without prepayment

    orig = calculate_summary(principal, annual_rate, tenure_months)


    # With prepayment — recalculate from prepay month

    schedule_before = generate_amortization(principal, annual_rate, tenure_months)


    if prepay_month >= tenure_months:

        print("  Prepayment month exceeds tenure.")

        return


    balance_at_prepay = schedule_before[prepay_month - 1]["balance"]

    new_principal     = max(0, balance_at_prepay - prepay_amount)


    if new_principal <= 0:

        print(f"\n  Prepayment fully closes the loan at month {prepay_month}!")

        interest_paid_so_far = sum(

            r["interest"] for r in schedule_before[:prepay_month]

        )

        total_paid = (prepay_month * orig["emi"]) + prepay_amount

        saved      = orig["total_payment"] - total_paid

        print(f"  Total paid     : {fmt(total_paid)}")

        print(f"  Interest saved : {fmt(orig['total_interest'] - interest_paid_so_far)}")

        return


    remaining_months = tenure_months - prepay_month

    new_summary      = calculate_summary(new_principal, annual_rate, remaining_months)


    interest_before  = sum(r["interest"] for r in schedule_before[:prepay_month])

    total_interest_new = interest_before + new_summary["total_interest"]

    interest_saved     = round(orig["total_interest"] - total_interest_new, 2)

    months_saved       = tenure_months - (prepay_month + remaining_months)


    print("\n" + "="*55)

    print("  PREPAYMENT IMPACT ANALYSIS")

    print("="*55)

    print(f"  Prepayment Amount  : {fmt(prepay_amount)} at month {prepay_month}")

    print(f"  Balance Before     : {fmt(balance_at_prepay)}")

    print(f"  Balance After      : {fmt(new_principal)}")

    print("-"*55)

    print(f"  Original Interest  : {fmt(orig['total_interest'])}")

    print(f"  New Total Interest : {fmt(total_interest_new)}")

    print(f"  Interest Saved     : {fmt(interest_saved)}")

    print(f"  New Monthly EMI    : {fmt(new_summary['emi'])}")

    print(f"  Old EMI            : {fmt(orig['emi'])}")

    print(f"  Months Saved       : {months_saved}")

    print("="*55)



# ============================================================

# EXPORT TO CSV

# ============================================================


def export_to_csv(schedule, summary, filename=None):

    if not filename:

        filename = f"emi_schedule_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"


    with open(filename, "w", newline="", encoding="utf-8") as f:

        writer = csv.writer(f)


        # Summary header

        writer.writerow(["LOAN EMI SCHEDULE"])

        writer.writerow(["Principal", fmt_plain(summary["principal"])])

        writer.writerow(["Annual Rate (%)", summary["annual_rate"]])

        writer.writerow(["Tenure (months)", summary["tenure_months"]])

        writer.writerow(["Monthly EMI", fmt_plain(summary["emi"])])

        writer.writerow(["Total Payment", fmt_plain(summary["total_payment"])])

        writer.writerow(["Total Interest", fmt_plain(summary["total_interest"])])

        writer.writerow([])


        # Table header

        writer.writerow(["Month", "Date", "EMI",

                         "Principal", "Interest", "Balance"])

        for row in schedule:

            writer.writerow([

                row["month"], row["pay_date"],

                fmt_plain(row["emi"]),

                fmt_plain(row["principal"]),

                fmt_plain(row["interest"]),

                fmt_plain(row["balance"])

            ])


    print(f"\n  Exported: {filename}")

    return filename



# ============================================================

# SAVE / LOAD HISTORY

# ============================================================


def save_to_history(summary):

    history = []

    if Path(HISTORY_FILE).exists():

        try:

            with open(HISTORY_FILE, "r") as f:

                history = json.load(f)

        except:

            pass


    summary["saved_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")

    history.append(summary)

    history = history[-20:]


    with open(HISTORY_FILE, "w") as f:

        json.dump(history, f, indent=2)

    print("  Saved to history.")



def show_history():

    if not Path(HISTORY_FILE).exists():

        print("\n  No history yet.")

        return


    try:

        with open(HISTORY_FILE, "r") as f:

            history = json.load(f)

    except:

        print("  Could not load history.")

        return


    print("\n" + "="*65)

    print(f"  CALCULATION HISTORY  ({len(history)} entries)")

    print("="*65)

    print(f"  {'DATE':<18} {'PRINCIPAL':>14} {'RATE':>6} "

          f"{'MONTHS':>7} {'EMI':>12}")

    print("  " + "-"*60)


    for h in reversed(history):

        print(f"  {h.get('saved_at',''):<18} "

              f"{fmt_plain(h['principal']):>14}  "

              f"{h['annual_rate']:>5}%  "

              f"{h['tenure_months']:>6}  "

              f"{fmt_plain(h['emi']):>12}")

    print("="*65)



# ============================================================

# GET LOAN INPUT

# ============================================================


def get_loan_input():

    print("\n  Enter Loan Details")

    print("  " + "-"*40)


    try:

        principal = float(input("  Loan Amount       : ").strip().replace(",", ""))

        if principal <= 0:

            print("  Amount must be positive.")

            return None, None, None


        rate = float(input("  Annual Interest % : ").strip())

        if rate < 0 or rate > 100:

            print("  Rate must be between 0 and 100.")

            return None, None, None


        tenure_input = input("  Tenure (months or Xy for years): ").strip()

        if tenure_input.lower().endswith("y"):

            tenure_months = int(float(tenure_input[:-1]) * 12)

        else:

            tenure_months = int(tenure_input)


        if tenure_months <= 0:

            print("  Tenure must be positive.")

            return None, None, None


        return principal, rate, tenure_months


    except ValueError:

        print("  Invalid input.")

        return None, None, None



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  LOAN EMI CALCULATOR")

    print("-"*48)

    print("  1. Calculate EMI & summary")

    print("  2. Full amortization table")

    print("  3. Yearly payment summary")

    print("  4. Compare rates & tenures")

    print("  5. Prepayment impact analysis")

    print("  6. Export schedule to CSV")

    print("  7. View calculation history")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     LOAN EMI CALCULATOR")

    print("="*55)

    print("\n  Calculate EMI for Home, Car, Personal loans.")

    print("  Generate full amortization & prepayment analysis.\n")


    last_summary  = None

    last_schedule = None


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice in ["1", "2", "3", "5", "6"]:

            if choice in ["2", "3", "5", "6"] and last_summary:

                reuse = input("\n  Use last loan details? (y/n): ").strip().lower()

                if reuse == "n":

                    last_summary = last_schedule = None


            if not last_summary:

                p, r, t = get_loan_input()

                if p is None:

                    continue

                last_summary  = calculate_summary(p, r, t)

                last_schedule = generate_amortization(p, r, t)


        if choice == "1":

            display_summary(last_summary)

            save_to_history(last_summary.copy())


        elif choice == "2":

            display_summary(last_summary)

            show_all = input("\n  Show ALL months? (y/n): ").strip().lower()

            display_amortization(last_schedule, show_all == "y")


        elif choice == "3":

            display_yearly_summary(last_schedule)


        elif choice == "4":

            try:

                p = float(input("\n  Loan amount for comparison: ")

                          .strip().replace(",", ""))

                compare_options(p)

            except ValueError:

                print("  Invalid amount.")


        elif choice == "5":

            if not last_summary:

                p, r, t = get_loan_input()

                if p is None:

                    continue

                last_summary  = calculate_summary(p, r, t)

                last_schedule = generate_amortization(p, r, t)


            display_summary(last_summary)

            try:

                prepay_amt   = float(input("\n  Prepayment amount : ").strip().replace(",", ""))

                prepay_month = int(input("  At which month    : ").strip())

                prepayment_impact(

                    last_summary["principal"],

                    last_summary["annual_rate"],

                    last_summary["tenure_months"],

                    prepay_amt, prepay_month

                )

            except ValueError:

                print("  Invalid input.")


        elif choice == "6":

            if last_summary and last_schedule:

                fname = input("  Filename (Enter for auto): ").strip() or None

                export_to_csv(last_schedule, last_summary, fname)

            else:

                print("  No data to export. Calculate EMI first.")


        elif choice == "7":

            show_history()


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Student Grade Calculator

import json

import os

import statistics

from datetime import datetime

from pathlib import Path


# ============================================================

# CONFIGURATION

# ============================================================


DATA_FILE    = "students_data.json"

PASSING_MARK = 40       # minimum marks to pass a subject

MAX_MARKS    = 100      # maximum marks per subject


# ============================================================

# GRADING SYSTEM

# ============================================================


def get_grade(marks, max_marks=100):

    """Return letter grade, grade point, and remarks."""

    pct = (marks / max_marks) * 100


    if pct >= 90:

        return "A+", 10.0, "Outstanding"

    elif pct >= 80:

        return "A",  9.0,  "Excellent"

    elif pct >= 70:

        return "B+", 8.0,  "Very Good"

    elif pct >= 60:

        return "B",  7.0,  "Good"

    elif pct >= 50:

        return "C+", 6.0,  "Above Average"

    elif pct >= 40:

        return "C",  5.0,  "Average"

    elif pct >= 35:

        return "D",  4.0,  "Pass (Marginal)"

    else:

        return "F",  0.0,  "Fail"



def get_class(percentage):

    """Return division/class based on overall percentage."""

    if percentage >= 75:

        return "First Class with Distinction"

    elif percentage >= 60:

        return "First Class"

    elif percentage >= 50:

        return "Second Class"

    elif percentage >= 40:

        return "Pass Class"

    else:

        return "Fail"



def calculate_gpa(subjects):

    """Calculate weighted GPA from a list of subject dicts."""

    total_points  = 0

    total_credits = 0

    for sub in subjects:

        _, gp, _ = get_grade(sub["marks_obtained"], sub["max_marks"])

        credits   = sub.get("credits", 1)

        total_points  += gp * credits

        total_credits += credits

    if total_credits == 0:

        return 0.0

    return round(total_points / total_credits, 2)



# ============================================================

# LOAD & SAVE DATA

# ============================================================


def load_data():

    if Path(DATA_FILE).exists():

        try:

            with open(DATA_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return {"students": []}



def save_data(data):

    with open(DATA_FILE, "w") as f:

        json.dump(data, f, indent=2)



# ============================================================

# ADD STUDENT

# ============================================================


def add_student(data):

    print("\n" + "="*50)

    print("  ADD NEW STUDENT")

    print("="*50)


    name     = input("  Student Name  : ").strip()

    roll_no  = input("  Roll Number   : ").strip()

    cls      = input("  Class/Semester: ").strip()

    section  = input("  Section       : ").strip() or "A"


    if not name or not roll_no:

        print("  Name and Roll Number are required.")

        return


    # Check duplicate roll number

    if any(s["roll_no"] == roll_no for s in data["students"]):

        print(f"  Roll number {roll_no} already exists.")

        return


    # Input subjects

    subjects = []

    print(f"\n  Enter subjects and marks (type 'done' to finish)")

    print(f"  Format: Subject Name | Marks Obtained | Max Marks | Credits")

    print(f"  Example: Mathematics | 85 | 100 | 4\n")


    while True:

        entry = input("  > ").strip()

        if entry.lower() == "done":

            if not subjects:

                print("  At least one subject is required.")

                continue

            break


        parts = [p.strip() for p in entry.split("|")]

        if len(parts) < 2:

            print("  Format: Subject | Marks | Max Marks (opt) | Credits (opt)")

            continue


        sub_name = parts[0]

        try:

            marks    = float(parts[1])

            max_m    = float(parts[2]) if len(parts) > 2 and parts[2] else MAX_MARKS

            credits  = float(parts[3]) if len(parts) > 3 and parts[3] else 1


            if marks < 0 or marks > max_m:

                print(f"  Invalid marks. Must be between 0 and {max_m}")

                continue


            grade, gp, remarks = get_grade(marks, max_m)

            subjects.append({

                "name":          sub_name,

                "marks_obtained": marks,

                "max_marks":     max_m,

                "credits":       credits,

                "grade":         grade,

                "grade_point":   gp,

                "remarks":       remarks,

                "passed":        marks >= (max_m * PASSING_MARK / MAX_MARKS)

            })

            print(f"  Added: {sub_name} — {marks}/{max_m}  Grade: {grade}  ({remarks})")


        except ValueError:

            print("  Invalid marks value.")

            continue


    # Calculate overall stats

    total_obtained = sum(s["marks_obtained"] for s in subjects)

    total_max      = sum(s["max_marks"]      for s in subjects)

    percentage     = round((total_obtained / total_max) * 100, 2) if total_max else 0

    gpa            = calculate_gpa(subjects)

    passed_all     = all(s["passed"] for s in subjects)

    division       = get_class(percentage)

    failed_subs    = [s["name"] for s in subjects if not s["passed"]]


    student = {

        "id":             len(data["students"]) + 1,

        "name":           name,

        "roll_no":        roll_no,

        "class":          cls,

        "section":        section,

        "subjects":       subjects,

        "total_obtained": total_obtained,

        "total_max":      total_max,

        "percentage":     percentage,

        "gpa":            gpa,

        "division":       division,

        "passed":         passed_all,

        "failed_subjects": failed_subs,

        "added_at":       datetime.now().strftime("%d-%m-%Y %H:%M:%S")

    }


    data["students"].append(student)

    save_data(data)


    print(f"\n  Student saved! Quick summary:")

    print(f"  Percentage : {percentage}%")

    print(f"  GPA        : {gpa}")

    print(f"  Result     : {'PASS' if passed_all else 'FAIL'}")

    print(f"  Division   : {division}")

    if failed_subs:

        print(f"  Failed in  : {', '.join(failed_subs)}")



# ============================================================

# PRINT REPORT CARD

# ============================================================


def print_report_card(student):

    w = 58

    print("\n" + "=" * w)

    print(f"  {'REPORT CARD':^{w-4}}")

    print("=" * w)

    print(f"  Name        : {student['name']}")

    print(f"  Roll No     : {student['roll_no']}")

    print(f"  Class       : {student['class']}  Section: {student['section']}")

    print(f"  Date        : {student['added_at'][:10]}")

    print("-" * w)

    print(f"  {'SUBJECT':<20} {'MAX':>5} {'OBT':>5} {'%':>6}  "

          f"{'GR':>3}  {'GP':>4}  REMARKS")

    print("-" * w)


    for sub in student["subjects"]:

        pct   = round((sub["marks_obtained"] / sub["max_marks"]) * 100, 1)

        pass_marker = " " if sub["passed"] else "*"

        print(f"  {sub['name']:<20} {sub['max_marks']:>5.0f} "

              f"{sub['marks_obtained']:>5.0f} {pct:>5.1f}%  "

              f"{sub['grade']:>3}  {sub['grade_point']:>4.1f}  "

              f"{sub['remarks']}{pass_marker}")


    print("-" * w)

    overall_pct = round((student["total_obtained"] / student["total_max"]) * 100, 1)

    print(f"  {'TOTAL':<20} {student['total_max']:>5.0f} "

          f"{student['total_obtained']:>5.0f} {overall_pct:>5.1f}%")

    print("=" * w)

    print(f"  GPA         : {student['gpa']} / 10.0")

    print(f"  Percentage  : {student['percentage']}%")

    print(f"  Division    : {student['division']}")

    result_str = "PASS" if student["passed"] else "FAIL"

    print(f"  Result      : {result_str}")

    if student.get("failed_subjects"):

        print(f"  Failed in   : {', '.join(student['failed_subjects'])}")

    print("=" * w)


    # Grade scale legend

    print(f"\n  Grade Scale: A+(90+)=10 | A(80+)=9 | B+(70+)=8 | "

          f"B(60+)=7 | C+(50+)=6 | C(40+)=5 | D(35+)=4 | F=0")

    print(f"  * = Failed subject (below passing marks)\n")



# ============================================================

# SEARCH STUDENT

# ============================================================


def search_student(data):

    query = input("\n  Search by name or roll number: ").strip().lower()

    results = [

        s for s in data["students"]

        if query in s["name"].lower() or query in s["roll_no"].lower()

    ]


    if not results:

        print(f"  No students found for: '{query}'")

        return None


    if len(results) == 1:

        return results[0]


    print(f"\n  Found {len(results)} students:")

    for i, s in enumerate(results, 1):

        print(f"  [{i}] {s['roll_no']}  {s['name']}  "

              f"Class: {s['class']}  {s['percentage']}%")

    try:

        idx = int(input("\n  Select number: ").strip()) - 1

        if 0 <= idx < len(results):

            return results[idx]

    except ValueError:

        pass

    return None



# ============================================================

# CLASS REPORT (ALL STUDENTS)

# ============================================================


def class_report(data):

    students = data["students"]

    if not students:

        print("\n  No students added yet.")

        return


    print("\n" + "="*70)

    print(f"  CLASS REPORT  ({len(students)} students)")

    print("="*70)

    print(f"  {'ROLL':<10} {'NAME':<22} {'%':>6}  "

          f"{'GPA':>5}  {'DIV':<30}  RESULT")

    print("  " + "-"*65)


    sorted_students = sorted(students,

                             key=lambda x: x["percentage"],

                             reverse=True)


    for rank, s in enumerate(sorted_students, 1):

        result = "PASS" if s["passed"] else "FAIL"

        print(f"  {s['roll_no']:<10} {s['name']:<22} "

              f"{s['percentage']:>5.1f}%  {s['gpa']:>5.2f}  "

              f"{s['division']:<30}  {result}")


    # Class statistics

    percentages = [s["percentage"] for s in students]

    gpas        = [s["gpa"]        for s in students]

    passed      = sum(1 for s in students if s["passed"])

    failed      = len(students) - passed


    print("  " + "-"*65)

    print(f"\n  CLASS STATISTICS")

    print(f"  Total Students : {len(students)}")

    print(f"  Passed         : {passed}  "

          f"({passed/len(students)*100:.1f}%)")

    print(f"  Failed         : {failed}  "

          f"({failed/len(students)*100:.1f}%)")

    print(f"  Highest %      : {max(percentages):.2f}%")

    print(f"  Lowest  %      : {min(percentages):.2f}%")

    print(f"  Average %      : {statistics.mean(percentages):.2f}%")

    print(f"  Median  %      : {statistics.median(percentages):.2f}%")

    print(f"  Average GPA    : {statistics.mean(gpas):.2f}")


    # Topper

    topper = max(students, key=lambda x: x["percentage"])

    print(f"\n  Class Topper   : {topper['name']} "

          f"({topper['roll_no']})  — {topper['percentage']}%")


    # Grade distribution

    grade_dist = {}

    for s in students:

        g, _, _ = get_grade(s["percentage"])

        grade_dist[g] = grade_dist.get(g, 0) + 1


    print(f"\n  Grade Distribution:")

    for grade in ["A+", "A", "B+", "B", "C+", "C", "D", "F"]:

        count = grade_dist.get(grade, 0)

        bar   = "█" * count

        print(f"  {grade:>3}: {count:>3}  {bar}")


    print("="*70)



# ============================================================

# SUBJECT-WISE ANALYSIS

# ============================================================


def subject_analysis(data):

    students = data["students"]

    if not students:

        print("\n  No data yet.")

        return


    # Collect all subjects

    sub_map = {}

    for s in students:

        for sub in s["subjects"]:

            name = sub["name"]

            if name not in sub_map:

                sub_map[name] = []

            sub_map[name].append(sub["marks_obtained"])


    if not sub_map:

        return


    print("\n" + "="*60)

    print("  SUBJECT-WISE ANALYSIS")

    print("="*60)

    print(f"  {'SUBJECT':<22} {'AVG':>6}  {'HIGH':>6}  "

          f"{'LOW':>6}  {'PASS%':>6}  STUDENTS")

    print("  " + "-"*56)


    for sub_name, marks_list in sorted(sub_map.items()):

        avg      = statistics.mean(marks_list)

        high     = max(marks_list)

        low      = min(marks_list)

        pass_pct = sum(1 for m in marks_list

                       if m >= PASSING_MARK) / len(marks_list) * 100

        print(f"  {sub_name:<22} {avg:>6.1f}  {high:>6.1f}  "

              f"{low:>6.1f}  {pass_pct:>5.1f}%  {len(marks_list)}")


    print("="*60)



# ============================================================

# DELETE STUDENT

# ============================================================


def delete_student(data):

    student = search_student(data)

    if not student:

        return

    confirm = input(f"\n  Delete {student['name']} ({student['roll_no']})? "

                    f"(yes/no): ").strip().lower()

    if confirm == "yes":

        data["students"] = [s for s in data["students"]

                            if s["roll_no"] != student["roll_no"]]

        save_data(data)

        print("  Student deleted.")

    else:

        print("  Cancelled.")



# ============================================================

# MAIN MENU

# ============================================================


def print_menu(data):

    count = len(data["students"])

    print("\n" + "-"*48)

    print(f"  STUDENT GRADE CALCULATOR  [{count} students]")

    print("-"*48)

    print("  1. Add new student with marks")

    print("  2. View student report card")

    print("  3. Search student")

    print("  4. Class report (all students)")

    print("  5. Subject-wise analysis")

    print("  6. Delete a student")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     STUDENT GRADE CALCULATOR")

    print("="*55)

    print(f"\n  Passing Marks : {PASSING_MARK}/{MAX_MARKS}")

    print(f"  Grading       : A+(90+) to F(<35)")

    print(f"  GPA Scale     : 0 - 10")


    data = load_data()

    if data["students"]:

        print(f"\n  Loaded {len(data['students'])} student(s) from {DATA_FILE}")


    while True:

        print_menu(data)

        choice = input("  > ").strip()


        if choice == "1":

            add_student(data)


        elif choice == "2":

            if not data["students"]:

                print("\n  No students added yet.")

                continue

            student = search_student(data)

            if student:

                print_report_card(student)


        elif choice == "3":

            if not data["students"]:

                print("\n  No students added yet.")

                continue

            student = search_student(data)

            if student:

                print(f"\n  Found: {student['name']}  |  "

                      f"Roll: {student['roll_no']}  |  "

                      f"Class: {student['class']}")

                print(f"  Percentage: {student['percentage']}%  |  "

                      f"GPA: {student['gpa']}  |  "

                      f"Result: {'PASS' if student['passed'] else 'FAIL'}")

                show = input("\n  Show full report card? (y/n): ").strip().lower()

                if show == "y":

                    print_report_card(student)


        elif choice == "4":

            class_report(data)


        elif choice == "5":

            subject_analysis(data)


        elif choice == "6":

            delete_student(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

CSV ↔ JSON Converter

import csv

import json

import os

from datetime import datetime

from pathlib import Path

from collections import OrderedDict


# ============================================================

# HELPERS

# ============================================================


def format_size(size_bytes):

    for unit in ["B", "KB", "MB", "GB"]:

        if size_bytes < 1024:

            return f"{size_bytes:.1f} {unit}"

        size_bytes /= 1024

    return f"{size_bytes:.1f} GB"



def detect_type(value):

    """Try to auto-detect and cast string values to int/float/bool."""

    if value is None or value == "":

        return None

    v = str(value).strip()

    if v.lower() in ("true", "yes"):

        return True

    if v.lower() in ("false", "no"):

        return False

    try:

        return int(v)

    except ValueError:

        pass

    try:

        return float(v)

    except ValueError:

        pass

    return v



def flatten_dict(d, parent_key="", sep="."):

    """Flatten nested JSON dict to single level for CSV."""

    items = []

    for k, v in d.items():

        new_key = f"{parent_key}{sep}{k}" if parent_key else k

        if isinstance(v, dict):

            items.extend(flatten_dict(v, new_key, sep).items())

        elif isinstance(v, list):

            items.append((new_key, json.dumps(v)))

        else:

            items.append((new_key, v))

    return dict(items)



# ============================================================

# CSV → JSON

# ============================================================


def csv_to_json(input_path, output_path=None,

                delimiter=",", auto_cast=True,

                indent=2, encoding="utf-8"):

    """

    Convert a CSV file to JSON.

    Returns the output path and record count.

    """

    input_path = Path(input_path)


    if not input_path.exists():

        print(f"  File not found: {input_path}")

        return None, 0


    if not output_path:

        output_path = input_path.with_suffix(".json")

    output_path = Path(output_path)


    records = []

    errors  = []


    try:

        with open(input_path, "r", encoding=encoding, newline="") as f:

            reader = csv.DictReader(f, delimiter=delimiter)

            headers = reader.fieldnames


            if not headers:

                print("  CSV file has no headers.")

                return None, 0


            print(f"\n  Reading CSV: {input_path.name}")

            print(f"  Columns ({len(headers)}): {', '.join(headers)}")


            for i, row in enumerate(reader, 1):

                if auto_cast:

                    record = {k: detect_type(v) for k, v in row.items()}

                else:

                    record = dict(row)

                records.append(record)


                if i % 1000 == 0:

                    print(f"  Processed {i} rows...", end="\r")


    except UnicodeDecodeError:

        print("  Encoding error. Trying latin-1...")

        return csv_to_json(input_path, output_path, delimiter,

                           auto_cast, indent, "latin-1")

    except Exception as e:

        print(f"  Error reading CSV: {e}")

        return None, 0


    # Write JSON

    try:

        with open(output_path, "w", encoding="utf-8") as f:

            json.dump(records, f, indent=indent, ensure_ascii=False)


        size = format_size(output_path.stat().st_size)

        print(f"\n  Converted {len(records)} records")

        print(f"  Output   : {output_path}  ({size})")

        return output_path, len(records)


    except Exception as e:

        print(f"  Error writing JSON: {e}")

        return None, 0



# ============================================================

# JSON → CSV

# ============================================================


def json_to_csv(input_path, output_path=None,

                delimiter=",", flatten=True,

                encoding="utf-8"):

    """

    Convert a JSON file (array of objects) to CSV.

    Returns the output path and record count.

    """

    input_path = Path(input_path)


    if not input_path.exists():

        print(f"  File not found: {input_path}")

        return None, 0


    if not output_path:

        output_path = input_path.with_suffix(".csv")

    output_path = Path(output_path)


    try:

        with open(input_path, "r", encoding=encoding) as f:

            data = json.load(f)

    except json.JSONDecodeError as e:

        print(f"  Invalid JSON: {e}")

        return None, 0

    except Exception as e:

        print(f"  Error reading JSON: {e}")

        return None, 0


    # Handle both list and single object

    if isinstance(data, dict):

        # Try common wrapper keys

        for key in ["data", "records", "results", "items", "rows"]:

            if key in data and isinstance(data[key], list):

                data = data[key]

                print(f"  Extracted records from key: '{key}'")

                break

        else:

            data = [data]


    if not isinstance(data, list) or not data:

        print("  JSON does not contain a list of records.")

        return None, 0


    # Flatten nested dicts if requested

    if flatten:

        data = [flatten_dict(r) if isinstance(r, dict) else {"value": r}

                for r in data]


    # Collect all unique keys across all records

    all_keys = list(OrderedDict.fromkeys(

        k for record in data if isinstance(record, dict)

        for k in record.keys()

    ))


    print(f"\n  Reading JSON: {input_path.name}")

    print(f"  Records   : {len(data)}")

    print(f"  Columns ({len(all_keys)}): {', '.join(str(k) for k in all_keys[:10])}"

          f"{'...' if len(all_keys) > 10 else ''}")


    try:

        with open(output_path, "w", encoding=encoding,

                  newline="") as f:

            writer = csv.DictWriter(f, fieldnames=all_keys,

                                    delimiter=delimiter,

                                    extrasaction="ignore")

            writer.writeheader()


            for i, record in enumerate(data, 1):

                if isinstance(record, dict):

                    # Convert lists/dicts in values to strings

                    row = {k: (json.dumps(v) if isinstance(v, (dict, list)) else v)

                           for k, v in record.items()}

                    writer.writerow(row)

                if i % 1000 == 0:

                    print(f"  Written {i} rows...", end="\r")


        size = format_size(output_path.stat().st_size)

        print(f"\n  Converted {len(data)} records")

        print(f"  Output   : {output_path}  ({size})")

        return output_path, len(data)


    except Exception as e:

        print(f"  Error writing CSV: {e}")

        return None, 0



# ============================================================

# PREVIEW FILE

# ============================================================


def preview_csv(filepath, rows=5):

    """Print first N rows of a CSV file."""

    try:

        with open(filepath, "r", encoding="utf-8", newline="") as f:

            reader = csv.reader(f)

            headers = next(reader, None)

            if not headers:

                print("  Empty CSV.")

                return


            print(f"\n  PREVIEW: {Path(filepath).name}")

            print("  " + "-"*60)


            # Header

            col_w = min(18, max(8, 60 // len(headers)))

            header_line = "  " + "  ".join(str(h)[:col_w].ljust(col_w) for h in headers)

            print(header_line)

            print("  " + "-"*60)


            # Rows

            for i, row in enumerate(reader):

                if i >= rows:

                    break

                line = "  " + "  ".join(str(v)[:col_w].ljust(col_w) for v in row)

                print(line)


            print("  " + "-"*60)


    except Exception as e:

        print(f"  Preview error: {e}")



def preview_json(filepath, rows=3):

    """Print first N records of a JSON file."""

    try:

        with open(filepath, "r", encoding="utf-8") as f:

            data = json.load(f)


        if isinstance(data, list):

            preview = data[:rows]

        elif isinstance(data, dict):

            preview = [data]

        else:

            preview = [{"value": data}]


        print(f"\n  PREVIEW: {Path(filepath).name}")

        print("  " + "-"*60)

        print(json.dumps(preview, indent=4, ensure_ascii=False)[:2000])

        if len(str(data)) > 2000:

            print("  ... (truncated)")

        print("  " + "-"*60)


    except Exception as e:

        print(f"  Preview error: {e}")



# ============================================================

# FILE INFO

# ============================================================


def file_info(filepath):

    p = Path(filepath)

    if not p.exists():

        print(f"  File not found: {filepath}")

        return


    size  = format_size(p.stat().st_size)

    mtime = datetime.fromtimestamp(p.stat().st_mtime).strftime("%d-%m-%Y %H:%M")

    ext   = p.suffix.lower()


    print(f"\n  File     : {p.name}")

    print(f"  Size     : {size}")

    print(f"  Modified : {mtime}")

    print(f"  Type     : {ext.upper()[1:]} file")


    if ext == ".csv":

        try:

            with open(filepath, "r", encoding="utf-8", newline="") as f:

                reader = csv.reader(f)

                headers = next(reader, [])

                row_count = sum(1 for _ in reader)

            print(f"  Rows     : {row_count:,}")

            print(f"  Columns  : {len(headers)}")

            print(f"  Headers  : {', '.join(headers[:8])}"

                  f"{'...' if len(headers) > 8 else ''}")

        except:

            pass


    elif ext == ".json":

        try:

            with open(filepath, "r", encoding="utf-8") as f:

                data = json.load(f)

            if isinstance(data, list):

                print(f"  Records  : {len(data):,}")

                if data and isinstance(data[0], dict):

                    keys = list(data[0].keys())

                    print(f"  Keys     : {', '.join(str(k) for k in keys[:8])}"

                          f"{'...' if len(keys) > 8 else ''}")

            elif isinstance(data, dict):

                print(f"  Keys     : {', '.join(list(data.keys())[:8])}")

        except:

            pass



# ============================================================

# BATCH CONVERT

# ============================================================


def batch_convert(folder, direction):

    folder = Path(folder)

    if not folder.is_dir():

        print("  Invalid folder.")

        return


    if direction == "csv_to_json":

        files = list(folder.glob("*.csv"))

        ext_from, ext_to = "CSV", "JSON"

    else:

        files = list(folder.glob("*.json"))

        ext_from, ext_to = "JSON", "CSV"


    if not files:

        print(f"  No {ext_from} files found in {folder}")

        return


    print(f"\n  Found {len(files)} {ext_from} file(s). Converting to {ext_to}...")

    success = 0


    for f in files:

        print(f"\n  [{f.name}]")

        if direction == "csv_to_json":

            out, count = csv_to_json(f)

        else:

            out, count = json_to_csv(f)

        if out:

            success += 1


    print(f"\n  Batch done: {success}/{len(files)} converted successfully.")



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  CSV  <->  JSON  CONVERTER")

    print("-"*48)

    print("  1. CSV  →  JSON")

    print("  2. JSON →  CSV")

    print("  3. Preview a CSV file")

    print("  4. Preview a JSON file")

    print("  5. File info & stats")

    print("  6. Batch convert CSV → JSON (folder)")

    print("  7. Batch convert JSON → CSV (folder)")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     CSV  <->  JSON  CONVERTER")

    print("="*55)

    print("\n  Bidirectional converter with auto type detection,")

    print("  nested JSON flattening, and batch processing.\n")


    while True:

        print_menu()

        choice = input("  > ").strip()


        # ── CSV → JSON ──────────────────────────────────────

        if choice == "1":

            print("\n  CSV to JSON Conversion")

            inp = input("  Input CSV file path: ").strip()

            if not inp:

                continue


            out = input("  Output JSON path (Enter = same name): ").strip() or None


            delim = input("  Delimiter (, or ; or Tab): ").strip()

            if delim.lower() == "tab":

                delim = "\t"

            elif delim not in [",", ";", "|", "\t"]:

                delim = ","


            cast = input("  Auto-detect types? (y/n, default y): ").strip().lower()

            cast = cast != "n"


            indent = input("  JSON indent spaces (default 2): ").strip()

            indent = int(indent) if indent.isdigit() else 2


            out_path, count = csv_to_json(inp, out, delim, cast, indent)


            if out_path and count:

                preview = input("\n  Preview output? (y/n): ").strip().lower()

                if preview == "y":

                    preview_json(out_path, rows=3)


        # ── JSON → CSV ──────────────────────────────────────

        elif choice == "2":

            print("\n  JSON to CSV Conversion")

            inp = input("  Input JSON file path: ").strip()

            if not inp:

                continue


            out = input("  Output CSV path (Enter = same name): ").strip() or None


            delim = input("  Delimiter (, or ; or Tab): ").strip()

            if delim.lower() == "tab":

                delim = "\t"

            elif delim not in [",", ";", "|", "\t"]:

                delim = ","


            flat = input("  Flatten nested objects? (y/n, default y): ").strip().lower()

            flat = flat != "n"


            out_path, count = json_to_csv(inp, out, delim, flat)


            if out_path and count:

                preview = input("\n  Preview output? (y/n): ").strip().lower()

                if preview == "y":

                    preview_csv(out_path, rows=5)


        # ── Preview CSV ─────────────────────────────────────

        elif choice == "3":

            path = input("\n  CSV file path: ").strip()

            rows = input("  Rows to preview (default 5): ").strip()

            rows = int(rows) if rows.isdigit() else 5

            preview_csv(path, rows)


        # ── Preview JSON ────────────────────────────────────

        elif choice == "4":

            path = input("\n  JSON file path: ").strip()

            rows = input("  Records to preview (default 3): ").strip()

            rows = int(rows) if rows.isdigit() else 3

            preview_json(path, rows)


        # ── File Info ───────────────────────────────────────

        elif choice == "5":

            path = input("\n  File path: ").strip()

            file_info(path)


        # ── Batch CSV → JSON ────────────────────────────────

        elif choice == "6":

            folder = input("\n  Folder path: ").strip()

            batch_convert(folder, "csv_to_json")


        # ── Batch JSON → CSV ────────────────────────────────

        elif choice == "7":

            folder = input("\n  Folder path: ").strip()

            batch_convert(folder, "json_to_csv")


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Personal Budget Tracker

import json

import os

from datetime import datetime

from pathlib import Path

from collections import defaultdict


try:

    import matplotlib.pyplot as plt

    import matplotlib.patches as mpatches

    MATPLOTLIB_OK = True

except ImportError:

    MATPLOTLIB_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


DATA_FILE    = "budget_data.json"

CURRENCY     = "INR"

CURRENCY_SYM = "₹"


# Default categories

EXPENSE_CATEGORIES = [

    "Food", "Transport", "Rent", "Utilities",

    "Entertainment", "Healthcare", "Shopping",

    "Education", "EMI / Loan", "Other"

]

INCOME_CATEGORIES = [

    "Salary", "Freelance", "Business",

    "Investment", "Gift", "Other"

]


# ============================================================

# LOAD & SAVE DATA

# ============================================================


def load_data():

    if Path(DATA_FILE).exists():

        try:

            with open(DATA_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return {"transactions": [], "budgets": {}}



def save_data(data):

    with open(DATA_FILE, "w") as f:

        json.dump(data, f, indent=2)



# ============================================================

# FORMAT AMOUNT

# ============================================================


def fmt(amount):

    return f"{CURRENCY_SYM}{amount:,.2f}"



# ============================================================

# ADD TRANSACTION

# ============================================================


def add_transaction(data, t_type):

    """Add an income or expense entry."""

    print(f"\n  Add {t_type.upper()}")

    print("  " + "-"*40)


    categories = INCOME_CATEGORIES if t_type == "income" else EXPENSE_CATEGORIES


    print("  Categories:")

    for i, cat in enumerate(categories, 1):

        print(f"  {i:>2}. {cat}")


    try:

        cat_idx = int(input("\n  Choose category (number): ").strip()) - 1

        if cat_idx < 0 or cat_idx >= len(categories):

            print("  Invalid category.")

            return

        category = categories[cat_idx]

    except ValueError:

        print("  Invalid input.")

        return


    try:

        amount = float(input("  Amount: ").strip())

        if amount <= 0:

            print("  Amount must be positive.")

            return

    except ValueError:

        print("  Invalid amount.")

        return


    note = input("  Note (optional): ").strip()


    date_input = input("  Date (DD-MM-YYYY) or Enter for today: ").strip()

    if date_input:

        try:

            date = datetime.strptime(date_input, "%d-%m-%Y").strftime("%d-%m-%Y")

        except ValueError:

            print("  Invalid date format. Using today.")

            date = datetime.now().strftime("%d-%m-%Y")

    else:

        date = datetime.now().strftime("%d-%m-%Y")


    entry = {

        "id":       len(data["transactions"]) + 1,

        "type":     t_type,

        "category": category,

        "amount":   amount,

        "note":     note,

        "date":     date,

        "added_at": datetime.now().strftime("%d-%m-%Y %H:%M:%S")

    }


    data["transactions"].append(entry)

    save_data(data)


    icon = "+" if t_type == "income" else "-"

    print(f"\n  {icon} Saved: {category} — {fmt(amount)} on {date}")

    if note:

        print(f"  Note: {note}")



# ============================================================

# VIEW TRANSACTIONS

# ============================================================


def view_transactions(data, month=None, t_type=None):

    txns = data["transactions"]


    # Filter by month (MM-YYYY)

    if month:

        txns = [t for t in txns if t["date"][3:] == month]


    # Filter by type

    if t_type:

        txns = [t for t in txns if t["type"] == t_type]


    if not txns:

        print("\n  No transactions found.")

        return


    # Sort by date descending

    txns_sorted = sorted(txns,

                         key=lambda x: datetime.strptime(x["date"], "%d-%m-%Y"),

                         reverse=True)


    print("\n" + "="*68)

    label = f"{t_type.upper() if t_type else 'ALL'}"

    label += f"  [{month}]" if month else ""

    print(f"  TRANSACTIONS — {label}  ({len(txns)} entries)")

    print("="*68)

    print(f"  {'#':<5} {'DATE':<13} {'TYPE':<10} {'CATEGORY':<16} "

          f"{'AMOUNT':>12}  NOTE")

    print("  " + "-"*63)


    for t in txns_sorted[:50]:    # show last 50

        icon = "+" if t["type"] == "income" else "-"

        note = t.get("note", "")[:20]

        print(f"  {t['id']:<5} {t['date']:<13} {t['type']:<10} "

              f"{t['category']:<16} {icon}{fmt(t['amount']):>12}  {note}")


    if len(txns_sorted) > 50:

        print(f"  ... and {len(txns_sorted) - 50} more. Filter by month to see all.")


    total_in  = sum(t["amount"] for t in txns if t["type"] == "income")

    total_out = sum(t["amount"] for t in txns if t["type"] == "expense")

    balance   = total_in - total_out


    print("  " + "-"*63)

    print(f"  Total Income  : {fmt(total_in)}")

    print(f"  Total Expense : {fmt(total_out)}")

    b_sign = "+" if balance >= 0 else ""

    print(f"  Balance       : {b_sign}{fmt(balance)}")

    print("="*68)



# ============================================================

# MONTHLY SUMMARY

# ============================================================


def monthly_summary(data):

    txns = data["transactions"]

    if not txns:

        print("\n  No data yet.")

        return


    # Group by month

    monthly = defaultdict(lambda: {"income": 0, "expense": 0,

                                    "by_cat": defaultdict(float)})

    for t in txns:

        month = t["date"][3:]     # MM-YYYY

        monthly[month][t["type"]] += t["amount"]

        if t["type"] == "expense":

            monthly[month]["by_cat"][t["category"]] += t["amount"]


    # Sort months chronologically

    def month_key(m):

        try:

            return datetime.strptime(m, "%m-%Y")

        except:

            return datetime.min


    sorted_months = sorted(monthly.keys(), key=month_key)


    print("\n" + "="*60)

    print("  MONTHLY SUMMARY")

    print("="*60)

    print(f"  {'MONTH':<12} {'INCOME':>12} {'EXPENSE':>12} "

          f"{'BALANCE':>12}  STATUS")

    print("  " + "-"*56)


    for month in sorted_months:

        m     = monthly[month]

        bal   = m["income"] - m["expense"]

        sign  = "+" if bal >= 0 else ""

        status = "SURPLUS" if bal >= 0 else "DEFICIT"

        bar    = _mini_bar(m["income"], m["expense"])

        print(f"  {month:<12} {fmt(m['income']):>12} "

              f"{fmt(m['expense']):>12} "

              f"{sign}{fmt(bal):>12}  {status}  {bar}")


    # Overall totals

    total_in  = sum(m["income"]  for m in monthly.values())

    total_out = sum(m["expense"] for m in monthly.values())

    total_bal = total_in - total_out


    print("  " + "-"*56)

    print(f"  {'TOTAL':<12} {fmt(total_in):>12} "

          f"{fmt(total_out):>12} "

          f"{('+' if total_bal>=0 else '')}{fmt(total_bal):>12}")

    print("="*60)



def _mini_bar(income, expense):

    if max(income, expense) == 0:

        return ""

    scale = 12

    i_bar = int((income  / max(income, expense)) * scale)

    e_bar = int((expense / max(income, expense)) * scale)

    return f"I:{'█'*i_bar}  E:{'█'*e_bar}"



# ============================================================

# CATEGORY BREAKDOWN

# ============================================================


def category_breakdown(data, month=None):

    txns = data["transactions"]

    if month:

        txns = [t for t in txns if t["date"][3:] == month]


    expenses = [t for t in txns if t["type"] == "expense"]

    incomes  = [t for t in txns if t["type"] == "income"]


    if not expenses and not incomes:

        print("\n  No data for this period.")

        return


    def show_breakdown(items, label):

        cat_totals = defaultdict(float)

        for t in items:

            cat_totals[t["category"]] += t["amount"]

        total = sum(cat_totals.values())

        if not cat_totals:

            return

        sorted_cats = sorted(cat_totals.items(), key=lambda x: x[1], reverse=True)

        max_val = sorted_cats[0][1]


        print(f"\n  {label}  (Total: {fmt(total)})")

        print("  " + "-"*55)

        print(f"  {'CATEGORY':<18} {'AMOUNT':>12}  {'%':>5}  BAR")

        print("  " + "-"*55)

        for cat, amt in sorted_cats:

            pct = (amt / total * 100) if total else 0

            bar = "█" * int((amt / max_val) * 20)

            print(f"  {cat:<18} {fmt(amt):>12}  {pct:>4.1f}%  {bar}")


    title = f"CATEGORY BREAKDOWN"

    if month:

        title += f"  [{month}]"

    print("\n" + "="*58)

    print(f"  {title}")

    print("="*58)


    show_breakdown(expenses, "EXPENSES")

    show_breakdown(incomes,  "INCOME SOURCES")



# ============================================================

# BUDGET GOALS

# ============================================================


def set_budget(data):

    print("\n  SET MONTHLY BUDGET LIMITS")

    print("  Categories:")

    for i, cat in enumerate(EXPENSE_CATEGORIES, 1):

        current = data["budgets"].get(cat, 0)

        current_str = f"  (current: {fmt(current)})" if current else ""

        print(f"  {i:>2}. {cat}{current_str}")


    try:

        idx = int(input("\n  Choose category: ").strip()) - 1

        if idx < 0 or idx >= len(EXPENSE_CATEGORIES):

            print("  Invalid.")

            return

        cat = EXPENSE_CATEGORIES[idx]

        amt = float(input(f"  Budget limit for {cat}: ").strip())

        data["budgets"][cat] = amt

        save_data(data)

        print(f"  Budget set: {cat} = {fmt(amt)}/month")

    except ValueError:

        print("  Invalid input.")



def check_budgets(data):

    budgets = data.get("budgets", {})

    if not budgets:

        print("\n  No budget limits set. Use option to set budgets.")

        return


    # Current month

    this_month = datetime.now().strftime("%m-%Y")

    txns = [t for t in data["transactions"]

            if t["type"] == "expense" and t["date"][3:] == this_month]


    cat_spent = defaultdict(float)

    for t in txns:

        cat_spent[t["category"]] += t["amount"]


    print("\n" + "="*58)

    print(f"  BUDGET STATUS — {this_month}")

    print("="*58)

    print(f"  {'CATEGORY':<18} {'BUDGET':>12} {'SPENT':>12} "

          f"{'LEFT':>12}  STATUS")

    print("  " + "-"*54)


    for cat, budget in sorted(budgets.items()):

        spent = cat_spent.get(cat, 0)

        left  = budget - spent

        pct   = (spent / budget * 100) if budget else 0

        bar   = "█" * min(int(pct / 5), 20)


        if pct >= 100:

            status = "OVER BUDGET!"

        elif pct >= 80:

            status = "WARNING"

        elif pct >= 50:

            status = "ON TRACK"

        else:

            status = "OK"


        print(f"  {cat:<18} {fmt(budget):>12} {fmt(spent):>12} "

              f"{fmt(left):>12}  {status}")

        print(f"  {'':18} {pct:>5.1f}% used  {bar}")


    print("="*58)



# ============================================================

# MATPLOTLIB CHARTS (Optional)

# ============================================================


def plot_monthly_chart(data):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    txns = data["transactions"]

    if not txns:

        print("\n  No data to plot.")

        return


    monthly = defaultdict(lambda: {"income": 0, "expense": 0})

    for t in txns:

        month = t["date"][3:]

        monthly[month][t["type"]] += t["amount"]


    def month_key(m):

        try:

            return datetime.strptime(m, "%m-%Y")

        except:

            return datetime.min


    months  = sorted(monthly.keys(), key=month_key)[-12:]

    incomes  = [monthly[m]["income"]  for m in months]

    expenses = [monthly[m]["expense"] for m in months]

    balances = [i - e for i, e in zip(incomes, expenses)]


    x = range(len(months))

    width = 0.35


    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

    fig.suptitle("Personal Budget Overview", fontsize=14, fontweight="bold")


    # Bar chart — income vs expense

    bars1 = ax1.bar([i - width/2 for i in x], incomes,

                    width, label="Income", color="#2ecc71", alpha=0.85)

    bars2 = ax1.bar([i + width/2 for i in x], expenses,

                    width, label="Expense", color="#e74c3c", alpha=0.85)

    ax1.set_title("Monthly Income vs Expense")

    ax1.set_xticks(list(x))

    ax1.set_xticklabels(months, rotation=45, ha="right")

    ax1.set_ylabel(f"Amount ({CURRENCY})")

    ax1.legend()

    ax1.yaxis.set_major_formatter(

        plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")

    )

    ax1.grid(axis="y", alpha=0.3)


    # Line chart — balance

    colors = ["#2ecc71" if b >= 0 else "#e74c3c" for b in balances]

    ax2.bar(list(x), balances, color=colors, alpha=0.8)

    ax2.axhline(0, color="black", linewidth=0.8, linestyle="--")

    ax2.set_title("Monthly Balance (Surplus / Deficit)")

    ax2.set_xticks(list(x))

    ax2.set_xticklabels(months, rotation=45, ha="right")

    ax2.set_ylabel(f"Balance ({CURRENCY})")

    ax2.yaxis.set_major_formatter(

        plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")

    )

    ax2.grid(axis="y", alpha=0.3)


    plt.tight_layout()

    plt.savefig("budget_chart.png", dpi=120, bbox_inches="tight")

    print("\n  Chart saved: budget_chart.png")

    plt.show()



def plot_expense_pie(data, month=None):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    txns = data["transactions"]

    if month:

        txns = [t for t in txns if t["date"][3:] == month]

    expenses = [t for t in txns if t["type"] == "expense"]


    if not expenses:

        print("\n  No expense data to plot.")

        return


    cat_totals = defaultdict(float)

    for t in expenses:

        cat_totals[t["category"]] += t["amount"]


    labels = list(cat_totals.keys())

    values = list(cat_totals.values())

    colors = plt.cm.Set3.colors[:len(labels)]


    fig, ax = plt.subplots(figsize=(9, 7))

    wedges, texts, autotexts = ax.pie(

        values, labels=labels, autopct="%1.1f%%",

        colors=colors, startangle=140,

        wedgeprops={"edgecolor": "white", "linewidth": 1.5}

    )

    for at in autotexts:

        at.set_fontsize(9)


    title = f"Expense Breakdown"

    if month:

        title += f" — {month}"

    ax.set_title(title, fontsize=13, fontweight="bold")


    total = sum(values)

    ax.text(0, -1.3, f"Total: {fmt(total)}",

            ha="center", fontsize=11, color="#333")


    plt.tight_layout()

    plt.savefig("expense_pie.png", dpi=120, bbox_inches="tight")

    print("\n  Pie chart saved: expense_pie.png")

    plt.show()



# ============================================================

# DELETE TRANSACTION

# ============================================================


def delete_transaction(data):

    try:

        tid = int(input("\n  Enter transaction ID to delete: ").strip())

    except ValueError:

        print("  Invalid ID.")

        return


    for i, t in enumerate(data["transactions"]):

        if t["id"] == tid:

            confirm = input(f"  Delete: {t['type']} {fmt(t['amount'])} "

                            f"— {t['category']} on {t['date']}? (y/n): ").strip().lower()

            if confirm == "y":

                data["transactions"].pop(i)

                save_data(data)

                print("  Deleted.")

            else:

                print("  Cancelled.")

            return


    print(f"  Transaction ID {tid} not found.")



# ============================================================

# QUICK STATS

# ============================================================


def quick_stats(data):

    txns = data["transactions"]

    if not txns:

        print("\n  No data yet.")

        return


    this_month = datetime.now().strftime("%m-%Y")

    this_txns  = [t for t in txns if t["date"][3:] == this_month]


    total_in   = sum(t["amount"] for t in txns if t["type"] == "income")

    total_out  = sum(t["amount"] for t in txns if t["type"] == "expense")

    balance    = total_in - total_out


    month_in   = sum(t["amount"] for t in this_txns if t["type"] == "income")

    month_out  = sum(t["amount"] for t in this_txns if t["type"] == "expense")

    month_bal  = month_in - month_out


    print("\n" + "="*50)

    print("  QUICK STATS")

    print("="*50)

    print(f"  This Month ({this_month}):")

    print(f"    Income   : {fmt(month_in)}")

    print(f"    Expense  : {fmt(month_out)}")

    b = "+" if month_bal >= 0 else ""

    print(f"    Balance  : {b}{fmt(month_bal)}")

    print(f"\n  All Time:")

    print(f"    Income   : {fmt(total_in)}")

    print(f"    Expense  : {fmt(total_out)}")

    b = "+" if balance >= 0 else ""

    print(f"    Balance  : {b}{fmt(balance)}")

    print(f"    Entries  : {len(txns)}")

    print("="*50)



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  PERSONAL BUDGET TRACKER")

    print("-"*48)

    print("  1.  Add income")

    print("  2.  Add expense")

    print("  3.  View transactions")

    print("  4.  Monthly summary")

    print("  5.  Category breakdown")

    print("  6.  Budget limits & status")

    print("  7.  Set budget limit for a category")

    print("  8.  Plot monthly income vs expense chart")

    print("  9.  Plot expense pie chart")

    print("  10. Quick stats")

    print("  11. Delete a transaction")

    print("  0.  Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     PERSONAL BUDGET TRACKER")

    print("="*55)


    data = load_data()

    count = len(data["transactions"])

    if count:

        print(f"\n  Loaded {count} transaction(s) from {DATA_FILE}")

    else:

        print(f"\n  No data yet. Start by adding income or expenses.")


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice == "1":

            add_transaction(data, "income")


        elif choice == "2":

            add_transaction(data, "expense")


        elif choice == "3":

            print("\n  Filter options:")

            print("  1. All transactions")

            print("  2. By month")

            print("  3. Income only")

            print("  4. Expenses only")

            sub = input("  Choice: ").strip()

            if sub == "1":

                view_transactions(data)

            elif sub == "2":

                month = input("  Month (MM-YYYY e.g. 04-2026): ").strip()

                view_transactions(data, month=month)

            elif sub == "3":

                view_transactions(data, t_type="income")

            elif sub == "4":

                view_transactions(data, t_type="expense")


        elif choice == "4":

            monthly_summary(data)


        elif choice == "5":

            month = input("\n  Month (MM-YYYY) or Enter for all time: ").strip()

            category_breakdown(data, month=month or None)


        elif choice == "6":

            check_budgets(data)


        elif choice == "7":

            set_budget(data)


        elif choice == "8":

            plot_monthly_chart(data)


        elif choice == "9":

            month = input("\n  Month (MM-YYYY) or Enter for all time: ").strip()

            plot_expense_pie(data, month=month or None)


        elif choice == "10":

            quick_stats(data)


        elif choice == "11":

            view_transactions(data)

            delete_transaction(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()