diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 94d0525..c7047ca 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -27,7 +27,7 @@ jobs:
with:
python-version: "3.12"
enable-cache: true
- cache-dependency-glob: "pyproject.toml"
+ cache-dependency-glob: "uv.lock"
- name: Install package with check dependencies
run: uv sync --group check
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 7364816..7583831 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -25,16 +25,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ - name: Set up uv
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
- python-version: "3.x"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install build
+ python-version: "3.12"
- name: Build package
- run: python -m build
+ run: uv build
- name: Publish package
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
with:
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index ef56dff..66bdd49 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -42,8 +42,8 @@ jobs:
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: ${{ matrix.python }}
- cache: "pip"
- cache-dependency-path: "**/pyproject.toml"
+ enable-cache: true
+ cache-dependency-glob: "uv.lock"
- name: Install package with check dependencies
run: uv sync --group test
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index aa35e82..6ba85cf 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -1,4 +1,9 @@
+.sidebar-logo {
+ width: 200px;
+ height: auto;
+}
+
div.sphinxsidebar .powered_by a {
text-decoration: none;
border-bottom: none;
diff --git a/docs/_static/undate_logo.svg b/docs/_static/undate_logo.svg
new file mode 100644
index 0000000..52bba2e
--- /dev/null
+++ b/docs/_static/undate_logo.svg
@@ -0,0 +1,14 @@
+
diff --git a/docs/_static/undate_logo_dark.svg b/docs/_static/undate_logo_dark.svg
new file mode 100644
index 0000000..bc907b7
--- /dev/null
+++ b/docs/_static/undate_logo_dark.svg
@@ -0,0 +1,15 @@
+
diff --git a/docs/conf.py b/docs/conf.py
index ff5d7d3..0f16646 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,7 +10,6 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
-
import sys
from pathlib import Path
@@ -39,8 +38,12 @@
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"myst_parser",
+ "sphinx_pyodide",
]
+# build static output for noscript fallback
+pyodide_build_output = True
+
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -59,30 +62,34 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = "alabaster"
+html_theme = "furo"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
+html_css_files = ["custom.css"]
html_theme_options = {
- "logo": "undate_logo.png",
- "logo_name": False,
- "github_user": "dh-tech",
- "github_repo": "undate-python",
- "github_button": False,
- "github_banner": True,
-}
-
-html_sidebars = {
- "**": [
- "about.html",
- "navigation.html",
- "localtoc.html",
- "searchbox.html",
- "sidebar_dhtech.html",
- ],
+ "light_logo": "undate_logo.svg",
+ "dark_logo": "undate_logo_dark.svg",
+ "source_repository": "https://github.com/dh-tech/undate-python",
+ "source_branch": "main",
+ "source_directory": "docs/",
+ "light_css_variables": {
+ "color-brand-primary": "#6670bb",
+ "color-brand-content": "#6670bb",
+ "color-background-primary": "#f0f1f8",
+ "color-background-secondary": "#e5e7f2",
+ "sidebar-logo-width": "200px",
+ },
+ "dark_css_variables": {
+ "color-brand-primary": "#9198d0",
+ "color-brand-content": "#9198d0",
+ "color-background-primary": "#1d202f",
+ "color-background-secondary": "#151825",
+ "sidebar-logo-width": "200px",
+ },
}
# turn on relative links; make sure both github and sphinx links work
diff --git a/docs/example_usage.rst b/docs/example_usage.rst
new file mode 100644
index 0000000..0b3f850
--- /dev/null
+++ b/docs/example_usage.rst
@@ -0,0 +1,232 @@
+Example Usage
+--------------
+
+Often humanities and cultural data include imprecise or uncertain temporal information. We want to store that information but also work with it in a structured way, not just treat it as text for display. Different projects may need to work with or convert between different date formats or even different calendars.
+
+An ``undate.Undate`` is analogous to python’s builtin ``datetime.date`` object, but with support for varying degrees of precision and unknown information. You can initialize an ``Undate`` with either strings or numbers for whichever parts of the date are known or partially known. An ``Undate`` can take an optional label.
+
+.. pyodide::
+ :packages: ./wheels/PyMeeus-0.5.12-py3-none-any.whl,./wheels/undate-0.8.0.dev0-py3-none-any.whl
+
+ from undate import __version__
+
+ print(f"Running undate v{__version__}")
+
+
+.. pyodide::
+ from undate import Undate
+
+ november7 = Undate(2000, 11, 7)
+ november = Undate(2000, 11)
+ year2k = Undate(2000)
+ november7_some_year = Undate(month=11, day=7)
+
+ partially_known_year = Undate("19XX")
+ partially_known_month = Undate(2022, "1X")
+
+ easter1916 = Undate(1916, 4, 23, label="Easter 1916")
+
+You can convert an ``Undate`` to string using a date formatter (current default is ISO8601):
+
+
+.. pyodide::
+ print([str(d) for d in [november7, november, year2k, november7_some_year]])
+
+
+If enough information is known, an ``Undate`` object can report on its duration:
+
+.. pyodide::
+ december = Undate(2000, 12)
+ feb_leapyear = Undate(2024, 2)
+ feb_regularyear = Undate(2023, 2)
+
+ example_dates = [
+ november7, november, december, year2k,
+ november7_some_year, feb_regularyear, feb_leapyear
+ ]
+ for d in example_dates:
+ print(f"{d!s:<10} duration in days: {d.duration().days:>2}")
+
+If enough of the date is known and the precision supports it, you can
+check if one date falls within another date:
+
+.. pyodide::
+ november7 = Undate(2000, 11, 7)
+ november2000 = Undate(2000, 11)
+ year2k = Undate(2000)
+ ad100 = Undate(100)
+ november7 in november
+
+ yes_no = {True: "✅", False: "❌"}
+ for range in [november2000, year2k, ad100]:
+ print(f"{november7!s:>10} within {range!s:<10}? {yes_no[november7 in range]}")
+ if november2000 != range: # don't test against itself
+ print(f"{november2000!s:>10} within {range!s:<10}? {yes_no[november2000 in range]}")
+
+
+For dates that are imprecise or partially known, ``undate`` calculates
+earliest and latest possible dates for comparison purposes so you can
+sort dates ...
+
+.. pyodide::
+ november7_2020 = Undate(2020, 11, 7)
+ november_2001 = Undate(2001, 11)
+ year2k = Undate(2000)
+ ad100 = Undate(100)
+ for date in sorted([november7_2020, november_2001, year2k, ad100]):
+ # print the date in ISO/EDTF format along with the python representation
+ print(f"{date!s:>10} : {repr(date)}")
+
+
+You can also compare with equals, greater than, and less than. You
+can also compare with python ``datetime.date`` objects.
+
+.. pyodide::
+ from datetime import date
+
+ jan2001 = date(2001, 1, 1)
+
+ print(f"{november7_2020!s:>10} before {november_2001!s:<10} ? {yes_no[november7_2020 > november_2001]}")
+ print(f"{year2k!s:>10} after {ad100!s:<10} ? {yes_no[year2k > ad100]}")
+ print(f"{year2k!s:>10} after {jan2001} ? {yes_no[year2k > jan2001]}")
+
+
+When dates cannot be compared due to ambiguity or precision, comparison
+methods raise a ``NotImplementedError``.
+
+.. pyodide::
+ :show-errors:
+
+ november_2020 = Undate(2020, 11)
+ try:
+ november7_2020 > november_2020
+ except NotImplementedError as err:
+ print(err)
+
+An ``UndateInterval`` is a date range between two ``Undate`` objects.
+Intervals can be open-ended, allow for optional labels, and can
+calculate duration if enough information is known. ``UndateIntervals``
+are inclusive (i.e., a closed interval), and include both the earliest
+and latest date as part of the range.
+
+.. pyodide::
+ from undate import UndateInterval
+
+ century19 = UndateInterval(Undate(1801), Undate(1900), label="19th century")
+ century20 = UndateInterval(Undate(1901), Undate(2000), label="20th century")
+ before2000 = UndateInterval(latest=Undate(2000)) # before 2000
+ after1900 = UndateInterval(Undate(1900)) # after 1900
+
+ for interval in [century19, century20, before2000, after1900]:
+ print(f"{repr(interval)}\n{interval}\n")
+
+Intervals can calculate duration if enough information is known:
+
+.. pyodide::
+ jan2000_interval = UndateInterval(
+ Undate(2000, 1, 1),
+ Undate(2000, 1, 31),
+ label="January 2000"
+ )
+ for interval in [century19, century20, jan2000_interval]:
+ print(f"{interval.label}: {interval.duration().days:,} days")
+
+Parsing and Formatting
+~~~~~~~~~~~~~~~~~~~~~~
+
+You can initialize ``Undate`` or ``UndateInterval`` objects by parsing a
+date string with a named converter, and output an ``Undate`` in a
+different format than it was parsed. The ``"ISO8601"`` and ``"EDTF"``
+converters handle the most common structured formats:
+
+.. pyodide::
+ print(repr(Undate.parse("2002", "ISO8601")))
+ print(repr(Undate.parse("2002-05", "EDTF")))
+ print(repr(Undate.parse("--05-03", "ISO8601")))
+ print(repr(Undate.parse("1800/1900", "EDTF")))
+
+ # convert between formats
+ print(Undate.parse("--05-03", "ISO8601").format("EDTF"))
+
+The ``"Gregorian"`` converter parses dates with full or abbreviated month
+names across multiple languages:
+
+.. pyodide::
+ :editable:
+ :show-errors:
+
+ dates = [
+ "7 November 2000",
+ "Nov 2000",
+ "avril 1362",
+ "2022 Ugushyingo 26",
+ ]
+ for date in dates:
+ print(repr(Undate.parse(date, "Gregorian")))
+
+
+The ``"holidays"`` converter parses Christian liturgical dates, including
+movable feasts:
+
+.. pyodide::
+ :editable:
+ :show-errors:
+
+ holidays = [
+ "Epiphany 1942",
+ "Easter 1942",
+ "Ash Wednesday 1942",
+ ]
+ undate_holidays = [Undate.parse(hol, "holidays") for hol in holidays]
+
+ for holidate in undate_holidays:
+ print(f"{holidate.label}: {holidate!s} (earliest: {holidate.earliest}, latest: {holidate.latest})")
+
+
+Calendars
+~~~~~~~~~
+
+All ``Undate`` objects are calendar aware, and date converters include
+support for parsing and working with dates from other calendars. The
+Gregorian calendar is used by default; currently ``undate`` supports the
+Islamic Hijri calendar and the Hebrew Anno Mundi calendar.
+
+Dates are stored with the year, month, day and appropriate precision for
+the original calendar; internally, earliest and latest dates are
+calculated in Gregorian / Proleptic Gregorian calendar for standardized
+comparison across dates from different calendars.
+
+.. pyodide::
+ tammuz4816 = Undate.parse("26 Tammuz 4816", "Hebrew")
+ rajab495 = Undate.parse("Rajab 495", "Islamic")
+ y2k = Undate.parse("2001", "EDTF")
+
+ for d in [tammuz4816, rajab495, y2k]:
+ print(f"{d!s:<10} {repr(d)}")
+
+.. pyodide::
+ print("Earliest Gregorian equivalent:")
+ for d in [rajab495, tammuz4816, y2k]:
+ print(f" {str(d):<20} earliest: {d.earliest}")
+
+ print("\nPrecision:")
+ for d in [rajab495, tammuz4816, y2k]:
+ print(f" {str(d):<20} precision: {d.precision}")
+
+.. pyodide::
+ print("Sorted by Gregorian date:")
+ for d in sorted([rajab495, tammuz4816, y2k]):
+ print(f" {repr(d)}")
+
+Try it yourself
+~~~~~~~~~~~~~~~
+
+.. pyodide::
+ :editable:
+ :show-errors:
+
+ # try your own dates here
+ from undate import Undate, UndateInterval
+
+ mydate = Undate(2025, 6)
+ print(mydate)
diff --git a/docs/index.rst b/docs/index.rst
index 4f302f9..294c40d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,14 +1,92 @@
-undate documentation
-====================
+undate
+======
-**undate** is a python library for working with uncertain or partially known dates.
+.. image:: _static/undate_logo.svg
+ :alt: undate
+ :width: 350px
+ :class: only-light
+.. image:: _static/undate_logo_dark.svg
+ :alt: undate
+ :width: 350px
+ :class: only-dark
+
+**undate** is a Python library for working with uncertain or partially known dates.
+
+.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.11068867.svg
+ :target: https://doi.org/10.5281/zenodo.11068867
+.. image:: https://img.shields.io/badge/License-Apache_2.0-blue.svg
+ :target: https://opensource.org/licenses/Apache-2.0
+.. image:: https://readthedocs.org/projects/undate-python/badge/?version=latest
+ :target: https://undate-python.readthedocs.io/en/latest/?badge=latest
+.. image:: https://github.com/dh-tech/undate-python/actions/workflows/unit_tests.yml/badge.svg
+ :target: https://github.com/dh-tech/undate-python/actions/workflows/unit_tests.yml
+.. image:: https://codecov.io/gh/dh-tech/undate-python/branch/main/graph/badge.svg?token=GE7HZE8C9D
+ :target: https://codecov.io/gh/dh-tech/undate-python
+
+.. note::
+ This is beta software; it is still in development and not fully feature complete.
+ If you use it, please let us know and share your feedback.
+
+``undate`` supports parsing, formatting, and reasoning with dates in varying
+precision and calendars. Dates with different precision and from different original
+calendars can be used together. Supported formats include:
+
+- portions of EDTF (Extended Date Time Format)
+- ISO8601
+- parsing and calendar conversion for dates in Hebrew Anno Mundi and Islamic Hijri calendars
+- Gregorian dates with full or abbreviated month names in any order for multiple languages
+ (English, Spanish, French, German, Kinyarwanda, Ganda, Tigrinya)
+- Christian liturgical dates (fixed holidays and movable feasts)
+
+For unambiguous dates, there is an experimental omnibus parser which combines all available parsers.
+
+For more about the origin and goals of ``undate``, read our 2025 software paper:
+
+ Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford.
+ "`Undate: Humanistic Dates for Computation `_."
+ *Computational Humanities Research*, August 5, 2025.
+
+Quick Start
+-----------
+
+An ``Undate`` is analogous to Python's ``datetime.date``, but with support for
+varying precision and unknown information:
+
+.. code-block:: python
+
+ from undate import Undate, UndateInterval
+
+ november = Undate(2000, 11) # year and month known
+ year2k = Undate(2000) # year only
+ partially_known = Undate("19XX") # partially unknown year
+
+ print([str(d) for d in [november, year2k, partially_known]])
+ # ['2000-11', '2000', '19XX']
+
+Dates can be parsed from multiple formats and calendars, sorted, and compared
+even when precision differs. See :doc:`example_usage` for interactive examples.
+
+Installation
+------------
+
+Install the latest published version from PyPI:
+
+.. code-block:: console
+
+ pip install undate
+
+To install a development version or specific branch:
+
+.. code-block:: console
+
+ pip install git+https://github.com/dh-tech/undate-python@develop#egg=undate
.. toctree::
- :maxdepth: 2
+ :maxdepth: 1
:caption: Contents:
- readme
+ example_usage
undate/index
CONTRIBUTING
DEVELOPER_NOTES
diff --git a/docs/readme.md b/docs/readme.md
deleted file mode 100644
index 4ec94a5..0000000
--- a/docs/readme.md
+++ /dev/null
@@ -1,4 +0,0 @@
-```{include} ../README.md
-:relative-docs: /
-:relative-images:
-```
diff --git a/pyproject.toml b/pyproject.toml
index 09c8ca5..23fb665 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,7 +51,13 @@ classifiers = [
]
[dependency-groups]
-docs = ["sphinx>=7.0.0", "alabaster", "myst-parser", "myst-parser[linkify]"]
+docs = [
+ "sphinx>=8.0.0",
+ "furo",
+ "myst-parser",
+ "myst-parser[linkify]",
+ "sphinx-pyodide",
+]
test = ["pytest>=9", "pytest-ordering", "pytest-cov"]
notebooks = ["jupyterlab", "pandas", "treon", "altair"]
check = [ { include-group = "docs" }, {include-group = "notebooks"}, "mypy", "ruff"]
@@ -60,9 +66,10 @@ dev = [
"twine",
"wheel",
"build",
+ "sphinx-autobuild>=2024.10.3",
{ include-group = "test" },
{ include-group = "check" },
- { include-group = "docs" }
+ { include-group = "docs" },
]
[project.urls]
@@ -122,3 +129,7 @@ ignore = []
[tool.ruff.lint.per-file-ignores]
# for test files, don't require docstrings or return type annotations
"tests/**.py" = ["D", "ANN", "RUF"]
+
+# uncomment for local development of sphinx-pyodide
+#[tool.uv.sources]
+#sphinx-pyodide = { path = "../sphinx-pyodide", editable = true }