Claude refactor
This commit is contained in:
parent
c5712e5f34
commit
1090d4ae50
2 changed files with 361 additions and 159 deletions
262
README.rst
262
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::
|
Testing past tense::
|
||||||
|
|
||||||
Russell commented 1 year, 127 days, 16 hours ago
|
Russell commented 1 year, 127 days, 16 hours ago
|
||||||
You replied 1 year, 127 days ago
|
You replied 1 year, 127 days ago
|
||||||
|
|
||||||
Testing future tense::
|
Testing future tense::
|
||||||
|
|
||||||
Program will shutdown in 2 days, 3 hours, 27 minutes
|
Program will shutdown in 2 days, 3 hours, 27 minutes
|
||||||
Job will run 2 days, 3 hours from now
|
Job will run 2 days, 3 hours from now
|
||||||
|
|
||||||
|
|
||||||
How to install
|
Installation
|
||||||
===================
|
============
|
||||||
|
|
||||||
There are a number of ways to install this package.
|
There are a number of ways to install this package.
|
||||||
|
|
||||||
You could run this ad hoc command::
|
You could run this ad hoc command::
|
||||||
|
|
||||||
pip install ago
|
pip install ago
|
||||||
|
|
||||||
or specify *ago* under the *setup_requires* list within your
|
or specify *ago* under the *setup_requires* list within your
|
||||||
*setuptools*-compatible project's *setup.py* file.
|
`setuptools <https://setuptools.readthedocs.io>`_-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
|
1. ``human``: Convert a datetime or timedelta to a human-readable string
|
||||||
#. delta2dict
|
2. ``delta2dict``: Convert a timedelta to a dictionary of units
|
||||||
#. get_delta_from_subject
|
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
|
from ago import human
|
||||||
a datetime, timedelta, or timestamp (integer/float) object to become human readable
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
precision (default 2):
|
# With a datetime object
|
||||||
the desired amount of unit precision
|
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'):
|
# With a timedelta object
|
||||||
the format string used for a past timedelta
|
delta = timedelta(days=5, hours=3, minutes=45)
|
||||||
|
print('Due in ' + human(delta)) # "Due in 5 days, 3 hours"
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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 <http://twitter.com/russellbal>`_.
|
||||||
|
|
||||||
License
|
License
|
||||||
=========================
|
-------
|
||||||
|
|
||||||
* Public Domain
|
This project is in the Public Domain.
|
||||||
|
|
||||||
|
Revision Control
|
||||||
|
----------------
|
||||||
|
|
||||||
Public Revision Control
|
The public revision control repository is available at: `https://git.unturf.com/python/ago <https://git.unturf.com/python/ago>`_.
|
||||||
==============================
|
|
||||||
|
|
||||||
* https://git.unturf.com/python/ago
|
|
||||||
|
|
|
||||||
258
ago/ago.py
258
ago/ago.py
|
|
@ -1,82 +1,194 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, Union, List, Tuple, Any, Callable
|
||||||
|
|
||||||
units = ("year", "day", "hour", "minute", "second", "millisecond", "microsecond")
|
# Define time units as a data structure
|
||||||
|
# Each unit contains:
|
||||||
|
# - name: full name of the unit (e.g., "year")
|
||||||
def get_delta_from_subject(subject):
|
# - abbr: abbreviation (e.g., "y")
|
||||||
"""Convert the subject to a timedelta and return it."""
|
# - seconds: number of seconds in this unit (for reference)
|
||||||
if isinstance(subject, timedelta):
|
# - extract: function to extract this unit's value from a timedelta
|
||||||
return subject
|
TIME_UNITS = [
|
||||||
|
{
|
||||||
if isinstance(subject, datetime):
|
"name": "year",
|
||||||
dt = subject
|
"abbr": "y",
|
||||||
else:
|
"seconds": 31536000,
|
||||||
# if it's not a datetime, assume it's a unix timestamp
|
"extract": lambda td: int(td.days / 365)
|
||||||
try:
|
},
|
||||||
subject = float(subject)
|
{
|
||||||
except ValueError:
|
"name": "day",
|
||||||
# some unknown type that couldn't be converted to a float
|
"abbr": "d",
|
||||||
raise TypeError("Unsupported subject type")
|
"seconds": 86400,
|
||||||
|
"extract": lambda td: int(td.days % 365)
|
||||||
dt = datetime.fromtimestamp(subject)
|
},
|
||||||
|
{
|
||||||
# return a timedelta from the datetime, using its timezone (if it has one)
|
"name": "hour",
|
||||||
return datetime.now(tz=dt.tzinfo) - dt
|
"abbr": "h",
|
||||||
|
"seconds": 3600,
|
||||||
|
"extract": lambda td: int(td.seconds / 3600)
|
||||||
def delta2dict(delta):
|
},
|
||||||
"""Accepts a delta, returns a dictionary of units"""
|
{
|
||||||
delta = abs(delta)
|
"name": "minute",
|
||||||
return {
|
"abbr": "m",
|
||||||
"year": int(delta.days / 365),
|
"seconds": 60,
|
||||||
"day": int(delta.days % 365),
|
"extract": lambda td: int(td.seconds / 60) % 60
|
||||||
"hour": int(delta.seconds / 3600),
|
},
|
||||||
"minute": int(delta.seconds / 60) % 60,
|
{
|
||||||
"second": int(delta.seconds % 60),
|
"name": "second",
|
||||||
"millisecond": int(delta.microseconds / 1000),
|
"abbr": "s",
|
||||||
"microsecond": int(delta.microseconds % 1000),
|
"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(
|
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.
|
Accept a subject, return a human readable timedelta string.
|
||||||
|
|
||||||
:param subject: a datetime, timedelta, or timestamp (integer / float) object
|
Args:
|
||||||
:param precision: the desired amount of unit precision (default: 2)
|
subject: A datetime, timedelta, or timestamp (int/float)
|
||||||
:param past_tense: the format string used for a past timedelta (default: '{} ago')
|
precision: The desired amount of unit precision (default: 2)
|
||||||
:param future_tense: the format string used for a future timedelta (default: 'in {}')
|
past_tense: The format string used for past timedeltas (default: '{} ago')
|
||||||
:param abbreviate: boolean to abbreviate units (default: False)
|
future_tense: The format string used for future timedeltas (default: 'in {}')
|
||||||
|
abbreviate: Boolean to abbreviate units (default: False)
|
||||||
:returns: Human readable timedelta string (Str)
|
|
||||||
|
Returns:
|
||||||
|
Human readable timedelta string
|
||||||
"""
|
"""
|
||||||
delta = get_delta_from_subject(subject)
|
# Convert input to timedelta and determine if it's in the past
|
||||||
the_tense = future_tense if delta < timedelta(0) else past_tense
|
delta, is_past = get_delta_from_subject(subject)
|
||||||
|
|
||||||
d = delta2dict(delta)
|
# Extract time components (e.g., years, days, hours)
|
||||||
|
components = extract_components(delta)
|
||||||
hlist = []
|
|
||||||
count = 0
|
# Handle edge case: no components (very small time difference)
|
||||||
|
if not components:
|
||||||
# start building up the output in the hlist.
|
return "just now"
|
||||||
for unit in units:
|
|
||||||
if count >= precision:
|
# Format the components into a readable string
|
||||||
break # met precision
|
formatted = format_components(components, precision, abbreviate)
|
||||||
if d[unit] == 0:
|
|
||||||
continue # skip 0's
|
# Apply the appropriate tense
|
||||||
if abbreviate:
|
if is_past:
|
||||||
if unit == "millisecond":
|
return past_tense.format(formatted)
|
||||||
abr = "ms"
|
else:
|
||||||
elif unit == "microsecond":
|
return future_tense.format(formatted)
|
||||||
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))
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue