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/README.md b/README.md index 6eacf95..28fa2db 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,17 @@ Currently `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) +- 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 dates (bare years are currently assumed to be Gregorian calendar). 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](https://doi.org/10.1017/chr.2025.10006).” _Computational Humanities Research_, August 5, 2025. - +> Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford. "[Undate: Humanistic Dates for Computation](https://doi.org/10.1017/chr.2025.10006)." _Computational Humanities Research_, August 5, 2025. --- @@ -31,8 +30,6 @@ For more about the origin and goals of `undate`, read our 2025 software paper: [![codecov](https://codecov.io/gh/dh-tech/undate-python/branch/main/graph/badge.svg?token=GE7HZE8C9D)](https://codecov.io/gh/dh-tech/undate-python) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -Project documentation is [available on ReadTheDocs](https://undate-python.readthedocs.io/en/latest/). - [![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](CONTRIBUTORS.md) @@ -54,198 +51,26 @@ Use the `@name` notation to specify the branch or tag; e.g., to install developm pip install git+https://github.com/dh-tech/undate-python@develop#egg=undate ``` -## Example Usage +## Quick Start -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. +An `Undate` is analogous to python’s `datetime.date` but supports varying degrees of precision and unknown information. Initialize with strings or numbers for whichever parts of the date are known: ```python -from undate import Undate +from undate import Undate, UndateInterval 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): - -```python ->>> [str(d) for d in [november7, november, year2k, november7_some_year]] -['2000-11-07', '2000-11', '2000', '--11-07'] -``` - -If enough information is known, an `Undate` object can report on its duration: - -```python ->>> december = Undate(2000, 12) ->>> feb_leapyear = Undate(2024, 2) ->>> feb_regularyear = Undate(2023, 2) ->>> for d in [november7, november, december, year2k, november7_some_year, feb_regularyear, feb_leapyear]: -... print(f"{d} - duration in days: {d.duration().days}") -... -2000-11-07 - duration in days: 1 -2000-11 - duration in days: 30 -2000-12 - duration in days: 31 -2000 - duration in days: 366 ---11-07 - duration in days: 1 -2023-02 - duration in days: 28 -2024-02 - duration in days: 29 -``` - -If enough of the date is known and the precision supports it, you can -check if one date falls within another date: - -```python ->>> november7 = Undate(2000, 11, 7) ->>> november2000 = Undate(2000, 11) ->>> year2k = Undate(2000) ->>> ad100 = Undate(100) ->>> november7 in november -True ->>> november2000 in year2k -True ->>> november7 in year2k -True ->>> november2000 in ad100 -False ->>> november7 in ad100 -False -``` -For dates that are imprecise or partially known, `undate` calculates -earliest and latest possible dates for comparison purposes so you can -sort dates and compare with equals, greater than, and less than. You -can also compare with python `datetime.date` objects. - -```python ->>> november7_2020 = Undate(2020, 11, 7) ->>> november_2001 = Undate(2001, 11) ->>> year2k = Undate(2000) ->>> ad100 = Undate(100) ->>> sorted([november7_2020, november_2001, year2k, ad100]) -[undate.Undate(year=100, calendar="Gregorian"), undate.Undate(year=2000, calendar="Gregorian"), undate.Undate(year=2001, month=11, calendar="Gregorian"), undate.Undate(year=2020, month=11, day=7, calendar="Gregorian")] ->>> november7_2020 > november_2001 -True ->>> year2k < ad100 -False ->>> from datetime import date ->>> year2k > date(2001, 1, 1) -False -``` - -When dates cannot be compared due to ambiguity or precision, comparison -methods raise a `NotImplementedError`. - -```python ->>> november_2020 = Undate(2020, 11) ->>> november7_2020 > november_2020 -Traceback (most recent call last): - File "", line 1, in - File "/Users/rkoeser/workarea/github/undate-python/src/undate/undate.py", line 262, in __gt__ - return not (self < other or self == other) - File "/Users/rkoeser/workarea/github/undate-python/src/undate/undate.py", line 245, in __lt__ - raise NotImplementedError( -NotImplementedError: Can't compare when one date falls within the other -``` +[str(d) for d in [november7, november, year2k, partially_known_year]] +# [‘2000-11-07’, ‘2000-11’, ‘2000’, ‘19XX’] -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. - -```python ->>> from undate import UndateInterval ->>> UndateInterval(Undate(1900), Undate(2000)) -undate.UndateInterval(earliest=undate.Undate(year=1900, calendar="Gregorian"), latest=undate.Undate(year=2000, calendar="Gregorian")) ->>> UndateInterval(Undate(1801), Undate(1900), label="19th century") -undate.UndateInterval(earliest=undate.Undate(year=1801, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian"), label="19th century") ->>> UndateInterval(Undate(1801), Undate(1900), label="19th century").duration().days -36524 ->>> UndateInterval(Undate(1901), Undate(2000), label="20th century") -undate.UndateInterval(earliest=undate.Undate(year=1901, calendar="Gregorian"), latest=undate.Undate(year=2000, calendar="Gregorian"), label="20th century") ->>> UndateInterval(latest=Undate(2000)) # before 2000 -undate.UndateInterval(latest=undate.Undate(year=2000, calendar="Gregorian")) ->>> UndateInterval(Undate(1900)) # after 1900 -undate.UndateInterval(earliest=undate.Undate(year=1900, calendar="Gregorian")) ->>> UndateInterval(Undate(1900), Undate(2000), label="19th century").duration().days -36890 ->>> UndateInterval(Undate(2000, 1, 1), Undate(2000, 1,31)).duration().days -31 +Undate.parse("Rajab 495", "Islamic") +# undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic") ``` -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. - -```python ->>> from undate import Undate ->>> Undate.parse("2002", "ISO8601") -undate.Undate(year=2002, calendar="Gregorian") ->>> Undate.parse("2002-05", "EDTF") -undate.Undate(year=2002, month=5, calendar="Gregorian") ->>> Undate.parse("--05-03", "ISO8601") -undate.Undate(month=5, day=3, calendar="Gregorian") ->>> Undate.parse("--05-03", "ISO8601").format("EDTF") -'XXXX-05-03' ->>> Undate.parse("1800/1900", format="EDTF") -undate.UndateInterval(earliest=undate.Undate(year=1800, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian")) -``` - -### 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 based on -calendar conversion logic implemented in the -[convertdate](https://convertdate.readthedocs.io/en/latest/) package. - -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. - -```python ->>> from undate import Undate ->>> tammuz4816 = Undate.parse("26 Tammuz 4816", "Hebrew") ->>> tammuz4816 -undate.Undate(year=4816, month=4, day=26, label="26 Tammuz 4816 Anno Mundi", calendar="Hebrew") ->>> rajab495 = Undate.parse("Rajab 495", "Islamic") ->>> rajab495 -undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic") ->>> y2k = Undate.parse("2001", "EDTF") ->>> y2k -undate.Undate(year=2001, calendar="Gregorian") ->>> [str(d.earliest) for d in [rajab495, tammuz4816, y2k]] -['1102-04-28', '1056-07-17', '2001-01-01'] ->>> [str(d.precision) for d in [rajab495, tammuz4816, y2k]] -['MONTH', 'DAY', 'YEAR'] ->>> sorted([rajab495, tammuz4816, y2k]) -[undate.Undate(year=4816, month=4, day=26, label="26 Tammuz 4816 Anno Mundi", calendar="Hebrew"), undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic"), undate.Undate(year=2001, calendar="Gregorian")] -``` - ---- - -For more examples, refer to the code notebooks included in the -[examples](https://github.com/dh-tech/undate-python/tree/main/examples/) -directory in this repository. +For full examples including duration, comparison, intervals, parsing, and calendar support, see the [interactive documentation](https://undate-python.readthedocs.io/en/latest/). ## Documentation 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 }