Habit Tracker

import json

import os

from datetime import datetime, date, timedelta

from pathlib import Path

from collections import defaultdict


try:

    import matplotlib.pyplot as plt

    import matplotlib.colors as mcolors

    import numpy as np

    MATPLOTLIB_OK = True

except ImportError:

    MATPLOTLIB_OK = False


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

# CONFIGURATION

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


DATA_FILE = "habits.json"


DAYS_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]

MONTHS     = ["Jan","Feb","Mar","Apr","May","Jun",

              "Jul","Aug","Sep","Oct","Nov","Dec"]


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

# LOAD & SAVE

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


def load_data():

    if Path(DATA_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return {"habits": [], "logs": {}}



def save_data(data):

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

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



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

# HELPERS

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


def today_str():

    return str(date.today())



def date_range(start_str, end_str):

    """Yield date strings from start to end inclusive."""

    start = datetime.strptime(start_str, "%Y-%m-%d").date()

    end   = datetime.strptime(end_str,   "%Y-%m-%d").date()

    cur   = start

    while cur <= end:

        yield str(cur)

        cur += timedelta(days=1)



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

    if max_val == 0:

        return ""

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

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



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

# STREAK CALCULATION

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


def calculate_streak(habit_id, logs):

    """Return current streak and longest streak for a habit."""

    today      = date.today()

    cur_streak = 0

    max_streak = 0

    tmp_streak = 0


    # Collect all logged dates for this habit

    logged_dates = sorted(

        [d for d, h in logs.items() if habit_id in h and h[habit_id]],

        reverse=True

    )


    if not logged_dates:

        return 0, 0


    # Current streak: count back from today

    check = today

    for _ in range(len(logged_dates) + 1):

        d_str = str(check)

        if d_str in logs and habit_id in logs[d_str] and logs[d_str][habit_id]:

            cur_streak += 1

            check -= timedelta(days=1)

        else:

            # Allow today to not be checked yet

            if check == today:

                check -= timedelta(days=1)

                continue

            break


    # Longest streak: scan all logged dates

    all_dates = sorted(

        [datetime.strptime(d, "%Y-%m-%d").date()

         for d in logs if habit_id in logs[d] and logs[d][habit_id]]

    )


    if all_dates:

        tmp_streak = 1

        max_streak = 1

        for i in range(1, len(all_dates)):

            if (all_dates[i] - all_dates[i-1]).days == 1:

                tmp_streak += 1

                max_streak = max(max_streak, tmp_streak)

            else:

                tmp_streak = 1


    return cur_streak, max_streak



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

# COMPLETION RATE

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


def completion_rate(habit_id, logs, days=30):

    """Return completion % over last N days."""

    today = date.today()

    count = 0

    total = 0


    for i in range(days):

        d = str(today - timedelta(days=i))

        # Only count days since habit was created

        total += 1

        if d in logs and habit_id in logs[d] and logs[d][habit_id]:

            count += 1


    return round((count / total * 100) if total else 0, 1)



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

# ADD HABIT

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


def add_habit(data):

    print("\n  ADD NEW HABIT")

    print("  " + "-"*40)


    name = input("  Habit name*  : ").strip()

    if not name:

        print("  Name is required.")

        return


    if any(h["name"].lower() == name.lower() for h in data["habits"]):

        print(f"  Habit '{name}' already exists.")

        return


    desc   = input("  Description  : ").strip()

    icon   = input("  Icon/emoji   : ").strip() or "•"


    print("  Frequency:")

    print("  1. Daily  2. Weekdays only  3. Weekends only  4. Custom days")

    freq_choice = input("  Choice (default 1): ").strip() or "1"


    freq_map = {"1": "daily", "2": "weekdays", "3": "weekends", "4": "custom"}

    frequency = freq_map.get(freq_choice, "daily")


    custom_days = []

    if frequency == "custom":

        print("  Enter days (1=Mon, 2=Tue, ..., 7=Sun), comma separated:")

        raw = input("  Days: ").strip()

        try:

            custom_days = [int(d.strip()) for d in raw.split(",")

                           if d.strip().isdigit() and 1 <= int(d.strip()) <= 7]

        except:

            custom_days = list(range(1, 8))


    goal = input("  Daily goal/target (optional, e.g. '8 glasses' or '30 min'): ").strip()


    habit = {

        "id":          f"h{len(data['habits']) + 1}_{datetime.now().strftime('%H%M%S')}",

        "name":        name,

        "description": desc,

        "icon":        icon,

        "frequency":   frequency,

        "custom_days": custom_days,

        "goal":        goal,

        "active":      True,

        "created":     today_str(),

        "color":       ["green", "blue", "orange", "purple", "red", "teal"][

                           len(data["habits"]) % 6

                       ],

    }


    data["habits"].append(habit)

    save_data(data)

    print(f"\n  Habit added: {icon} {name}")



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

# DAILY CHECK-IN

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


def is_scheduled(habit, date_str):

    """Check if a habit is scheduled for a given date."""

    freq = habit.get("frequency", "daily")

    d    = datetime.strptime(date_str, "%Y-%m-%d")

    dow  = d.isoweekday()   # 1=Mon, 7=Sun


    if freq == "daily":

        return True

    elif freq == "weekdays":

        return dow <= 5

    elif freq == "weekends":

        return dow >= 6

    elif freq == "custom":

        return dow in habit.get("custom_days", list(range(1, 8)))

    return True



def daily_checkin(data):

    today     = today_str()

    scheduled = [h for h in data["habits"]

                 if h["active"] and is_scheduled(h, today)]


    if not scheduled:

        print("\n  No habits scheduled for today!")

        return


    if today not in data["logs"]:

        data["logs"][today] = {}


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

    print(f"  DAILY CHECK-IN  —  {today}")

    print(f"  {datetime.now().strftime('%A, %d %B %Y')}")

    print("="*55)


    completed = 0

    for habit in scheduled:

        hid      = habit["id"]

        already  = data["logs"][today].get(hid, None)

        status   = "✓" if already else "○"

        goal_str = f"  Goal: {habit['goal']}" if habit.get("goal") else ""

        streak, _ = calculate_streak(hid, data["logs"])


        print(f"\n  {habit['icon']} {habit['name']}{goal_str}")

        print(f"     Current streak: {streak} day(s)")

        if already is not None:

            print(f"     Status: {'DONE' if already else 'SKIPPED'} (already logged)")

            if already:

                completed += 1

            relog = input("     Change? (y/n): ").strip().lower()

            if relog != "y":

                continue


        ans = input(f"     Completed today? (y/n/s=skip): ").strip().lower()

        if ans == "y":

            data["logs"][today][hid] = True

            completed += 1

            print(f"     Marked DONE! Streak: {streak + 1} day(s)")

        elif ans == "n":

            data["logs"][today][hid] = False

            print(f"     Marked as not done.")

        # 's' or anything else = skip (don't log)


    save_data(data)


    total = len(scheduled)

    pct   = round(completed / total * 100) if total else 0

    bar   = draw_bar(completed, total)


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

    print(f"  Today's progress: {completed}/{total}  ({pct}%)")

    print(f"  {bar}")

    if pct == 100:

        print("  PERFECT DAY! All habits completed!")

    print("="*55)



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

# VIEW ALL HABITS

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


def view_habits(data):

    habits = [h for h in data["habits"] if h["active"]]

    if not habits:

        print("\n  No active habits. Add one!")

        return


    today = today_str()

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

    print(f"  YOUR HABITS  ({len(habits)} active)")

    print("="*70)

    print(f"  {'HABIT':<22} {'STREAK':>7} {'BEST':>6} {'7D%':>6} "

          f"{'30D%':>6}  TODAY")

    print("  " + "-"*65)


    for h in habits:

        hid          = h["id"]

        streak, best = calculate_streak(hid, data["logs"])

        rate_7       = completion_rate(hid, data["logs"], 7)

        rate_30      = completion_rate(hid, data["logs"], 30)


        today_status = data["logs"].get(today, {}).get(hid)

        if today_status is True:

            today_icon = "✓ DONE"

        elif today_status is False:

            today_icon = "✗ MISS"

        elif is_scheduled(h, today):

            today_icon = "○ DUE"

        else:

            today_icon = "- REST"


        name_str = f"{h['icon']} {h['name']}"[:20]

        print(f"  {name_str:<22} {streak:>6}d {best:>5}d "

              f"{rate_7:>5.0f}% {rate_30:>5.0f}%  {today_icon}")


    print("="*70)



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

# HABIT DETAIL

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


def habit_detail(data, habit):

    hid          = habit["id"]

    streak, best = calculate_streak(hid, data["logs"])

    rate_7       = completion_rate(hid, data["logs"], 7)

    rate_30      = completion_rate(hid, data["logs"], 30)

    rate_all     = completion_rate(hid, data["logs"], 365)


    # Count total completions

    total_done = sum(

        1 for d, h in data["logs"].items()

        if hid in h and h[hid]

    )


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

    print(f"  {habit['icon']} {habit['name']}")

    print("="*52)

    if habit.get("description"):

        print(f"  {habit['description']}")

    if habit.get("goal"):

        print(f"  Goal: {habit['goal']}")

    print(f"  Frequency   : {habit['frequency'].capitalize()}")

    print(f"  Started     : {habit['created']}")

    print("-"*52)

    print(f"  Current streak : {streak} day(s)")

    print(f"  Longest streak : {best} day(s)")

    print(f"  Total done     : {total_done} time(s)")

    print(f"  Last 7 days    : {rate_7}%  {draw_bar(rate_7, 100, 20)}")

    print(f"  Last 30 days   : {rate_30}%  {draw_bar(rate_30, 100, 20)}")

    print(f"  Last 365 days  : {rate_all}%  {draw_bar(rate_all, 100, 20)}")


    # Last 14 days mini-calendar

    print(f"\n  Last 14 days:")

    today = date.today()

    row1  = "  "

    row2  = "  "

    for i in range(13, -1, -1):

        d   = str(today - timedelta(days=i))

        val = data["logs"].get(d, {}).get(hid)

        if val is True:

            row1 += "█ "

        elif val is False:

            row1 += "░ "

        else:

            row1 += "· "

        day_label = (today - timedelta(days=i)).strftime("%d")

        row2 += f"{day_label} "

    print(row1)

    print(row2)

    print("  (█=done  ░=missed  ·=not logged)")

    print("="*52)



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

# ASCII HEATMAP (last 12 weeks)

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


def ascii_heatmap(data, habit=None):

    today   = date.today()

    weeks   = 12

    start   = today - timedelta(weeks=weeks)


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

    if habit:

        print(f"  HEATMAP: {habit['icon']} {habit['name']}")

    else:

        print("  HEATMAP: All Habits Combined")

    print("="*65)


    # Build week grid

    # Start from Monday of start week

    start_monday = start - timedelta(days=start.weekday())


    grid = []   # list of weeks, each week = list of 7 day completion %

    cur  = start_monday


    while cur <= today:

        week = []

        for d in range(7):

            day    = cur + timedelta(days=d)

            d_str  = str(day)

            if day > today:

                week.append(None)

                continue


            if habit:

                # Single habit

                val = data["logs"].get(d_str, {}).get(habit["id"])

                pct = 100 if val is True else (0 if val is False else None)

            else:

                # All habits

                scheduled = [h for h in data["habits"]

                             if h["active"] and is_scheduled(h, d_str)]

                if not scheduled:

                    pct = None

                else:

                    done = sum(1 for h in scheduled

                               if data["logs"].get(d_str, {}).get(h["id"]) is True)

                    pct  = round(done / len(scheduled) * 100)


            week.append(pct)

        grid.append(week)

        cur += timedelta(days=7)


    # Print day labels

    print("       " + "  ".join(DAYS_SHORT))

    print("       " + "-" * (len(DAYS_SHORT) * 5 - 1))


    # Print month labels and heatmap rows

    for w_idx, week in enumerate(grid):

        # Week label (show month when it changes)

        first_day = start_monday + timedelta(weeks=w_idx)

        week_label = first_day.strftime("%d%b")


        row = f"  {week_label:<5} "

        for pct in week:

            if pct is None:

                row += "·   "

            elif pct == 0:

                row += "░   "

            elif pct < 50:

                row += "▒   "

            elif pct < 100:

                row += "▓   "

            else:

                row += "█   "

        print(row)


    print("\n  Legend: · = no data  ░ = 0%  ▒ = <50%  ▓ = <100%  █ = 100%")

    print("="*65)



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

# WEEKLY REPORT

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


def weekly_report(data):

    today  = date.today()

    monday = today - timedelta(days=today.weekday())

    habits = [h for h in data["habits"] if h["active"]]


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

    print(f"  WEEKLY REPORT  —  Week of {monday.strftime('%d %b %Y')}")

    print("="*65)


    days_of_week = [(monday + timedelta(days=i)) for i in range(7)]

    day_labels   = [d.strftime("%a %d") for d in days_of_week]


    # Header

    print(f"  {'HABIT':<20}", end="")

    for label in day_labels:

        print(f"  {label:<8}", end="")

    print(f"  {'%':>5}")

    print("  " + "-"*80)


    for h in habits:

        print(f"  {(h['icon'] + ' ' + h['name'])[:19]:<20}", end="")

        done_count = 0

        sched_count = 0

        for d in days_of_week:

            d_str = str(d)

            if is_scheduled(h, d_str):

                sched_count += 1

                val = data["logs"].get(d_str, {}).get(h["id"])

                if val is True:

                    done_count += 1

                    print("  ✓       ", end="")

                elif val is False:

                    print("  ✗       ", end="")

                else:

                    print("  ○       ", end="")

            else:

                print("  -       ", end="")


        pct = round(done_count / sched_count * 100) if sched_count else 0

        print(f"  {pct:>4}%")


    print("="*65)



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

# MATPLOTLIB HEATMAP

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


def plot_heatmap(data, habit=None):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib numpy")

        return


    today  = date.today()

    weeks  = 20

    start  = today - timedelta(weeks=weeks)

    start  = start - timedelta(days=start.weekday())


    # Build grid

    grid_data = []

    week      = []

    cur       = start


    while cur <= today + timedelta(days=6 - today.weekday()):

        d_str = str(cur)

        if cur.weekday() == 0 and week:

            grid_data.append(week)

            week = []


        if cur > today:

            week.append(np.nan)

        else:

            if habit:

                val = data["logs"].get(d_str, {}).get(habit["id"])

                pct = 1.0 if val is True else (0.0 if val is False else np.nan)

            else:

                scheduled = [h for h in data["habits"]

                             if h["active"] and is_scheduled(h, d_str)]

                if not scheduled:

                    pct = np.nan

                else:

                    done = sum(1 for h in scheduled

                               if data["logs"].get(d_str, {}).get(h["id"]) is True)

                    pct  = done / len(scheduled)

            week.append(pct)

        cur += timedelta(days=1)


    if week:

        while len(week) < 7:

            week.append(np.nan)

        grid_data.append(week)


    matrix = np.array(grid_data).T   # shape: (7 days, N weeks)


    fig, ax = plt.subplots(figsize=(max(12, weeks // 2), 4))

    cmap    = plt.cm.Greens

    cmap.set_bad(color="#f0f0f0")


    im = ax.imshow(matrix, cmap=cmap, vmin=0, vmax=1,

                   aspect="auto", interpolation="nearest")


    ax.set_yticks(range(7))

    ax.set_yticklabels(DAYS_SHORT, fontsize=9)

    ax.set_xticks([])


    title = f"Habit Heatmap: {habit['name']}" if habit else "All Habits Combined"

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


    plt.colorbar(im, ax=ax, label="Completion rate",

                 fraction=0.03, pad=0.04)

    plt.tight_layout()

    fname = "habit_heatmap.png"

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

    print(f"\n  Heatmap saved: {fname}")

    plt.show()



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

# EDIT / ARCHIVE HABIT

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


def edit_habit(data, habit):

    print(f"\n  Editing: {habit['icon']} {habit['name']}")

    print("  (Press Enter to keep current value)\n")


    new_name = input(f"  Name [{habit['name']}]: ").strip()

    if new_name:

        habit["name"] = new_name


    new_desc = input(f"  Description [{habit.get('description','')}]: ").strip()

    if new_desc:

        habit["description"] = new_desc


    new_icon = input(f"  Icon [{habit['icon']}]: ").strip()

    if new_icon:

        habit["icon"] = new_icon


    new_goal = input(f"  Goal [{habit.get('goal','')}]: ").strip()

    if new_goal:

        habit["goal"] = new_goal


    for i, h in enumerate(data["habits"]):

        if h["id"] == habit["id"]:

            data["habits"][i] = habit

            break


    save_data(data)

    print(f"  Updated: {habit['icon']} {habit['name']}")



def archive_habit(data, habit):

    confirm = input(f"\n  Archive '{habit['name']}'? (yes/no): ").strip().lower()

    if confirm == "yes":

        for h in data["habits"]:

            if h["id"] == habit["id"]:

                h["active"] = False

                break

        save_data(data)

        print(f"  Archived: {habit['name']}")

    else:

        print("  Cancelled.")



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

# SELECT HABIT

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


def select_habit(data):

    habits = [h for h in data["habits"] if h["active"]]

    if not habits:

        print("  No active habits.")

        return None


    print()

    for i, h in enumerate(habits, 1):

        streak, _ = calculate_streak(h["id"], data["logs"])

        print(f"  [{i}] {h['icon']} {h['name']}  (streak: {streak}d)")


    try:

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

        if 0 <= idx < len(habits):

            return habits[idx]

    except ValueError:

        pass

    return None



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

# MAIN MENU

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


def print_menu(data):

    habits  = [h for h in data["habits"] if h["active"]]

    today   = today_str()

    done    = sum(

        1 for h in habits

        if data["logs"].get(today, {}).get(h["id"]) is True

    )

    due = sum(1 for h in habits if is_scheduled(h, today))


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

    print(f"  HABIT TRACKER  [{len(habits)} habits | "

          f"Today: {done}/{due}]")

    print("-"*50)

    print("  1.  Daily check-in (today)")

    print("  2.  View all habits & stats")

    print("  3.  Habit detail")

    print("  4.  Add new habit")

    print("  5.  Edit habit")

    print("  6.  Archive habit")

    print("  7.  Weekly report")

    print("  8.  ASCII heatmap (terminal)")

    print("  9.  Plot heatmap chart (matplotlib)")

    print("  0.  Exit")

    print("-"*50)



def main():

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

    print("     HABIT TRACKER")

    print("="*55)

    print("\n  Build habits, track streaks, visualize progress.")


    data = load_data()


    if not data["habits"]:

        print("\n  No habits yet! Let's add your first habit.")

        add_habit(data)

    else:

        h_count = len([h for h in data["habits"] if h["active"]])

        print(f"\n  Loaded {h_count} active habit(s).")


    while True:

        print_menu(data)

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


        if choice == "1":

            daily_checkin(data)


        elif choice == "2":

            view_habits(data)


        elif choice == "3":

            habit = select_habit(data)

            if habit:

                habit_detail(data, habit)


        elif choice == "4":

            add_habit(data)


        elif choice == "5":

            habit = select_habit(data)

            if habit:

                edit_habit(data, habit)


        elif choice == "6":

            habit = select_habit(data)

            if habit:

                archive_habit(data, habit)


        elif choice == "7":

            weekly_report(data)


        elif choice == "8":

            print("\n  Show heatmap for:")

            print("  1. All habits combined")

            print("  2. Specific habit")

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

            if sub == "2":

                habit = select_habit(data)

                ascii_heatmap(data, habit)

            else:

                ascii_heatmap(data)


        elif choice == "9":

            print("\n  Plot heatmap for:")

            print("  1. All habits combined")

            print("  2. Specific habit")

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

            if sub == "2":

                habit = select_habit(data)

                plot_heatmap(data, habit)

            else:

                plot_heatmap(data)


        elif choice == "0":

            print("\n  Keep up the great habits! Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

Random Quote Generator

import json

import random

import os

from datetime import datetime

from pathlib import Path

from collections import defaultdict


try:

    import requests

    REQUESTS_OK = True

except ImportError:

    REQUESTS_OK = False


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

# CONFIGURATION

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


QUOTES_FILE    = "quotes.json"

FAVORITES_FILE = "favorite_quotes.json"

HISTORY_FILE   = "quote_history.json"


# Free API — no key needed

QUOTE_API_URL  = "https://api.quotable.io/random"

QUOTES_BY_TAG  = "https://api.quotable.io/quotes?tags={tag}&limit=20"


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

# BUILT-IN QUOTE DATABASE (works offline)

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


BUILTIN_QUOTES = [

    # Motivation

    {"text": "The only way to do great work is to love what you do.", "author": "Steve Jobs", "category": "motivation"},

    {"text": "It does not matter how slowly you go as long as you do not stop.", "author": "Confucius", "category": "motivation"},

    {"text": "Success is not final, failure is not fatal: it is the courage to continue that counts.", "author": "Winston Churchill", "category": "motivation"},

    {"text": "Believe you can and you're halfway there.", "author": "Theodore Roosevelt", "category": "motivation"},

    {"text": "The secret of getting ahead is getting started.", "author": "Mark Twain", "category": "motivation"},

    {"text": "Don't watch the clock; do what it does. Keep going.", "author": "Sam Levenson", "category": "motivation"},

    {"text": "You are never too old to set another goal or to dream a new dream.", "author": "C.S. Lewis", "category": "motivation"},

    {"text": "The future belongs to those who believe in the beauty of their dreams.", "author": "Eleanor Roosevelt", "category": "motivation"},


    # Wisdom

    {"text": "In the middle of every difficulty lies opportunity.", "author": "Albert Einstein", "category": "wisdom"},

    {"text": "Life is what happens when you're busy making other plans.", "author": "John Lennon", "category": "wisdom"},

    {"text": "The only true wisdom is in knowing you know nothing.", "author": "Socrates", "category": "wisdom"},

    {"text": "Yesterday is history, tomorrow is a mystery, today is a gift of God.", "author": "Bill Keane", "category": "wisdom"},

    {"text": "We do not remember days, we remember moments.", "author": "Cesare Pavese", "category": "wisdom"},

    {"text": "The unexamined life is not worth living.", "author": "Socrates", "category": "wisdom"},

    {"text": "He who knows others is wise; he who knows himself is enlightened.", "author": "Lao Tzu", "category": "wisdom"},

    {"text": "The journey of a thousand miles begins with one step.", "author": "Lao Tzu", "category": "wisdom"},


    # Success

    {"text": "Success usually comes to those who are too busy to be looking for it.", "author": "Henry David Thoreau", "category": "success"},

    {"text": "I find that the harder I work, the more luck I seem to have.", "author": "Thomas Jefferson", "category": "success"},

    {"text": "Don't be afraid to give up the good to go for the great.", "author": "John D. Rockefeller", "category": "success"},

    {"text": "I have not failed. I've just found 10,000 ways that won't work.", "author": "Thomas Edison", "category": "success"},

    {"text": "The road to success and the road to failure are almost exactly the same.", "author": "Colin R. Davis", "category": "success"},

    {"text": "Success is walking from failure to failure with no loss of enthusiasm.", "author": "Winston Churchill", "category": "success"},


    # Happiness

    {"text": "Happiness is not something ready-made. It comes from your own actions.", "author": "Dalai Lama", "category": "happiness"},

    {"text": "For every minute you are angry you lose sixty seconds of happiness.", "author": "Ralph Waldo Emerson", "category": "happiness"},

    {"text": "The most wasted of days is one without laughter.", "author": "E.E. Cummings", "category": "happiness"},

    {"text": "Count your age by friends, not years. Count your life by smiles, not tears.", "author": "John Lennon", "category": "happiness"},

    {"text": "Happiness depends upon ourselves.", "author": "Aristotle", "category": "happiness"},


    # Technology

    {"text": "Any sufficiently advanced technology is indistinguishable from magic.", "author": "Arthur C. Clarke", "category": "technology"},

    {"text": "It's not a faith in technology. It's faith in people.", "author": "Steve Jobs", "category": "technology"},

    {"text": "The science of today is the technology of tomorrow.", "author": "Edward Teller", "category": "technology"},

    {"text": "Technology is best when it brings people together.", "author": "Matt Mullenweg", "category": "technology"},

    {"text": "The real danger is not that computers will begin to think like men, but that men will begin to think like computers.", "author": "Sydney J. Harris", "category": "technology"},


    # Programming

    {"text": "First, solve the problem. Then, write the code.", "author": "John Johnson", "category": "programming"},

    {"text": "Programs must be written for people to read, and only incidentally for machines to execute.", "author": "Harold Abelson", "category": "programming"},

    {"text": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", "author": "Martin Fowler", "category": "programming"},

    {"text": "The most important property of a program is whether it accomplishes the intention of its user.", "author": "C.A.R. Hoare", "category": "programming"},

    {"text": "Debugging is twice as hard as writing the code in the first place.", "author": "Brian Kernighan", "category": "programming"},

    {"text": "Talk is cheap. Show me the code.", "author": "Linus Torvalds", "category": "programming"},

    {"text": "Clean code always looks like it was written by someone who cares.", "author": "Robert C. Martin", "category": "programming"},

    {"text": "Simplicity is the soul of efficiency.", "author": "Austin Freeman", "category": "programming"},


    # Life

    {"text": "Life is short, and it's up to you to make it sweet.", "author": "Sarah Louise Delany", "category": "life"},

    {"text": "In three words I can sum up everything I've learned about life: it goes on.", "author": "Robert Frost", "category": "life"},

    {"text": "To live is the rarest thing in the world. Most people exist, that is all.", "author": "Oscar Wilde", "category": "life"},

    {"text": "Life is really simple, but we insist on making it complicated.", "author": "Confucius", "category": "life"},

    {"text": "The purpose of life is not to be happy but to be useful.", "author": "Ralph Waldo Emerson", "category": "life"},


    # Courage

    {"text": "You gain strength, courage, and confidence by every experience in which you really stop to look fear in the face.", "author": "Eleanor Roosevelt", "category": "courage"},

    {"text": "Courage is not the absence of fear, but the triumph over it.", "author": "Nelson Mandela", "category": "courage"},

    {"text": "It takes courage to grow up and become who you really are.", "author": "E.E. Cummings", "category": "courage"},

    {"text": "Fortune favors the brave.", "author": "Virgil", "category": "courage"},


    # Learning

    {"text": "Live as if you were to die tomorrow. Learn as if you were to live forever.", "author": "Mahatma Gandhi", "category": "learning"},

    {"text": "An investment in knowledge pays the best interest.", "author": "Benjamin Franklin", "category": "learning"},

    {"text": "Education is the most powerful weapon which you can use to change the world.", "author": "Nelson Mandela", "category": "learning"},

    {"text": "The beautiful thing about learning is that no one can take it away from you.", "author": "B.B. King", "category": "learning"},

    {"text": "Tell me and I forget. Teach me and I remember. Involve me and I learn.", "author": "Benjamin Franklin", "category": "learning"},

]


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

# LOAD & SAVE

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


def load_quotes():

    if Path(QUOTES_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    # First run — save built-in quotes

    save_quotes(BUILTIN_QUOTES)

    return BUILTIN_QUOTES



def save_quotes(quotes):

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

        json.dump(quotes, f, indent=2, ensure_ascii=False)



def load_favorites():

    if Path(FAVORITES_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return []



def save_favorites(favs):

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

        json.dump(favs, f, indent=2, ensure_ascii=False)



def load_history():

    if Path(HISTORY_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return []



def save_to_history(quote):

    history = load_history()

    history.append({

        "text":     quote["text"],

        "author":   quote["author"],

        "category": quote.get("category", ""),

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

    })

    history = history[-100:]

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

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



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

# DISPLAY QUOTE

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


def display_quote(quote, show_box=True):

    text     = quote.get("text", "")

    author   = quote.get("author", "Unknown")

    category = quote.get("category", "")


    if show_box:

        width = min(max(len(text) + 6, 50), 65)

        print("\n" + "─" * width)


        # Word-wrap text

        words   = text.split()

        lines   = []

        current = ""

        for word in words:

            if len(current) + len(word) + 1 <= width - 6:

                current = current + " " + word if current else word

            else:

                lines.append(current)

                current = word

        if current:

            lines.append(current)


        for line in lines:

            print(f"  {line}")


        print(f"\n  — {author}")

        if category:

            print(f"  [{category}]")

        print("─" * width)

    else:

        print(f"\n  \"{text}\"")

        print(f"  — {author}")

        if category:

            print(f"  [{category}]")



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

# FETCH FROM API

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


def fetch_online_quote(tag=None):

    if not REQUESTS_OK:

        return None

    try:

        url  = QUOTES_BY_TAG.format(tag=tag) if tag else QUOTE_API_URL

        resp = requests.get(url, timeout=8)

        resp.raise_for_status()

        data = resp.json()


        # quotable.io returns either a single object or {results: [...]}

        if "results" in data:

            items = data["results"]

            if not items:

                return None

            item = random.choice(items)

        else:

            item = data


        return {

            "text":     item.get("content", ""),

            "author":   item.get("author", "Unknown"),

            "category": tag or "online",

        }

    except Exception as e:

        return None



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

# GET RANDOM QUOTE

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


def get_random_quote(quotes, category=None):

    pool = quotes

    if category:

        pool = [q for q in quotes if q.get("category", "").lower() == category.lower()]

        if not pool:

            print(f"  No quotes in category '{category}'. Showing random.")

            pool = quotes

    return random.choice(pool) if pool else None



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

# QUOTE OF THE DAY

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


def quote_of_the_day(quotes):

    """Deterministic quote based on today's date — same quote all day."""

    today_num = int(datetime.now().strftime("%Y%j"))

    idx       = today_num % len(quotes)

    quote     = quotes[idx]


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

    print(f"  QUOTE OF THE DAY  —  {datetime.now().strftime('%d %B %Y')}")

    print("="*55)

    display_quote(quote)



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

# ADD CUSTOM QUOTE

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


def add_quote(quotes):

    print("\n  ADD YOUR OWN QUOTE")

    print("  " + "-"*40)


    text   = input("  Quote text  : ").strip()

    author = input("  Author      : ").strip() or "Unknown"


    # Show categories

    cats = sorted(set(q.get("category", "") for q in quotes if q.get("category")))

    print(f"  Categories  : {', '.join(cats)}")

    category = input("  Category    : ").strip().lower() or "general"


    if not text:

        print("  Quote text is required.")

        return quotes


    quote = {

        "text":     text,

        "author":   author,

        "category": category,

        "custom":   True,

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

    }

    quotes.append(quote)

    save_quotes(quotes)

    print(f"\n  Quote added! Total quotes: {len(quotes)}")

    return quotes



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

# SEARCH QUOTES

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


def search_quotes(quotes, keyword):

    keyword = keyword.lower()

    results = [

        q for q in quotes

        if keyword in q["text"].lower()

        or keyword in q["author"].lower()

        or keyword in q.get("category", "").lower()

    ]


    if not results:

        print(f"\n  No quotes found for: '{keyword}'")

        return


    print(f"\n  Found {len(results)} quote(s) for '{keyword}':\n")

    for i, q in enumerate(results[:10], 1):

        print(f"  [{i}] \"{q['text'][:70]}{'...' if len(q['text']) > 70 else ''}\"")

        print(f"       — {q['author']}  [{q.get('category', '')}]\n")


    if len(results) > 10:

        print(f"  ... and {len(results) - 10} more.")



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

# FAVORITES

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


def add_to_favorites(quote):

    favs = load_favorites()

    if any(f["text"] == quote["text"] for f in favs):

        print("  Already in favorites.")

        return

    favs.append({

        "text":     quote["text"],

        "author":   quote["author"],

        "category": quote.get("category", ""),

        "saved_at": datetime.now().strftime("%d-%m-%Y %H:%M"),

    })

    save_favorites(favs)

    print("  Added to favorites!")



def view_favorites():

    favs = load_favorites()

    if not favs:

        print("\n  No favorites yet.")

        return


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

    print(f"  FAVORITE QUOTES  ({len(favs)})")

    print("="*55)

    for i, q in enumerate(favs, 1):

        print(f"\n  [{i}]")

        display_quote(q, show_box=False)


    print("="*55)



def remove_from_favorites():

    favs = load_favorites()

    if not favs:

        print("\n  No favorites to remove.")

        return


    view_favorites()

    try:

        idx = int(input("\n  Enter number to remove: ").strip()) - 1

        if 0 <= idx < len(favs):

            removed = favs.pop(idx)

            save_favorites(favs)

            print(f"  Removed: \"{removed['text'][:50]}...\"")

        else:

            print("  Invalid number.")

    except ValueError:

        print("  Invalid input.")



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

# CATEGORY STATS

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


def show_stats(quotes):

    total      = len(quotes)

    categories = defaultdict(int)

    authors    = defaultdict(int)

    custom     = sum(1 for q in quotes if q.get("custom"))


    for q in quotes:

        categories[q.get("category", "uncategorized")] += 1

        authors[q.get("author", "Unknown")] += 1


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

    print(f"  QUOTE LIBRARY STATS")

    print("="*50)

    print(f"  Total quotes   : {total}")

    print(f"  Custom quotes  : {custom}")

    print(f"  Favorites      : {len(load_favorites())}")

    print(f"  Categories     : {len(categories)}")

    print(f"  Authors        : {len(authors)}")


    print(f"\n  BY CATEGORY:")

    max_count = max(categories.values()) if categories else 1

    for cat, count in sorted(categories.items(), key=lambda x: -x[1]):

        bar = "█" * int((count / max_count) * 20)

        print(f"  {cat:<18} {count:>4}  {bar}")


    print(f"\n  TOP 5 AUTHORS:")

    for author, count in sorted(authors.items(), key=lambda x: -x[1])[:5]:

        print(f"  {author:<30} {count} quote(s)")


    print("="*50)



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

# QUOTE SLIDESHOW

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


def slideshow(quotes, category=None, count=5):

    """Display N quotes one by one."""

    pool = quotes

    if category:

        pool = [q for q in quotes

                if q.get("category", "").lower() == category.lower()]

        if not pool:

            pool = quotes


    sample = random.sample(pool, min(count, len(pool)))


    print(f"\n  SLIDESHOW  —  {len(sample)} quotes")

    print("  Press Enter for next, 'f' to favorite, 'q' to quit\n")


    for i, quote in enumerate(sample, 1):

        print(f"\n  Quote {i} of {len(sample)}")

        display_quote(quote)

        save_to_history(quote)


        action = input("\n  [Enter=next | f=favorite | q=quit]: ").strip().lower()

        if action == "f":

            add_to_favorites(quote)

        elif action == "q":

            break



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

# VIEW HISTORY

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


def view_history():

    history = load_history()

    if not history:

        print("\n  No quote history yet.")

        return


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

    print(f"  RECENTLY VIEWED QUOTES  (last {min(len(history), 10)})")

    print("="*60)


    for h in reversed(history[-10:]):

        print(f"\n  {h['shown_at']}  [{h.get('category', '')}]")

        print(f"  \"{h['text'][:70]}{'...' if len(h['text']) > 70 else ''}\"")

        print(f"  — {h['author']}")


    print("="*60)



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

# MAIN MENU

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


def print_menu(quotes):

    cats  = sorted(set(q.get("category", "") for q in quotes if q.get("category")))

    total = len(quotes)

    favs  = len(load_favorites())

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

    print(f"  RANDOM QUOTE GENERATOR  [{total} quotes | ★{favs} favs]")

    print("-"*50)

    print("  1.  Random quote")

    print("  2.  Quote by category")

    print("  3.  Quote of the day")

    print("  4.  Quote slideshow")

    print("  5.  Fetch online quote (API)")

    print("  6.  Search quotes")

    print("  7.  Add your own quote")

    print("  8.  View favorites")

    print("  9.  Remove from favorites")

    print("  10. View history")

    print("  11. Library stats")

    print("  0.  Exit")

    print(f"\n  Categories: {', '.join(cats)}")

    print("-"*50)



def main():

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

    print("     RANDOM QUOTE GENERATOR")

    print("="*55)


    quotes = load_quotes()

    print(f"\n  Loaded {len(quotes)} quotes from library.")

    print("  Works offline with built-in quotes.")

    if REQUESTS_OK:

        print("  Online API enabled (quotable.io).")


    while True:

        print_menu(quotes)

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


        if choice == "1":

            quote = get_random_quote(quotes)

            if quote:

                display_quote(quote)

                save_to_history(quote)

                fav = input("\n  Add to favorites? (y/n): ").strip().lower()

                if fav == "y":

                    add_to_favorites(quote)


        elif choice == "2":

            cats = sorted(set(q.get("category", "") for q in quotes if q.get("category")))

            print(f"\n  Available: {', '.join(cats)}")

            cat = input("  Category: ").strip()

            quote = get_random_quote(quotes, cat)

            if quote:

                display_quote(quote)

                save_to_history(quote)

                fav = input("\n  Add to favorites? (y/n): ").strip().lower()

                if fav == "y":

                    add_to_favorites(quote)


        elif choice == "3":

            quote_of_the_day(quotes)


        elif choice == "4":

            cats = sorted(set(q.get("category", "") for q in quotes if q.get("category")))

            print(f"\n  Categories: {', '.join(cats)}")

            cat = input("  Category (Enter=all): ").strip() or None

            try:

                n = int(input("  Number of quotes (default 5): ").strip() or 5)

            except ValueError:

                n = 5

            slideshow(quotes, cat, n)


        elif choice == "5":

            if not REQUESTS_OK:

                print("\n  requests not installed: pip install requests")

            else:

                print("\n  Popular tags: inspirational, life, humor, love, technology")

                tag   = input("  Tag (Enter=random): ").strip() or None

                print("  Fetching...")

                quote = fetch_online_quote(tag)

                if quote:

                    display_quote(quote)

                    save_to_history(quote)

                    # Offer to save to local library

                    save = input("\n  Save to local library? (y/n): ").strip().lower()

                    if save == "y":

                        quotes.append(quote)

                        save_quotes(quotes)

                        print("  Saved!")

                    fav = input("  Add to favorites? (y/n): ").strip().lower()

                    if fav == "y":

                        add_to_favorites(quote)

                else:

                    print("  Could not fetch online quote. Check internet connection.")


        elif choice == "6":

            kw = input("\n  Search keyword: ").strip()

            if kw:

                search_quotes(quotes, kw)


        elif choice == "7":

            quotes = add_quote(quotes)


        elif choice == "8":

            view_favorites()


        elif choice == "9":

            remove_from_favorites()


        elif choice == "10":

            view_history()


        elif choice == "11":

            show_stats(quotes)


        elif choice == "0":

            print("\n  Goodbye! Stay inspired!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

Unit Converter

 


import os

import json

from datetime import datetime

from pathlib import Path

from collections import defaultdict


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

# CONVERSION DATA

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


CATEGORIES = {


    # ── LENGTH ──────────────────────────────────────────────

    "length": {

        "base":  "meter",

        "units": {

            "meter":      1,

            "kilometer":  1000,

            "centimeter": 0.01,

            "millimeter": 0.001,

            "micrometer": 1e-6,

            "nanometer":  1e-9,

            "mile":       1609.344,

            "yard":       0.9144,

            "foot":       0.3048,

            "inch":       0.0254,

            "nautical mile": 1852,

            "light year": 9.461e15,

        }

    },


    # ── WEIGHT / MASS ────────────────────────────────────────

    "weight": {

        "base":  "kilogram",

        "units": {

            "kilogram":     1,

            "gram":         0.001,

            "milligram":    1e-6,

            "microgram":    1e-9,

            "metric ton":   1000,

            "pound":        0.453592,

            "ounce":        0.0283495,

            "stone":        6.35029,

            "quintal":      100,

            "carat":        0.0002,

        }

    },


    # ── TEMPERATURE ──────────────────────────────────────────

    "temperature": {

        "base":  "celsius",

        "units": {

            "celsius":    None,   # special handling

            "fahrenheit": None,

            "kelvin":     None,

            "rankine":    None,

        }

    },


    # ── VOLUME ───────────────────────────────────────────────

    "volume": {

        "base":  "liter",

        "units": {

            "liter":          1,

            "milliliter":     0.001,

            "cubic meter":    1000,

            "cubic centimeter": 0.001,

            "cubic inch":     0.0163871,

            "cubic foot":     28.3168,

            "gallon (US)":    3.78541,

            "gallon (UK)":    4.54609,

            "quart (US)":     0.946353,

            "pint (US)":      0.473176,

            "cup (US)":       0.236588,

            "fluid ounce":    0.0295735,

            "tablespoon":     0.0147868,

            "teaspoon":       0.00492892,

        }

    },


    # ── SPEED ────────────────────────────────────────────────

    "speed": {

        "base":  "meter/second",

        "units": {

            "meter/second":    1,

            "kilometer/hour":  0.277778,

            "mile/hour":       0.44704,

            "foot/second":     0.3048,

            "knot":            0.514444,

            "mach":            340.29,

            "light speed":     299792458,

        }

    },


    # ── AREA ─────────────────────────────────────────────────

    "area": {

        "base":  "square meter",

        "units": {

            "square meter":      1,

            "square kilometer":  1e6,

            "square centimeter": 1e-4,

            "square millimeter": 1e-6,

            "square mile":       2589988.11,

            "square yard":       0.836127,

            "square foot":       0.092903,

            "square inch":       6.4516e-4,

            "hectare":           10000,

            "acre":              4046.86,

            "cent":              40.4686,    # Indian unit

            "bigha":             2508.38,    # common Indian unit

        }

    },


    # ── TIME ─────────────────────────────────────────────────

    "time": {

        "base":  "second",

        "units": {

            "second":       1,

            "millisecond":  0.001,

            "microsecond":  1e-6,

            "nanosecond":   1e-9,

            "minute":       60,

            "hour":         3600,

            "day":          86400,

            "week":         604800,

            "month":        2628000,   # avg 30.44 days

            "year":         31536000,

            "decade":       315360000,

            "century":      3153600000,

        }

    },


    # ── DATA / STORAGE ───────────────────────────────────────

    "data": {

        "base":  "byte",

        "units": {

            "bit":       0.125,

            "byte":      1,

            "kilobyte":  1024,

            "megabyte":  1024**2,

            "gigabyte":  1024**3,

            "terabyte":  1024**4,

            "petabyte":  1024**5,

            "kibibyte":  1024,

            "mebibyte":  1024**2,

            "gibibyte":  1024**3,

        }

    },


    # ── PRESSURE ─────────────────────────────────────────────

    "pressure": {

        "base":  "pascal",

        "units": {

            "pascal":      1,

            "kilopascal":  1000,

            "megapascal":  1e6,

            "bar":         100000,

            "millibar":    100,

            "atmosphere":  101325,

            "psi":         6894.76,

            "mmHg":        133.322,

            "torr":        133.322,

            "inHg":        3386.39,

        }

    },


    # ── ENERGY ───────────────────────────────────────────────

    "energy": {

        "base":  "joule",

        "units": {

            "joule":          1,

            "kilojoule":      1000,

            "megajoule":      1e6,

            "calorie":        4.184,

            "kilocalorie":    4184,

            "watt-hour":      3600,

            "kilowatt-hour":  3.6e6,

            "electronvolt":   1.602e-19,

            "BTU":            1055.06,

            "therm":          105480400,

            "foot-pound":     1.35582,

        }

    },


    # ── FUEL EFFICIENCY ──────────────────────────────────────

    "fuel": {

        "base":  "km/l",

        "units": {

            "km/l":    1,

            "l/100km": None,   # inverse — special handling

            "mpg (US)": 0.425144,

            "mpg (UK)": 0.354006,

            "miles/l":  1.60934,

        }

    },


}


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

# HISTORY FILE

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


HISTORY_FILE = "unit_converter_history.json"


def load_history():

    if Path(HISTORY_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return []



def save_history(entry):

    history = load_history()

    history.append(entry)

    history = history[-50:]

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

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



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

# CONVERSION LOGIC

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


def convert_temperature(value, from_unit, to_unit):

    """Special handling for temperature conversions."""

    # Convert to Celsius first

    if from_unit == "celsius":

        celsius = value

    elif from_unit == "fahrenheit":

        celsius = (value - 32) * 5 / 9

    elif from_unit == "kelvin":

        celsius = value - 273.15

    elif from_unit == "rankine":

        celsius = (value - 491.67) * 5 / 9

    else:

        return None


    # Convert from Celsius to target

    if to_unit == "celsius":

        return celsius

    elif to_unit == "fahrenheit":

        return celsius * 9 / 5 + 32

    elif to_unit == "kelvin":

        return celsius + 273.15

    elif to_unit == "rankine":

        return (celsius + 273.15) * 9 / 5

    return None



def convert_fuel(value, from_unit, to_unit):

    """Special handling for fuel efficiency (l/100km is inverse)."""

    # Convert to km/l first

    if from_unit == "l/100km":

        kml = 100 / value if value != 0 else 0

    else:

        factor = CATEGORIES["fuel"]["units"].get(from_unit, 1)

        kml    = value * factor


    # Convert from km/l to target

    if to_unit == "l/100km":

        return 100 / kml if kml != 0 else 0

    else:

        factor = CATEGORIES["fuel"]["units"].get(to_unit, 1)

        return kml / factor



def convert(value, from_unit, to_unit, category):

    """

    Universal conversion function.

    Returns converted value or None.

    """

    from_unit = from_unit.lower().strip()

    to_unit   = to_unit.lower().strip()


    if from_unit == to_unit:

        return value


    cat_data = CATEGORIES.get(category)

    if not cat_data:

        return None


    # Special categories

    if category == "temperature":

        return convert_temperature(value, from_unit, to_unit)

    if category == "fuel":

        return convert_fuel(value, from_unit, to_unit)


    units = cat_data["units"]


    if from_unit not in units or to_unit not in units:

        return None


    # Convert via base unit

    value_in_base = value * units[from_unit]

    result        = value_in_base / units[to_unit]

    return result



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

# FORMAT RESULT

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


def fmt_result(value):

    """Smart formatting: remove unnecessary decimals."""

    if value is None:

        return "N/A"

    if abs(value) >= 1e12 or (abs(value) < 1e-6 and value != 0):

        return f"{value:.6e}"

    if value == int(value):

        return f"{int(value):,}"

    if abs(value) >= 100:

        return f"{value:,.4f}"

    if abs(value) >= 1:

        return f"{value:.6f}".rstrip("0").rstrip(".")

    return f"{value:.10f}".rstrip("0").rstrip(".")



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

# DISPLAY UNIT LIST

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


def list_units(category):

    cat_data = CATEGORIES.get(category)

    if not cat_data:

        print(f"  Unknown category: {category}")

        return


    units = list(cat_data["units"].keys())

    print(f"\n  Units in '{category}':")

    print("  " + "-"*40)

    for i, unit in enumerate(units, 1):

        print(f"  {i:>3}. {unit}")

    print("  " + "-"*40)



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

# QUICK CONVERT (ALL TO ONE)

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


def convert_one_to_all(value, from_unit, category):

    """Show value converted to ALL units in the category."""

    cat_data = CATEGORIES.get(category)

    if not cat_data:

        return


    print(f"\n  {fmt_result(value)} {from_unit}  =")

    print("  " + "-"*45)


    for unit in cat_data["units"].keys():

        if unit == from_unit.lower():

            continue

        result = convert(value, from_unit, unit, category)

        if result is not None:

            print(f"  {fmt_result(result):<20}  {unit}")


    print("  " + "-"*45)



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

# INTERACTIVE CONVERSION

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


def do_convert(category):

    list_units(category)


    from_unit = input(f"\n  From unit: ").strip().lower()

    to_unit   = input(f"  To unit  : ").strip().lower()


    cat_data = CATEGORIES.get(category, {})

    units    = cat_data.get("units", {})


    # Fuzzy match

    def fuzzy_match(u, unit_list):

        if u in unit_list:

            return u

        matches = [k for k in unit_list if u in k or k in u]

        return matches[0] if matches else None


    from_unit = fuzzy_match(from_unit, units) or from_unit

    to_unit   = fuzzy_match(to_unit, units) or to_unit


    if from_unit not in units:

        print(f"  Unknown unit: {from_unit}")

        return

    if to_unit not in units:

        print(f"  Unknown unit: {to_unit}")

        return


    try:

        value = float(input(f"  Value     : ").strip())

    except ValueError:

        print("  Invalid value.")

        return


    result = convert(value, from_unit, to_unit, category)


    if result is None:

        print("  Conversion failed.")

        return


    print(f"\n  {'='*45}")

    print(f"  {fmt_result(value)} {from_unit}")

    print(f"  =  {fmt_result(result)} {to_unit}")

    print(f"  {'='*45}")


    # Show all option

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

    if show_all == "y":

        convert_one_to_all(value, from_unit, category)


    # Save to history

    save_history({

        "time":     datetime.now().strftime("%d-%m-%Y %H:%M"),

        "category": category,

        "value":    value,

        "from":     from_unit,

        "result":   result,

        "to":       to_unit,

    })



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

# QUICK CONVERTER (COMMON PAIRS)

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


def quick_convert():

    print("\n  QUICK CONVERSIONS")

    print("  " + "-"*40)


    quick_pairs = [

        ("1",  "km",       "mile",       "length"),

        ("2",  "kg",       "pound",      "weight"),

        ("3",  "celsius",  "fahrenheit", "temperature"),

        ("4",  "liter",    "gallon (US)","volume"),

        ("5",  "meter/second", "km/hour","speed"),

        ("6",  "hectare",  "acre",       "area"),

        ("7",  "gigabyte", "megabyte",   "data"),

        ("8",  "bar",      "psi",        "pressure"),

        ("9",  "kilocalorie","joule",    "energy"),

        ("10", "km/l",     "mpg (US)",   "fuel"),

    ]


    for num, f, t, cat in quick_pairs:

        result = convert(1, f, t, cat)

        print(f"  {num:>3}. 1 {f:<18} = {fmt_result(result)} {t}")


    print("  " + "-"*40)

    print("\n  Enter a number to use that conversion,")

    choice = input("  or press Enter to go back: ").strip()


    if not choice:

        return


    # Find the pair

    selected = None

    for num, f, t, cat in quick_pairs:

        if choice == num:

            selected = (f, t, cat)

            break


    if not selected:

        return


    from_unit, to_unit, category = selected


    try:

        value = float(input(f"\n  Enter value in {from_unit}: ").strip())

    except ValueError:

        print("  Invalid value.")

        return


    result = convert(value, from_unit, to_unit, category)

    print(f"\n  {fmt_result(value)} {from_unit}  =  {fmt_result(result)} {to_unit}")


    save_history({

        "time":     datetime.now().strftime("%d-%m-%Y %H:%M"),

        "category": category,

        "value":    value,

        "from":     from_unit,

        "result":   result,

        "to":       to_unit,

    })



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

# SHOW HISTORY

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


def show_history():

    history = load_history()

    if not history:

        print("\n  No conversion history yet.")

        return


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

    print(f"  CONVERSION HISTORY  (last {len(history)})")

    print("="*65)

    print(f"  {'TIME':<18} {'CAT':<14} {'FROM':<25} TO")

    print("  " + "-"*60)


    for h in reversed(history[-20:]):

        from_str = f"{fmt_result(h['value'])} {h['from']}"

        to_str   = f"{fmt_result(h['result'])} {h['to']}"

        print(f"  {h['time']:<18} {h['category']:<14} "

              f"{from_str:<25} {to_str}")

    print("="*65)



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

# MAIN MENU

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


def print_menu():

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

    print("  UNIT CONVERTER")

    print("-"*50)

    print("  1.  Length")

    print("  2.  Weight / Mass")

    print("  3.  Temperature")

    print("  4.  Volume")

    print("  5.  Speed")

    print("  6.  Area")

    print("  7.  Time")

    print("  8.  Data / Storage")

    print("  9.  Pressure")

    print("  10. Energy")

    print("  11. Fuel Efficiency")

    print("  12. Quick conversions (common pairs)")

    print("  13. Conversion history")

    print("  0.  Exit")

    print("-"*50)



MENU_MAP = {

    "1":  "length",

    "2":  "weight",

    "3":  "temperature",

    "4":  "volume",

    "5":  "speed",

    "6":  "area",

    "7":  "time",

    "8":  "data",

    "9":  "pressure",

    "10": "energy",

    "11": "fuel",

}



def main():

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

    print("     UNIT CONVERTER")

    print("="*55)

    print(f"\n  {len(CATEGORIES)} categories  |  "

          f"{sum(len(v['units']) for v in CATEGORIES.values())}+ units")

    print("  Length, Weight, Temperature, Volume, Speed,")

    print("  Area, Time, Data, Pressure, Energy, Fuel")


    while True:

        print_menu()

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


        if choice in MENU_MAP:

            do_convert(MENU_MAP[choice])


        elif choice == "12":

            quick_convert()


        elif choice == "13":

            show_history()


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()