py-datenum
MATLAB-style numeric dates for Python. A thin, well-tested wrapper around the
standard datetime module that represents dates as datenums — fractional
days since a configurable epoch — so date arithmetic becomes plain arithmetic:
import datetime as dt
import datenum
dn = datenum.from_datetime(dt.datetime(2024, 7, 15))
dn + 7 # a week later, as a number
dn2 = datenum.from_datetime(dt.datetime(2024, 1, 31))
dn2 - dn # days between two dates: -166.0
Why?
Comparing, sorting, differencing, and interpolating timestamps is simplest when
they are numbers. MATLAB users know this workflow as datenum. This library
brings it to Python while staying fully interoperable with datetime, and adds
NumPy-vectorized conversions for array workloads.
Features
- Scalar conversions between
datetime,(year, month, day, ...)tuples, strings, and datenums. - Vectorized conversions accepting lists, tuples, and NumPy arrays — every conversion function accepts scalar or array input.
- Component accessors (
year,month,day,hour,minute,second,microsecond) that work element-wise on arrays. - Calendar helpers:
is_leap_year,day_of_year,day_of_week,day_of_year_229(fixed 366-day calendar for climatology). - Month arithmetic with end-of-month clamping (
add_month). - Configurable epoch, globally via
set_epochor scoped viatemporary_epoch. - Microsecond-precision roundtrips (
datetime -> datenum -> datetimeis exact at microsecond resolution for dates near the epoch).
Installation
Requires Python ≥ 3.10.
pip install datenum
or with uv:
uv add datenum
Dependencies: python-dateutil and numpy (optional for vectorization) .
Core concept: datenum
A datenum is a float: the number of days (including fractions) since the
epoch. Values after the epoch are positive; values before it are negative.
The default epoch is 2000-01-01T00:00:00 — naive (no timezone), at
midnight. datenum.from_datetime(dt.datetime(2000, 1, 1)) is exactly 0.0;
noon the same day is 0.5; 1999-12-31 is -1.0.
Note: this differs from MATLAB, whose
datenumcounts from year 0. Convert once at your system boundary, or set the epoch to match your data.
Timezones: datenums are timezone-less by design. The epoch must be naive and at midnight; timezone interpretation is the caller's responsibility.
Quick start
Conversions
import datetime as dt
import datenum
# datetime <-> datenum
dn = datenum.from_datetime(dt.datetime(2024, 7, 15, 13, 45, 30))
datenum.to_datetime(dn) # datetime.datetime(2024, 7, 15, 13, 45, 30)
# YMD <-> datenum
dn = datenum.from_ymd(2024, 7, 15) # midnight assumed
dn = datenum.from_ymd(2024, 7, 15, 10, 30, 45) # with time of day
datenum.to_ymd(dn) # (2024, 7, 15, 10, 30, 45, 0)
# Strings <-> datenum (Python strptime/strftime formats, plus fuzzy parsing)
dn = datenum.from_string("15/07/2024", fmt="%d/%m/%Y")
datenum.to_string(dn, fmt="%Y-%m-%d") # '2024-07-15'
Arithmetic
Because datenums are floats, durations and shifts are ordinary numbers:
dn = datenum.from_ymd(2024, 1, 1)
later = dn + 90 # 90 days later
gap = later - dn # 90.0
dn = datenum.add_month(dn, 1) # calendar-aware month addition
add_month clamps overflow days: Jan 31 + 1 month is Feb 29 (leap year) or
Feb 28 otherwise, and time-of-day is preserved.
Components and calendar helpers
dn = datenum.from_ymd(2024, 7, 15, 13, 45, 30, 123456)
datenum.year(dn) # 2024
datenum.month(dn) # 7
datenum.day(dn) # 15 (day of month)
datenum.hour(dn) # 13
datenum.minute(dn) # 45
datenum.second(dn) # 30
datenum.microsecond(dn) # 123456
datenum.day_of_week(dn) # 1 (ISO: 1=Monday … 7=Sunday)
datenum.day_of_year(dn) # 197 (1–366)
datenum.is_leap_year(dn) # True
Arrays (NumPy)
Every conversion function accepts scalar or array input and returns a scalar or array correspondingly:
import numpy as np
import datenum
dns = datenum.from_datetime_array(
np.array(["2000-01-01", "2000-01-02"], dtype="datetime64[ns]")
)
# array([0., 1.])
dt64 = datenum.to_datetime_array(np.array([0.0, 0.5]))
# array(['2000-01-01T00:00:00', '2000-01-01T12:00:00'], dtype='datetime64[ns]')
years = datenum.year([0.0, 400.0, 8000.0]) # array([2000, 2001, 2021])
leap = datenum.is_leap_year([0.0, 366.0]) # array([True, False])
shifted = datenum.add_month(np.array([0.0, 31.0]), 1) # broadcast over dn
Rules for add_month broadcasting: scalar dn + array delta, array dn +
scalar delta (broadcast), or two arrays of identical shape (mismatched shapes
raise ValueError).
Array results use vectorized integer/nanosecond paths — no per-element Python
loops — and return float64, int64, or datetime64[ns] arrays.
Configuring the epoch
import datetime as dt
import datenum
datenum.set_epoch(dt.datetime(1970, 1, 1)) # module-wide, affects everything after
assert datenum.get_epoch() == dt.datetime(1970, 1, 1)
with datenum.temporary_epoch(dt.datetime(1900, 1, 1)):
dn = datenum.from_datetime(dt.datetime(1900, 1, 2)) # 1.0 within the block
# previous epoch restored automatically
Both reject invalid epochs with ValueError:
- timezone-aware datetimes,
- datetimes with a time-of-day other than midnight.
A failed set_epoch leaves the current epoch unchanged.
API summary
| Category | Functions |
|---|---|
| Epoch | get_epoch, set_epoch, temporary_epoch |
| datetime ↔ datenum | from_datetime, to_datetime, from_datetime_array, to_datetime_array |
| YMD ↔ datenum | from_ymd, to_ymd |
| Strings ↔ datenum | from_string, to_string |
| Components | year, month, day, hour, minute, second, microsecond |
| Calendar | is_leap_year, day_of_year, day_of_week, day_of_month, day_of_year_229 |
| Arithmetic | add_month |
Full details live in the docstrings (help(datenum) / datenum.from_ymd?).
Behavior notes
- String parsing follows Python conventions, not MATLAB
datestrsyntax:fmtis astrptime/strftimepattern. Withoutfmt,from_stringusesdateutil's flexible parser. - Day-of-week is ISO (
1=Monday … 7=Sunday); MATLAB'sweekdayinstead uses1=Sunday. - Validation: scalar
from_ymdrejects non-whole floats and out-of-range components withValueError; array inputs are assumed valid and are not range-checked. - Precision: datenums are
float64(~15–16 significant digits). Around the default epoch that comfortably covers microsecond resolution; dates many tens of thousands of years from the epoch lose sub-second precision. - Negative datenums work throughout (before the epoch), including arrays.
Development
uv sync # create venv + install deps
uv run pytest # run the test suite
uv run ruff check . # lint
Metadata
Release files for datenum 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| datenum-0.1.0.tar.gz | 11.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| datenum-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.7 kB
Release files / datenum-0.1.0.tar.gz
| Download URL | datenum-0.1.0.tar.gz |
|---|---|
| Size | 11.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e0c3eea8abd7ad1f2d30824377acc8111fba61ff293a7898a6c9a72a6449e4d1
|
|
BLAKE2b-256 checksum How to use checksums |
615744b64c5496b9164e308e83788982c7a8ad52dc0d71240fee0c48cde32bc5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency logRelease files / datenum-0.1.0-py3-none-any.whl
| Download URL | datenum-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2b6d57bdf27dce5780d13f2a83967d3d06a2642c9ea7cf511fb3c78d1afc0c0f
|
|
BLAKE2b-256 checksum How to use checksums |
c010e281270b7ecbd1b1cde84b7576d2332f498e5e797f486b3fa48b58789228
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency log