From 51311932167a4b6b646020f00e4648e9e21a194e Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 16 Jan 2026 09:03:34 -0500 Subject: [PATCH 01/14] Preliminary sphinx-pyodide integration --- docs/conf.py | 10 ++++++---- docs/index.rst | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a55e3ae..2e1e361 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,9 +10,10 @@ # 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 os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys + +sys.path.insert(0, os.path.abspath(".")) import undate @@ -23,7 +24,7 @@ author = "DHtech Community" # The full version, including alpha/beta/rc tags -release = undate.__version__ +release = undate.__version__ # type: ignore[attr-defined] master_doc = "index" @@ -37,6 +38,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "myst_parser", + "sphinx_pyodide", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.rst b/docs/index.rst index 4f302f9..9ad10ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ undate documentation :caption: Contents: readme + pyodide undate/index CONTRIBUTING DEVELOPER_NOTES From 363293a50209ff039b7899a59645dfa1c4abc993 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 15:42:26 -0400 Subject: [PATCH 02/14] Switch docs theme to furo, add light/dark mode logo Assisted-by: OpenCode:BigPickle (glm-5.1) --- docs/_static/custom.css | 5 ++++ docs/_static/undate_logo.svg | 14 +++++++++++ docs/_static/undate_logo_dark.svg | 15 +++++++++++ docs/conf.py | 42 +++++++++++++++++-------------- pyproject.toml | 9 +++++-- 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 docs/_static/undate_logo.svg create mode 100644 docs/_static/undate_logo_dark.svg 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 2e1e361..4044f4e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,10 +10,10 @@ # 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 os import sys +from pathlib import Path -sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(0, str(Path().resolve())) import undate @@ -59,30 +59,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/pyproject.toml b/pyproject.toml index 09c8ca5..02aae48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ classifiers = [ ] [dependency-groups] -docs = ["sphinx>=7.0.0", "alabaster", "myst-parser", "myst-parser[linkify]"] +docs = ["sphinx>=7.0.0", "furo", "myst-parser", "myst-parser[linkify]"] test = ["pytest>=9", "pytest-ordering", "pytest-cov"] notebooks = ["jupyterlab", "pandas", "treon", "altair"] check = [ { include-group = "docs" }, {include-group = "notebooks"}, "mypy", "ruff"] @@ -60,9 +60,11 @@ dev = [ "twine", "wheel", "build", + "sphinx-autobuild>=2024.10.3", + "sphinx-pyodide", { include-group = "test" }, { include-group = "check" }, - { include-group = "docs" } + { include-group = "docs" }, ] [project.urls] @@ -122,3 +124,6 @@ ignore = [] [tool.ruff.lint.per-file-ignores] # for test files, don't require docstrings or return type annotations "tests/**.py" = ["D", "ANN", "RUF"] + +[tool.uv.sources] +sphinx-pyodide = { path = "../sphinx-pyodide", editable = true } From 76af22ed6b6f18d66b92c86deaa6c0f8a090364c Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 17:29:12 -0400 Subject: [PATCH 03/14] Add live examples based on the readme --- docs/conf.py | 3 ++ docs/example_usage.rst | 103 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 4 +- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 docs/example_usage.rst diff --git a/docs/conf.py b/docs/conf.py index 4044f4e..43c7c81 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,6 +41,9 @@ "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"] diff --git a/docs/example_usage.rst b/docs/example_usage.rst new file mode 100644 index 0000000..f1900fd --- /dev/null +++ b/docs/example_usage.rst @@ -0,0 +1,103 @@ + +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. + +.. note:: + These are examples are written to run live in the browser with `Pyodide `_ and `sphinx-pyodide `_. + +.. 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} before {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:: + november_2020 = Undate(2020, 11) + november7_2020 > november_2020 diff --git a/docs/index.rst b/docs/index.rst index 9ad10ca..b1679eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ undate documentation .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: readme @@ -16,6 +16,8 @@ undate documentation CONTRIBUTORS LICENSE +.. include:: example_usage.rst + Indices and tables ================== From fe0dc8e3a0ea56c42e9328e3f682132ae0d844a8 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 17:42:10 -0400 Subject: [PATCH 04/14] Convert remaining examples from readme to live pyodide examples Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/example_usage.rst | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index f1900fd..1a9742f 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -101,3 +101,75 @@ methods raise a ``NotImplementedError``. .. pyodide:: november_2020 = Undate(2020, 11) november7_2020 > november_2020 + +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(repr(interval)) + +Intervals can calculate duration if enough information is known: + +.. pyodide:: + for interval in [century19, century20]: + print(f"{interval.label}: {interval.duration().days} days") + print(f"January 2000: {UndateInterval(Undate(2000, 1, 1), Undate(2000, 1, 31)).duration().days} days") + +You can initialize ``Undate`` or ``UndateInterval`` objects by parsing a +date string with a specific converter, and you can also output an +``Undate`` object in those formats. Currently available converters +are "ISO8601" and "EDTF" and supported calendars. + +.. pyodide:: + print(repr(Undate.parse("2002", "ISO8601"))) + print(repr(Undate.parse("2002-05", "EDTF"))) + print(repr(Undate.parse("--05-03", "ISO8601"))) + print(Undate.parse("--05-03", "ISO8601").format("EDTF")) + print(repr(Undate.parse("1800/1900", format="EDTF"))) + + +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(repr(d)) + +.. pyodide:: + print("Earliest Gregorian equivalent:") + for d in [rajab495, tammuz4816, y2k]: + print(f" {str(d):<30} earliest: {d.earliest}") + + print("\nPrecision:") + for d in [rajab495, tammuz4816, y2k]: + print(f" {str(d):<30} precision: {d.precision}") + +.. pyodide:: + print("Sorted by Gregorian date:") + for d in sorted([rajab495, tammuz4816, y2k]): + print(f" {repr(d)}") From f4fc9d66fac6f8509dde1647b9d6dfa140f9c405 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 17:56:19 -0400 Subject: [PATCH 05/14] Revise examples ported from readme --- docs/example_usage.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index 1a9742f..4ae0390 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -99,6 +99,7 @@ When dates cannot be compared due to ambiguity or precision, comparison methods raise a ``NotImplementedError``. .. pyodide:: + :show-errors: november_2020 = Undate(2020, 11) november7_2020 > november_2020 @@ -117,14 +118,18 @@ and latest date as part of the range. after1900 = UndateInterval(Undate(1900)) # after 1900 for interval in [century19, century20, before2000, after1900]: - print(repr(interval)) + print(f"{repr(interval)}\n{interval}\n") Intervals can calculate duration if enough information is known: .. pyodide:: - for interval in [century19, century20]: - print(f"{interval.label}: {interval.duration().days} days") - print(f"January 2000: {UndateInterval(Undate(2000, 1, 1), Undate(2000, 1, 31)).duration().days} days") + 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") You can initialize ``Undate`` or ``UndateInterval`` objects by parsing a date string with a specific converter, and you can also output an @@ -158,16 +163,16 @@ comparison across dates from different calendars. y2k = Undate.parse("2001", "EDTF") for d in [tammuz4816, rajab495, y2k]: - print(repr(d)) + print(f"{d!s:<10} {repr(d)}") .. pyodide:: print("Earliest Gregorian equivalent:") for d in [rajab495, tammuz4816, y2k]: - print(f" {str(d):<30} earliest: {d.earliest}") + print(f" {str(d):<20} earliest: {d.earliest}") print("\nPrecision:") for d in [rajab495, tammuz4816, y2k]: - print(f" {str(d):<30} precision: {d.precision}") + print(f" {str(d):<20} precision: {d.precision}") .. pyodide:: print("Sorted by Gregorian date:") From f3300e6fd23e1a6c32e63056cccd7a11cd7ab4fd Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 18:05:14 -0400 Subject: [PATCH 06/14] Clean up examples and highlight new features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed output consistency — all calls use repr(), with the format conversion separated into a clear comment - Added "Parsing and Formatting" section heading (same ~~~~~ level as "Calendars") - Added Gregorian examples — French and Kinyarwanda to illustrate the multilingual support - Added holidays examples — Epiphany (fixed), Easter and Ash Wednesday (movable feasts) Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/example_usage.rst | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index 4ae0390..c31e986 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -100,6 +100,7 @@ methods raise a ``NotImplementedError``. .. pyodide:: :show-errors: + november_2020 = Undate(2020, 11) november7_2020 > november_2020 @@ -131,17 +132,37 @@ Intervals can calculate duration if enough information is known: 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 specific converter, and you can also output an -``Undate`` object in those formats. Currently available converters -are "ISO8601" and "EDTF" and supported calendars. +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")) - print(repr(Undate.parse("1800/1900", format="EDTF"))) + +The ``"Gregorian"`` converter parses dates with full or abbreviated month +names across multiple languages: + +.. pyodide:: + print(repr(Undate.parse("avril 1362", "Gregorian"))) # French + print(repr(Undate.parse("2022 Ugushyingo 26", "Gregorian"))) # Kinyarwanda + +The ``"holidays"`` converter parses Christian liturgical dates, including +movable feasts: + +.. pyodide:: + print(repr(Undate.parse("Epiphany 1942", "holidays"))) + print(repr(Undate.parse("Easter 1942", "holidays"))) + print(repr(Undate.parse("Ash Wednesday 1942", "holidays"))) Calendars From 59b5f409f9762fad38a505477be07ae89616c6c8 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 18:27:24 -0400 Subject: [PATCH 07/14] Add editable examples & make them nicer to play with --- docs/example_usage.rst | 47 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index c31e986..fa2de58 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -102,7 +102,10 @@ methods raise a ``NotImplementedError``. :show-errors: november_2020 = Undate(2020, 11) - november7_2020 > november_2020 + 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 @@ -153,16 +156,35 @@ The ``"Gregorian"`` converter parses dates with full or abbreviated month names across multiple languages: .. pyodide:: - print(repr(Undate.parse("avril 1362", "Gregorian"))) # French - print(repr(Undate.parse("2022 Ugushyingo 26", "Gregorian"))) # Kinyarwanda + :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:: - print(repr(Undate.parse("Epiphany 1942", "holidays"))) - print(repr(Undate.parse("Easter 1942", "holidays"))) - print(repr(Undate.parse("Ash Wednesday 1942", "holidays"))) + :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 @@ -199,3 +221,16 @@ comparison across dates from different calendars. 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) From d597eacf8293411dc453d0cc719a0b85d5599c38 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 19:23:30 -0400 Subject: [PATCH 08/14] Restructure docs to provide brief overview; move examples to new page Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/example_usage.rst | 4 --- docs/index.rst | 82 ++++++++++++++++++++++++++++++++++++++---- docs/readme.md | 4 --- 3 files changed, 75 insertions(+), 15 deletions(-) delete mode 100644 docs/readme.md diff --git a/docs/example_usage.rst b/docs/example_usage.rst index fa2de58..8db176b 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -1,4 +1,3 @@ - Example Usage -------------- @@ -6,9 +5,6 @@ Often humanities and cultural data include imprecise or uncertain temporal infor 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. -.. note:: - These are examples are written to run live in the browser with `Pyodide `_ and `sphinx-pyodide `_. - .. pyodide:: :packages: ./wheels/PyMeeus-0.5.12-py3-none-any.whl,./wheels/undate-0.8.0.dev0-py3-none-any.whl diff --git a/docs/index.rst b/docs/index.rst index b1679eb..4341493 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,91 @@ -undate documentation -==================== +undate +====== -**undate** is a python library for working with uncertain or partially known dates. +.. image:: _static/undate_logo.png + :alt: undate +**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: 1 :caption: Contents: - readme - pyodide + example_usage undate/index CONTRIBUTING DEVELOPER_NOTES CONTRIBUTORS LICENSE -.. include:: example_usage.rst - Indices and tables ================== 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: -``` From fdd26f103059ce58405dd709da6d1a540861fda5 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 20:00:31 -0400 Subject: [PATCH 09/14] Add sphinx-pyodide to dependencies --- pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 02aae48..4c72439 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,13 @@ classifiers = [ ] [dependency-groups] -docs = ["sphinx>=7.0.0", "furo", "myst-parser", "myst-parser[linkify]"] +docs = [ + "sphinx>=7.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"] From 04bc5a9d0f5fa337d344707e46b050b55c692fec Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 20:05:37 -0400 Subject: [PATCH 10/14] Remove sphinx-pyodide from dev deps --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4c72439..69a4d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ classifiers = [ [dependency-groups] docs = [ - "sphinx>=7.0.0", + "sphinx>=8.0.0", "furo", "myst-parser", "myst-parser[linkify]", @@ -67,7 +67,6 @@ dev = [ "wheel", "build", "sphinx-autobuild>=2024.10.3", - "sphinx-pyodide", { include-group = "test" }, { include-group = "check" }, { include-group = "docs" }, From 885605f55f2ab57f27264acb343aba653b3f05de Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 10:43:07 -0400 Subject: [PATCH 11/14] Fix py dep caching discrepancy - everything uv Assisted-by: ClaudeCode:claude-sonnet-4-6 --- .github/workflows/check.yml | 2 +- .github/workflows/unit_tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 From 92450b041ce458cbce67269cb3a692cf0b2d47c6 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 10:46:26 -0400 Subject: [PATCH 12/14] Switch publish workflow to uv as well Assisted-by: ClaudeCode:claude-sonnet-4-6 --- .github/workflows/python-publish.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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: From 4a78125873fc33663632ce0af561a5aaba21ee4e Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 10:56:15 -0400 Subject: [PATCH 13/14] Comment out local path used for sphinx-pyodide dev --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69a4d82..23fb665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,5 +130,6 @@ ignore = [] # for test files, don't require docstrings or return type annotations "tests/**.py" = ["D", "ANN", "RUF"] -[tool.uv.sources] -sphinx-pyodide = { path = "../sphinx-pyodide", editable = true } +# uncomment for local development of sphinx-pyodide +#[tool.uv.sources] +#sphinx-pyodide = { path = "../sphinx-pyodide", editable = true } From 7e2f703443f2a81dc30fbab99234b7de755d24b0 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 11:03:02 -0400 Subject: [PATCH 14/14] Use light/dark mode logo for docs index Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/index.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 4341493..294c40d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,15 @@ undate ====== -.. image:: _static/undate_logo.png +.. 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.