A focused Python library for precise duration representation, decomposition, and highly customizable compiled formatting.
Project description
Chronomancer
Precise duration representation, exact unit decomposition, and highly customizable compiled formatting for Python.
Chronomancer focuses entirely on pure, signed durations from weeks to microseconds.
No dates. No time zones. No ambiguous months or years.
Why Chronomancer?
A duration's value and its representation are not the same thing:
from chronomancer import ChronoDelta
hours = ChronoDelta(hours=24)
days = ChronoDelta(days=1)
hours == days
# True
hours.hours
# 24
days.days
# 1
Both objects represent exactly the same duration, while preserving the components used to construct them.
Normalize when you want a canonical representation. Re-express the same value in exactly the units your application needs.
Highlights
- Value and representation are separate — preserve
49 hours, normalize it to2 days, 1 hour, or express it using another selected set of units. - Exact unit decomposition — choose any combination of weeks, days, hours, minutes, seconds, milliseconds, and microseconds.
- Construct from any unit — turn one integer value in any supported unit into your preferred component layout.
timedeltainteroperability — convert to and fromdatetime.timedelta, optionally selecting the resulting components.- Exact by default — Chronomancer raises rather than silently discarding precision; truncation must be explicitly enabled.
- Flexible compiled formatting — give every component its own template, then reuse the compiled formatter.
- Solid value semantics — immutable, hashable, comparable, signed, and equipped with arithmetic and
divmod. - Zero dependencies — pure Python, standard-library only, with inline type annotations.
Installation
pip install py-chronomancer
Control the representation
The constructor preserves the components you provide:
from chronomancer import ChronoDelta
duration = ChronoDelta(hours=49, minutes=1)
duration.strfmt("{h}h {m}m")
# '49h 1m'
Normalize it into the canonical largest-to-smallest decomposition:
duration.normalized().strfmt("{d}d {h}h {m}m")
# '2d 1h 1m'
Or re-express the same duration using only selected components:
duration.expressed_in(
hours=True,
minutes=True,
).strfmt("{h}h {m}m")
# '49h 1m'
expressed_in() is exact by default. If the enabled components cannot represent the full duration, it raises instead of silently losing the remainder.
Use truncate=True only when discarding smaller units is intentional.
Construct from any unit
from_unit() accepts one integer value and a source ChronoUnit, then decomposes it directly into the components you enable:
from chronomancer import ChronoDelta, ChronoUnit
duration = ChronoDelta.from_unit(
90_061,
ChronoUnit.SECOND,
hours=True,
minutes=True,
seconds=True,
)
duration.strfmt("{h}h {m}m {s}s")
# '25h 1m 1s'
If no output components are selected, Chronomancer uses the full canonical decomposition.
This works with every supported unit:
WEEK · DAY · HOUR · MINUTE · SECOND · MILLISECOND · MICROSECOND
Convert to and from timedelta
Without component options, from_timedelta() returns a normalized ChronoDelta:
from datetime import timedelta
from chronomancer import ChronoDelta
duration = ChronoDelta.from_timedelta(
timedelta(days=2, seconds=3_661)
)
duration.strfmt("{d}d {h}h {m}m {s}s")
# '2d 1h 1m 1s'
Select components when you need a different representation:
duration = ChronoDelta.from_timedelta(
timedelta(days=2, seconds=3_661),
hours=True,
minutes=True,
seconds=True,
)
duration.strfmt("{h}h {m}m {s}s")
# '49h 1m 1s'
duration.to_timedelta()
# datetime.timedelta(days=2, seconds=3661)
Conversion is exact within the range supported by datetime.timedelta.
Formatting
Chronomancer provides two formatting layers:
strfmt()for quick, one-off layouts.DeltaFormatterfor reusable, component-aware formatting.
Quick formatting with strfmt()
duration = ChronoDelta.negative(
hours=2,
minutes=5,
seconds=9,
)
duration.strfmt("{sign}{h:02d}:{m:02d}:{s:02d}")
# '-02:05:09'
Available fields:
{sign} {w} {d} {h} {m} {s} {ms} {us}
strfmt() formats the components exactly as they are currently represented.
Reusable compiled formatting
Create a formatter once, then reuse it across many durations:
from chronomancer import (
ChronoDelta,
DeltaFormatSpec,
create_delta_formatter,
)
clock = create_delta_formatter(
DeltaFormatSpec(
hours="{value:02d}",
minutes="{value:02d}",
seconds="{value:02d}",
separator=":",
show_zero_components=True,
)
)
clock.format(ChronoDelta(hours=1, minutes=1, seconds=1))
# '01:01:01'
Each component has its own independent template. Components set to None do not participate, and zero-valued components can be omitted automatically:
compact = create_delta_formatter(
DeltaFormatSpec(
days="{value}{symbol}",
hours="{value}{symbol}",
minutes="{value}{symbol}",
separator=" ",
)
)
compact.format(ChronoDelta(days=2, minutes=5))
# '2d 5m'
Available component placeholders:
{value} {name} {abbr} {symbol} {plural}
They support standard Python format specifications and the !s, !r, and !a conversions:
human = create_delta_formatter(
DeltaFormatSpec(
hours="{value} {name}{plural}",
minutes="{value} {name}{plural}",
separator=", ",
)
)
human.format(ChronoDelta(hours=2, minutes=1))
# '2 hours, 1 minute'
The formatter also supports:
- negative-only, always-visible, or hidden signs through
sign="-",sign="+-", orsign=None; - custom separators;
- automatic zero-component omission;
- fixed-width output with
show_zero_components=True.
Static unit metadata is resolved when the formatter is created. Compile once, then format repeatedly.
Value semantics
Equality, hashing, and ordering use the exact duration value:
ChronoDelta(hours=24) == ChronoDelta(days=1)
# True
hash(ChronoDelta(hours=24)) == hash(ChronoDelta(days=1))
# True
Arithmetic also operates on the exact value and returns normalized results:
ChronoDelta(hours=20) + ChronoDelta(hours=5)
# ChronoDelta representing 1 day and 1 hour
ChronoDelta supports:
+ - * divmod() abs() unary - comparisons bool()
API at a glance
ChronoDelta(...)— construct a duration while preserving the supplied components.ChronoDelta.negative(...)— construct a negative duration from non-negative components.ChronoDelta.zero()— obtain the cached zero value.ChronoDelta.from_total_us(...)— construct from total microseconds with canonical decomposition.ChronoDelta.from_unit(...)— construct from one source unit and selected output components.normalized()— obtain the canonical representation.expressed_in(...)— re-express an existing duration using selected components.from_timedelta(...)/to_timedelta()— interoperate with the standard library.strfmt(...)/verbose_str(...)— quick built-in string formatting.DeltaFormatSpec+create_delta_formatter(...)— create reusable compiled formatters.components/iter_components()— inspect values together with theirChronoUnit.total_secondsthroughtotal_weeks— access floating-point totals in common units.is_positive,is_negative,is_zero, andsign— inspect the duration's sign.
License
MIT License: MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_chronomancer-0.1.0.tar.gz.
File metadata
- Download URL: py_chronomancer-0.1.0.tar.gz
- Upload date:
- Size: 11.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c04120887d52bdee707c7bf6ff6fe3e0f9916e39373771397987774c2e01f436
|
|
| MD5 |
72ac725b8cd7bb8f3c68911ea7407740
|
|
| BLAKE2b-256 |
693517fe9366664034564460d2d014eb423595719de5282d40ff87cc5bf30a74
|
File details
Details for the file py_chronomancer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: py_chronomancer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0deb8db703812dbd2b6f6cafd5427b3ff9d584a12fed3f49d0089dc8dfbbd954
|
|
| MD5 |
250eafc16cbc032f82ae4019ea4b8f90
|
|
| BLAKE2b-256 |
0957324286809b2da190d33f50cae646fe8acfc84c24a61d7bcdf581e0e704b0
|