[Datetime-SIG] Fwd: Calendar vs timespan calculations...

Alexander Belopolsky alexander.belopolsky at gmail.com
Tue Aug 4 20:33:37 CEST 2015

[Tim Peters]
>> TAI
>> is an instance of naive time, that's the obvious choice.  So add:
>> 1.5.  Use the frickin' leap second table to convert to TAI.
>> and, in 2, s/UTC/TAI/, and
>> 2.5  Use the frickin' leap second table to convert back to UTC.
[Chris Barker]
> Exactly. So for now, we simply document that Naive time IS TAI time,
> and that the to-from UTC methods on the tzinfo objects are actually
> to-from TAI ( renaming them is probably out of the question, though)

I this is not the POV that I've been advocating.  Since the subject of
this thread is "Calendar vs timespan calculations", let me discuss a
use case that is hopefully more familiar: compute the number of days
from 1900-02-01 to 1900-03-01:

>>> date(1900,3,1)  - date(1900,2,1)

Python's answer (28) is correct in the most of the Western world, but
in Greece, there were 29 days from 1900-02-01 to 1900-03-01 because
they did have 1900-02-29.  (Greece did not switch to the Gregorian
calendar until 1923.)

Note that "1900-02-01" and "1900-03-01" are just names for two
historical days which had different meanings in different parts of the
world.  The digits that appear in those names do not necessarily mean
anything arithmetically.  The first day of February does not have to
be in a constant relationship to the first day of  March any more than
the first day of Hanukkah  to the first day of Ramadan.

While it is convenient to have a universal bijection between days and
the (additive group of) integers, it is not a prerequisite for being
able to compute the number of days between two dates.  For example,
the hypothetical

>>> julian_days("1900-02-01", "1900-03-01")
>>> gregorian_days("1900-02-01", "1900-03-01")

can be implemented without any conversion to the common scale.  (For
example, we can have a large list of julian days from "1000-01-01" to
"9000-01-01" and another large list of gregorian days from
"0006-06-06" to "9999-09-09" and implement our functions using a
binary search into these lists.   Not an efficient algorithm, but
universal enough to cover calendars that use names of the living
Emperors instead of years.)

My main point is that as long as we can spell two dates in a way that
is understood in some part of the world, we can have a software module
that can tell the number of days (or seconds) between these two dates
as long as it has accurate enough information about timekeeping
practices in that location.  This software module may or may not
operate by converting to any well-known integer scale.

My specific proposal can be summarized in the following pseudocode:

class datetime:
    def __add__(self, other):
            add = self.tzinfo.add
        except AttributeError:
            # old logic
            return add(self, other)

    def __sub__(self, other):
            sub = self.tzinfo.sub
        except AttributeError:
            # old logic
            return sub(self, other)

If we do that, we can implement a HistoricalGreek timezone, so that

>>> datetime(1900,3,1,tzinfo=HistoricalGreek)  - date(1900,2,1,tzinfo=HistoricalGreek)

but we will face the problem
datetime(1900,2,29,tzinfo=HistoricalGreek) will still raise
"ValueError: ('day must be in 1..28', 29)".  This problem can be
solved by implementing HistoricalGreek.add so that

>>> datetime(1900,2,28,tzinfo=HistoricalGreek) + timedelta(1)
datetime .datetime(1900,2,29,tzinfo=HistoricalGreek,first=False)

Now, if we also want "next day after 1900-02-28 in Greece" to print
nicely as "1900-02-29", we should also arrange that isoformat() is
also delegated to HistoricalGreek:

    def isoformat(self, other):
            fmt = self.tzinfo.isoformat
        except AttributeError:
            # old logic
            return fmt(self, other)

and HistoricalGreek.isoformat will then know that what comes as the
"repeated February 28" is the "February 29" in disguise.

The only tricky issue here is the mixed timezone arithmetics.  My
solution is to disallow subtraction of datetime instances that both
have tzinfo implement the "sub" method, but not the same:

    def __sub__(self, other):
            sub = self.tzinfo.sub
                other_sub = other.tzinfo.sub
            except AttributeError:
                other_sub = sub
        except AttributeError:
            # old logic
            if sub.__func__ is not other_sub.__func__:
                raise ValueError("Incompatible calendars")
            return sub(self, other)

More information about the Datetime-SIG mailing list