Partial date values#

A PartialDate is a date in Python code, known to the year, the month or the day. It holds a year and, when they were recorded, a month and a day. It is immutable, so a value never changes once it is built. This page shows how to build one, read its parts, compare it with another and ask which period it covers.

Build a value#

Give the parts you know. A part that was not recorded is None.

>>> from partial_dates import PartialDate, Precision
>>> PartialDate(1990)
PartialDate(year=1990, month=None, day=None)
>>> PartialDate(1990, 6, 14)
PartialDate(year=1990, month=6, day=14)

Read and write the ISO form#

PartialDate.fromisoformat reads the three ISO forms, YYYY, YYYY-MM and YYYY-MM-DD, and nothing else. isoformat() writes the form back, and reading it gives an equal value.

>>> value = PartialDate.fromisoformat("1990-06")
>>> value == PartialDate(1990, 6)
True
>>> value.isoformat()
'1990-06'
>>> str(PartialDate(990))
'0990'

To show a date to a reader, use display_form(). Showing a partial date covers it.

Find out how much was recorded#

The precision of a value says how much of it was recorded, and the parts are attributes.

>>> value.year, value.month, value.day
(1990, 6, None)
>>> value.precision is Precision.MONTH
True

Handle an invalid date#

Building or reading something that is not a date, such as a day without a month, 1990-02-30 or a year outside 0001 to 9999, raises ValueError.

>>> PartialDate.fromisoformat("1990-6")
Traceback (most recent call last):
    ...
ValueError: Not a partial date in ISO form: '1990-6'

Sort and compare values#

Values sort in the order of their ISO forms, so a year comes before its months and a month before its days. Two values are equal when all their parts are equal: 1990 is not 1990-01-01.

>>> values = [
...     PartialDate(1991),
...     PartialDate(1990, 6, 14),
...     PartialDate(1990),
...     PartialDate(1990, 6),
... ]
>>> [value.isoformat() for value in sorted(values)]
['1990', '1990-06', '1990-06-14', '1991']
>>> PartialDate(1990) == PartialDate(1990, 1, 1)
False

A value is never equal to a datetime.date, a string or an integer, and ordering against one raises TypeError.

Use a value as a key#

Values are hashable, so they work as set members and dictionary keys.

>>> len({PartialDate(1990, 6), PartialDate.fromisoformat("1990-06")})
1
>>> counts = {PartialDate(1990, 6): 3}
>>> counts[PartialDate.fromisoformat("1990-06")]
3

Get the period a value covers#

start and end give the first and last day of the period as datetime.date objects, with the last day of February worked out for the year.

>>> PartialDate(1990).start, PartialDate(1990).end
(datetime.date(1990, 1, 1), datetime.date(1990, 12, 31))
>>> PartialDate(2000, 2).end
datetime.date(2000, 2, 29)
>>> PartialDate(1990, 6, 14).start == PartialDate(1990, 6, 14).end
True

Find out whether one value is earlier than another#

relation_to says whether the period of one value is Relation.BEFORE, Relation.AFTER or Relation.OVERLAPPING the period of another. Passing anything but a PartialDate raises TypeError.

>>> from partial_dates import Relation
>>> PartialDate(1990, 6).relation_to(PartialDate(1991)) is Relation.BEFORE
True
>>> PartialDate(1991).relation_to(PartialDate(1990, 6)) is Relation.AFTER
True
>>> PartialDate(1990).relation_to(PartialDate(1990, 6)) is Relation.OVERLAPPING
True
>>> PartialDate(1990) < PartialDate(1990, 6)
True

Why sorting does not say which date is earlier#

Sorting and “is this one earlier?” are different questions. 1990 sorts before 1990-06, but the two overlap, so neither is earlier. The last example above shows both answers side by side.