Source code for duck.utils.readable

"""
Human readable formatting utilities.

Converts raw values -- byte counts, durations, timestamps, and large
numbers -- into concise, human readable strings suitable for display
in UIs, logs, and reports.

Functions:
    readable_size: Format a byte count as a storage size (e.g. "1.50 MB").
    readable_duration: Format a duration in seconds (e.g. "2 hours 5 minutes").
    readable_date: Format a datetime as relative or absolute (e.g. "3 days ago").
    readable_count: Format a large number compactly (e.g. "1.2K", "3.4M").
"""
from __future__ import annotations

import math
from datetime import datetime


# Datetime
RELATIVE_DATE_THRESHOLD_DAYS = 7  # beyond this, show absolute date
DURATION_UNITS = (
    ("year", 365 * 24 * 3600),
    ("day", 24 * 3600),
    ("hour", 3600),
    ("minute", 60),
    ("second", 1),
)

# Storage Size
BINARY_UNITS = ("B", "KB", "MB", "GB", "TB", "PB", "EB")
BINARY_BASE = 1024

# Compact Count
COMPACT_COUNT_UNITS = (("B", 1e9), ("M", 1e6), ("K", 1e3))


[docs] def readable_size(num_bytes: int, precision: int = 2) -> str: """ Convert a byte count into a human readable storage size string. Args: num_bytes: Size in bytes. Must be non-negative. precision: Number of decimal places to show. Returns: Human readable size, e.g. "1.50 MB". Raises: ValueError: If num_bytes is negative. """ if num_bytes < 0: raise ValueError("num_bytes must be non-negative") if num_bytes == 0: return "0 B" exponent = min(int(math.log(num_bytes, BINARY_BASE)), len(BINARY_UNITS) - 1) value = num_bytes / (BINARY_BASE**exponent) unit = BINARY_UNITS[exponent] # Skip decimals when the value is still in whole bytes if unit == "B": return f"{int(value)} {unit}" return f"{value:.{precision}f} {unit}"
[docs] def readable_duration(total_seconds: float, max_units: int = 2) -> str: """ Convert a duration in seconds into a human readable string. Args: total_seconds: Duration in seconds. Must be non-negative. max_units: Maximum number of time units to include, e.g. 2 gives "1 day 3 hours" instead of "1 day 3 hours 5 minutes". Returns: Human readable duration, e.g. "2 hours 5 minutes". Raises: ValueError: If total_seconds is negative. """ if total_seconds < 0: raise ValueError("total_seconds must be non-negative") if total_seconds < 1: return "0 seconds" remaining = int(total_seconds) parts: list[str] = [] for unit_name, unit_seconds in DURATION_UNITS: unit_value, remaining = divmod(remaining, unit_seconds) if unit_value: plural = "s" if unit_value != 1 else "" parts.append(f"{unit_value} {unit_name}{plural}") return " ".join(parts[:max_units]) if parts else "0 seconds"
[docs] def readable_date(dt: datetime, reference: datetime | None = None) -> str: """ Convert a datetime into a human readable relative or absolute string. Args: dt: The datetime to describe. reference: The datetime to compare against. Defaults to now. Returns: Relative description for recent dates, e.g. "3 hours ago" or "in 2 days"; falls back to an absolute "%b %d, %Y" date once the gap exceeds RELATIVE_DATE_THRESHOLD_DAYS. """ reference = reference or datetime.now(dt.tzinfo) delta_seconds = (reference - dt).total_seconds() if abs(delta_seconds) >= RELATIVE_DATE_THRESHOLD_DAYS * 24 * 3600: return dt.strftime("%b %d, %Y") is_future = delta_seconds < 0 duration_text = readable_duration(abs(delta_seconds), max_units=1) if duration_text == "0 seconds": return "just now" return f"in {duration_text}" if is_future else f"{duration_text} ago"
[docs] def readable_count(num: float, precision: int = 1) -> str: """ Convert a large number into a compact human readable string. Args: num: The number to format. precision: Number of decimal places to show for scaled values. Returns: Human readable count, e.g. "1.2K", "3.4M", "980". """ sign = "-" if num < 0 else "" num = abs(num) for unit, threshold in COMPACT_COUNT_UNITS: if num >= threshold: return f"{sign}{num / threshold:.{precision}f}{unit}" return f"{sign}{int(num)}"