[Datetime-SIG] Timeline arithmetic?

Tim Peters tim.peters at gmail.com
Fri Sep 11 04:41:59 CEST 2015


It's become beyond obvious that I'll never be able to make enough time
to respond to all of these, so I'll address just this for now. because
it's impossible to make progress on anything unless there's agreement
on what technical terms mean:


[Carl Meyer <carl at oddbird.net>]
>>> If you are doing any kind of "integer arithmetic on POSIX timestamps", you
>>> are _always_ doing timeline arithmetic.

[Tim]
>> True.

[Carl]
>>> Classic arithmetic may be many things, but the one thing it definitively is
>>> _not_ is "arithmetic on POSIX timestamps."

[Tim]
>> False.  UTC is an eternally-fixed-offset zone.  There are no
>> transitions to be accounted for in UTC.  Classic and timeline
>> arithmetic are exactly the same thing in any eternally-fixed-offset
>> zone.  Because POSIX timestamps _are_ "in UTC", any arithmetic
>> performed on one is being done in UTC too.  Your illustration next
>> goes way beyond anything I could possibly read as doing arithmetic on
>> POSIX timestamps:

[Carl]
> Translation: "I refuse to countenance the possibility of Model A."

Not at all.  I've tried several times to get it across in English, so
this time I'll try code instead:

    def dt_add(dt, td, timeline=False):
        ofs = dt.utcoffset()
        as_utc = dt.replace(tzinfo=timezone.utc)

        # and the following is identical to converting to
        # a timestamp, "using POSIX timestamp arithmetic",
        # then converting back to calendar notation
        as_utc -= ofs
        as_utc += td

        if timeline:
            return as_utc.astimezone(dt.tzinfo)
        else: # classic
            return (as_utc + ofs).replace(tzinfo=dt.tzinfo)

That adds an aware datetime to a timedelta, doing either classic or
timeline arithmetic depending on the optional flag.  If you want to
claim this doesn't do either kind of arithmetic correctly, prove it
with a specific example (of course cases where it's impossible to do
_conversions_ correctly today would be off-point).  Here's a variant
of an earlier specific example:

    from datetime import datetime, timedelta, timezone
    from pytz.reference import Eastern

    turkey_in = datetime(2004, 10, 30, 15, tzinfo=Eastern)
    DAY = timedelta(days=1)
    turkey_out1 = dt_add(turkey_in, DAY, timeline=True)
    turkey_out2 = dt_add(turkey_in, DAY, timeline=False)
    print(turkey_in)
    print(turkey_out1)
    print(turkey_out2)

and its output:

    2004-10-30 15:00:00-04:00  # start
    2004-10-31 14:00:00-05:00  # "a day later" in timeline
    2004-10-31 15:00:00-05:00  # "a day later" in classic

"Timeline" arithmetic accounts for that an hour was inserted when DST
ended, and "classic" does not.

The "POSIX timestamp arithmetic" part is identical across both cases.
The only difference is in how the POSIX timestamp - which is always
and only a count of seconds in UTC (which isn't my definition - it's
POSIX's) - is converted back to local calendar notation at the very
end.

I believe you have _pictured_ the POSIX timestamp number line
annotated with local calendar notations in your head, but those labels
have nothing to do with the timestamp arithmetic.  The labels have
only to do with the functions used to map local calendar notations to
and from POSIX timestamps.  Those labelings are the difference between
"timeline" and "classic" arithmetic at the higher level of aware
datetime arithmetic.  At the POSIX timestamp level, an integer is just
an integer, with no defined meaning of any kind beyond a count of
seconds in UTC. and a POSIX-defined mapping to and from propleptic
Gregorian calendar notation.

That said, two things to note:

1. The "as_utc -= ofs" line is theoretically impure, because it's
treating a local time _as if_ it were a UTC time.  There's no real way
around that.  We have to convert from local to UTC _somehow_, and
POSIX dodges the issue by providing mktime() to do that "by magic".
Here we're _inside_ the sausage factory, doing it ourselves.  Some rat
guts are visible at this level.  If you look inside a C mktime()
implementation, you'll find rat guts all over that too.

But it's no problem for Guido ;-)  We just set the hands on a UTC
clock to match the local clock, then move the hands on the UTC clock
by the amount the local clock is "ahead of" or "behind" UTC.  In that
way you can indeed picture the operation as being entirely "in UTC".

2. This would be a foolish _implementation_ of classic arithmetic, but
not for semantic reasons.  It's just grossly inefficient.  Stare at
the code, and in the classic case it subtracts the UTC offset at first
only to add the same offset back later.  Those cancel out, so there's
no _semantic_ need to do either..  It's only excessive concern for
theoretical purity that could stop one from spelling it as

    return dt + td

from the start.  That's technically absurd, since it's doing POSIX
timestamp arithmetic on a timestamp that's _not_ a UTC seconds count.
Its only virtue is that it gets the same answer far faster ;-)

BTW, the same kind of reasoning shows why the value of the `timeline=`
flag makes no difference in any case a fixed-offset zone is being
used.  Which is, concretely, what I mean by saying that timeline and
classic arithmetic are exactly the same thing in any fixed-offset
zone.


More information about the Datetime-SIG mailing list