From 1090d4ae50d2467bb6e532d0c7cff822eb89e39c Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Sat, 8 Mar 2025 18:40:39 +0000 Subject: [PATCH] Claude refactor --- README.rst | 262 +++++++++++++++++++++++++++++++++++------------------ ago/ago.py | 258 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 361 insertions(+), 159 deletions(-) diff --git a/README.rst b/README.rst index cd33405..6595372 100644 --- a/README.rst +++ b/README.rst @@ -1,125 +1,215 @@ -What are human readable timedeltas? +What are human readable timedeltas? =============================================== -ago.py makes customizable human readable timedeltas, for example: +The ``ago.py`` module makes customizable human readable timedeltas. For example: Testing past tense:: - Russell commented 1 year, 127 days, 16 hours ago - You replied 1 year, 127 days ago + Russell commented 1 year, 127 days, 16 hours ago + You replied 1 year, 127 days ago Testing future tense:: - Program will shutdown in 2 days, 3 hours, 27 minutes - Job will run 2 days, 3 hours from now + Program will shutdown in 2 days, 3 hours, 27 minutes + Job will run 2 days, 3 hours from now -How to install -=================== +Installation +============ There are a number of ways to install this package. You could run this ad hoc command:: - pip install ago + pip install ago or specify *ago* under the *setup_requires* list within your -*setuptools*-compatible project's *setup.py* file. +`setuptools `_-compatible project's ``setup.py`` file. -How to use -================== +How to Use +========== -The ago module comes with three functions: +The ``ago`` module comes with the following functions: -#. human -#. delta2dict -#. get_delta_from_subject +1. ``human``: Convert a datetime or timedelta to a human-readable string +2. ``delta2dict``: Convert a timedelta to a dictionary of units +3. ``extract_components``: Extract time components from a timedelta (builds on delta2dict) +4. ``format_components``: Format time components into a readable string +5. ``get_delta_from_subject``: Convert various input types to a timedelta -You really only need to worry about *human*. +Basic Usage +----------- -Here are all the available arguments and defaults:: +The primary function you'll use is ``human``: - human(subject, precision=2, past_tense='{} ago', future_tense='in {}', abbreviate=False): +.. code-block:: python -subject - a datetime, timedelta, or timestamp (integer/float) object to become human readable + from ago import human + from datetime import datetime, timedelta -precision (default 2): - the desired amount of unit precision + # With a datetime object + db_date = datetime(year=2010, month=5, day=4, hour=6, minute=54, second=33) + print('Created ' + human(db_date)) # "Created X years, Y months ago" -past_tense (default '{} ago'): - the format string used for a past timedelta - -future_tense (default 'in {}'): - the format string used for a future timedelta - -abbreviate (default False): - boolean to abbreviate units - -Here is an example on how to use *human*:: - - from ago import human - from ago import delta2dict - - from datetime import datetime - from datetime import timedelta - - # pretend this was stored in database - db_date = datetime(year=2010, month=5, day=4, hour=6, minute=54, second=33, microsecond=4000) - - # to find out how long ago, use the human function - print 'Created ' + human( db_date ) - - # optionally pass a precision - print 'Created ' + human( db_date, 3 ) - print 'Created ' + human( db_date, 6 ) - -We also support future dates and times:: - - PRESENT = datetime.now() - PAST = PRESENT - timedelta( 492, 58711, 45 ) # days, secs, ms - FUTURE = PRESENT + timedelta( 2, 12447, 963 ) # days, secs, ms - - print human( FUTURE ) - -Example past_tense and future_tense keyword arguments:: - - output1 = human( PAST, - past_tense = 'titanic sunk {0} ago', - future_tense = 'titanic will sink in {0} from now' - ) - - output2 = human( FUTURE, - past_tense = 'titanic sunk {0} ago', - future_tense = 'titanic will sink in {0} from now' - ) - - print output1 - # titanic sunk 1 year, 127 days ago - print output2 - # titanic will sink in 2 days, 3 hours from now + # With a timedelta object + delta = timedelta(days=5, hours=3, minutes=45) + print('Due in ' + human(delta)) # "Due in 5 days, 3 hours" -Need more examples? -========================== +Function Arguments +------------------ -You should look at test_ago.py +The ``human`` function accepts the following arguments: + +.. code-block:: python + + human(subject, precision=2, past_tense='{} ago', future_tense='in {}', abbreviate=False) + +**subject** + A datetime, timedelta, or timestamp (integer/float) object to be converted to a human-readable string. + +**precision** (default: 2) + The desired amount of unit precision. + +**past_tense** (default: ``'{} ago'``) + The format string used for a past timedelta. + +**future_tense** (default: ``'in {}'``) + The format string used for a future timedelta. + +**abbreviate** (default: False) + Boolean flag to abbreviate units. -How do I thank you? -========================== +Examples +-------- -You should follow me on twitter http://twitter.com/russellbal +Basic usage with different precisions: +.. code-block:: python + + from ago import human + from datetime import datetime + + # Pretend this was stored in a database + db_date = datetime(year=2010, month=5, day=4, hour=6, minute=54, second=33) + + # To find out how long ago, use the human function + print('Created ' + human(db_date)) # "Created X years, Y months ago" + + # Optionally pass a precision + print('Created ' + human(db_date, 3)) # Shows 3 units (e.g., years, months, days) + print('Created ' + human(db_date, 6)) # Shows up to 6 units + +Future dates and times: + +.. code-block:: python + + from ago import human + from datetime import datetime, timedelta + + PRESENT = datetime.now() + FUTURE = PRESENT + timedelta(days=2, seconds=12447, microseconds=963) + + print(human(FUTURE)) # "in 2 days, 3 hours" + +Custom format strings: + +.. code-block:: python + + from ago import human + from datetime import datetime, timedelta + + PRESENT = datetime.now() + PAST = PRESENT - timedelta(days=492, seconds=58711, microseconds=45) + FUTURE = PRESENT + timedelta(days=2, seconds=12447, microseconds=963) + + output1 = human( + PAST, + past_tense='titanic sunk {} ago', + future_tense='titanic will sink in {} from now' + ) + # "titanic sunk 1 year, 127 days ago" + + output2 = human( + FUTURE, + past_tense='titanic sunk {} ago', + future_tense='titanic will sink in {} from now' + ) + # "titanic will sink in 2 days, 3 hours from now" + +Using abbreviations: + +.. code-block:: python + + from ago import human + from datetime import timedelta + + print(human(timedelta(days=5, hours=3, minutes=45), abbreviate=True)) + # "5d, 3h ago" + + +Advanced Usage +-------------- + +For more advanced use cases, you can utilize the other functions. + +Getting a dictionary of time units: + +.. code-block:: python + + from ago import delta2dict + from datetime import timedelta + + delta = timedelta(days=400, hours=5, minutes=30) + time_dict = delta2dict(delta) + # Returns {"year": 1, "day": 35, "hour": 5, "minute": 30, "second": 0, ...} + +Extracting non-zero time components: + +.. code-block:: python + + from ago import extract_components + from datetime import timedelta + + delta = timedelta(days=400, hours=5, minutes=30) + components = extract_components(delta) + # Returns a list of components: + # [{"unit": "year", "abbr": "y", "value": 1}, + # {"unit": "day", "abbr": "d", "value": 35}, ...] + +Formatting time components: + +.. code-block:: python + + from ago import extract_components, format_components + from datetime import timedelta + + delta = timedelta(days=400, hours=5, minutes=30) + components = extract_components(delta) + formatted = format_components(components, precision=3, abbreviate=True) + # "1y, 35d, 5h" + + +More Examples +------------- + +For additional examples, please refer to the file ``test_ago.py``. + +Acknowledgements +---------------- + +**How do I thank you?** + +Follow me on Twitter: `@russellbal `_. License -========================= +------- -* Public Domain +This project is in the Public Domain. +Revision Control +---------------- -Public Revision Control -============================== - -* https://git.unturf.com/python/ago +The public revision control repository is available at: `https://git.unturf.com/python/ago `_. diff --git a/ago/ago.py b/ago/ago.py index f81a04e..4af5c51 100644 --- a/ago/ago.py +++ b/ago/ago.py @@ -1,82 +1,194 @@ from datetime import datetime, timedelta +from typing import Dict, Union, List, Tuple, Any, Callable -units = ("year", "day", "hour", "minute", "second", "millisecond", "microsecond") - - -def get_delta_from_subject(subject): - """Convert the subject to a timedelta and return it.""" - if isinstance(subject, timedelta): - return subject - - if isinstance(subject, datetime): - dt = subject - else: - # if it's not a datetime, assume it's a unix timestamp - try: - subject = float(subject) - except ValueError: - # some unknown type that couldn't be converted to a float - raise TypeError("Unsupported subject type") - - dt = datetime.fromtimestamp(subject) - - # return a timedelta from the datetime, using its timezone (if it has one) - return datetime.now(tz=dt.tzinfo) - dt - - -def delta2dict(delta): - """Accepts a delta, returns a dictionary of units""" - delta = abs(delta) - return { - "year": int(delta.days / 365), - "day": int(delta.days % 365), - "hour": int(delta.seconds / 3600), - "minute": int(delta.seconds / 60) % 60, - "second": int(delta.seconds % 60), - "millisecond": int(delta.microseconds / 1000), - "microsecond": int(delta.microseconds % 1000), +# Define time units as a data structure +# Each unit contains: +# - name: full name of the unit (e.g., "year") +# - abbr: abbreviation (e.g., "y") +# - seconds: number of seconds in this unit (for reference) +# - extract: function to extract this unit's value from a timedelta +TIME_UNITS = [ + { + "name": "year", + "abbr": "y", + "seconds": 31536000, + "extract": lambda td: int(td.days / 365) + }, + { + "name": "day", + "abbr": "d", + "seconds": 86400, + "extract": lambda td: int(td.days % 365) + }, + { + "name": "hour", + "abbr": "h", + "seconds": 3600, + "extract": lambda td: int(td.seconds / 3600) + }, + { + "name": "minute", + "abbr": "m", + "seconds": 60, + "extract": lambda td: int(td.seconds / 60) % 60 + }, + { + "name": "second", + "abbr": "s", + "seconds": 1, + "extract": lambda td: int(td.seconds % 60) + }, + { + "name": "millisecond", + "abbr": "ms", + "seconds": 0.001, + "extract": lambda td: int(td.microseconds / 1000) + }, + { + "name": "microsecond", + "abbr": "μs", + "seconds": 0.000001, + "extract": lambda td: int(td.microseconds % 1000) } +] +def get_delta_from_subject(subject: Union[datetime, timedelta, int, float]) -> Tuple[timedelta, bool]: + """ + Convert various input types to a timedelta and determine if it's in the past. + + Args: + subject: A datetime, timedelta, or timestamp (int/float) + + Returns: + tuple: (timedelta object, is_past boolean) + """ + if isinstance(subject, timedelta): + return subject, subject >= timedelta(0) + + if isinstance(subject, datetime): + delta = datetime.now(tz=subject.tzinfo) - subject + return delta, delta >= timedelta(0) + + # Assume it's a timestamp + try: + dt = datetime.fromtimestamp(float(subject)) + delta = datetime.now() - dt + return delta, delta >= timedelta(0) + except (ValueError, TypeError, OverflowError): + raise TypeError(f"Cannot convert {type(subject)} to a time delta") + +def delta2dict(delta: timedelta) -> Dict[str, int]: + """ + Accepts a delta, returns a dictionary of units. + + Args: + delta: A timedelta object + + Returns: + Dictionary with unit names as keys and their values + """ + delta = abs(delta) + result = {} + + for unit in TIME_UNITS: + result[unit["name"]] = unit["extract"](delta) + + return result + +def extract_components(delta: timedelta) -> List[Dict[str, Union[str, int]]]: + """ + Extract time components from a timedelta, filtering out zero values. + + Args: + delta: A timedelta object + + Returns: + List of components with values > 0 + """ + # Use delta2dict to get all time values + time_dict = delta2dict(delta) + + components = [] + # For each time unit, create a component if value > 0 + for unit in TIME_UNITS: + unit_name = unit["name"] + value = time_dict[unit_name] + + if value > 0: + components.append({ + "unit": unit_name, + "abbr": unit["abbr"], + "value": value + }) + + return components + +def format_components( + components: List[Dict[str, Union[str, int]]], + precision: int = 2, + abbreviate: bool = False +) -> str: + """ + Format the time components into a human-readable string. + + Args: + components: List of time components with values + precision: Number of units to include + abbreviate: Whether to use abbreviations + + Returns: + Formatted string (e.g., "2 years, 1 day" or "2y, 1d") + """ + result = [] + + # Only include up to 'precision' number of components + for component in components[:precision]: + if abbreviate: + result.append(f"{component['value']}{component['abbr']}") + else: + unit_name = component["unit"] + if component["value"] != 1: + # Handle plurals + unit_name += "s" + result.append(f"{component['value']} {unit_name}") + + return ", ".join(result) def human( - subject, precision=2, past_tense="{} ago", future_tense="in {}", abbreviate=False -): + subject: Union[datetime, timedelta, int, float], + precision: int = 2, + past_tense: str = "{} ago", + future_tense: str = "in {}", + abbreviate: bool = False +) -> str: """ Accept a subject, return a human readable timedelta string. - - :param subject: a datetime, timedelta, or timestamp (integer / float) object - :param precision: the desired amount of unit precision (default: 2) - :param past_tense: the format string used for a past timedelta (default: '{} ago') - :param future_tense: the format string used for a future timedelta (default: 'in {}') - :param abbreviate: boolean to abbreviate units (default: False) - - :returns: Human readable timedelta string (Str) + + Args: + subject: A datetime, timedelta, or timestamp (int/float) + precision: The desired amount of unit precision (default: 2) + past_tense: The format string used for past timedeltas (default: '{} ago') + future_tense: The format string used for future timedeltas (default: 'in {}') + abbreviate: Boolean to abbreviate units (default: False) + + Returns: + Human readable timedelta string """ - delta = get_delta_from_subject(subject) - the_tense = future_tense if delta < timedelta(0) else past_tense - - d = delta2dict(delta) - - hlist = [] - count = 0 - - # start building up the output in the hlist. - for unit in units: - if count >= precision: - break # met precision - if d[unit] == 0: - continue # skip 0's - if abbreviate: - if unit == "millisecond": - abr = "ms" - elif unit == "microsecond": - abr = "um" - else: - abr = unit[0] - hlist.append("{}{}".format(d[unit], abr)) - else: - s = "" if d[unit] == 1 else "s" # handle plurals - hlist.append("{} {}{}".format(d[unit], unit, s)) - count += 1 - - return the_tense.format(", ".join(hlist)) + # Convert input to timedelta and determine if it's in the past + delta, is_past = get_delta_from_subject(subject) + + # Extract time components (e.g., years, days, hours) + components = extract_components(delta) + + # Handle edge case: no components (very small time difference) + if not components: + return "just now" + + # Format the components into a readable string + formatted = format_components(components, precision, abbreviate) + + # Apply the appropriate tense + if is_past: + return past_tense.format(formatted) + else: + return future_tense.format(formatted) \ No newline at end of file