.. Licensed to the Apache Software Foundation (ASF) under one
.. or more contributor license agreements.  See the NOTICE file
.. distributed with this work for additional information
.. regarding copyright ownership.  The ASF licenses this file
.. to you under the Apache License, Version 2.0 (the
.. "License"); you may not use this file except in compliance
.. with the License.  You may obtain a copy of the License at

..   http://www.apache.org/licenses/LICENSE-2.0

.. Unless required by applicable law or agreed to in writing,
.. software distributed under the License is distributed on an
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
.. KIND, either express or implied.  See the License for the
.. specific language governing permissions and limitations
.. under the License.

**********
Timestamps
**********

Arrow/Pandas Timestamps
=======================

Arrow timestamps are stored as a 64-bit integer with column metadata to
associate a time unit (e.g. milliseconds, microseconds, or nanoseconds), and an
optional time zone.  Pandas (``Timestamp``) uses a 64-bit integer representing
nanoseconds and an optional time zone.
Python/Pandas timestamp types without a associated time zone are referred to as
"Time Zone Naive".  Python/Pandas timestamp types with an associated time zone are
referred to as "Time Zone Aware".


Timestamp Conversions
=====================

Pandas/Arrow ⇄ Spark
--------------------

Spark stores timestamps as 64-bit integers representing microseconds since
the UNIX epoch.  It does not store any metadata about time zones with its
timestamps.

Spark interprets timestamps with the *session local time zone*, (i.e.
``spark.sql.session.timeZone``). If that time zone is undefined, Spark turns to
the default system time zone. For simplicity's sake below, the session
local time zone is always defined.

This implies a few things when round-tripping timestamps:

#.  Timezone information is lost (all timestamps that result from
    converting from spark to arrow/pandas are "time zone naive").
#.  Timestamps are truncated to microseconds.
#.  The session time zone might have unintuitive impacts on
    translation of timestamp values.

Spark to Pandas (through Apache Arrow)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following cases assume the Spark configuration
``spark.sql.execution.arrow.enabled`` is set to ``"true"``.

::

    >>> pdf = pd.DataFrame({'naive': [datetime(2019, 1, 1, 0)],
    ...                     'aware': [Timestamp(year=2019, month=1, day=1,
    ...                               nanosecond=500, tz=timezone(timedelta(hours=-8)))]})
    >>> pdf
           naive                               aware
           0 2019-01-01 2019-01-01 00:00:00.000000500-08:00

    >>> spark.conf.set("spark.sql.session.timeZone", "UTC")
    >>> utc_df = sqlContext.createDataFrame(pdf)
    >>> utf_df.show()
    +-------------------+-------------------+
    |              naive|              aware|
    +-------------------+-------------------+
    |2019-01-01 00:00:00|2019-01-01 08:00:00|
    +-------------------+-------------------+

Note that conversion of the aware timestamp is shifted to reflect the time
assuming UTC (it represents the same instant in time).  For naive
timestamps, Spark treats them as being in the system local
time zone and converts them UTC. Recall that internally, the schema
for spark dataframe's does not store any time zone information with
timestamps.

Now if the session time zone is set to US Pacific Time (PST) we don't
see any shift in the display of the aware time zone (it
still represents the same instant in time):

::

    >>> spark.conf.set("spark.sql.session.timeZone", "US/Pacific")
    >>> pst_df = sqlContext.createDataFrame(pdf)
    >>> pst_df.show()
    +-------------------+-------------------+
    |              naive|              aware|
    +-------------------+-------------------+
    |2019-01-01 00:00:00|2019-01-01 00:00:00|
    +-------------------+-------------------+

Looking again at utc_df.show() we see one of the impacts of the session time
zone.  The naive timestamp was initially converted assuming UTC, the instant it
reflects is actually earlier than the naive time zone from the PST converted
data frame:

::

    >>> utc_df.show()
    +-------------------+-------------------+
    |              naive|              aware|
    +-------------------+-------------------+
    |2018-12-31 16:00:00|2019-01-01 00:00:00|
    +-------------------+-------------------+

Spark to Pandas
~~~~~~~~~~~~~~~

We can observe what happens when converting back to Arrow/Pandas.  Assuming the
session time zone is still PST:

::

   >>> pst_df.show()
   +-------------------+-------------------+
   |              naive|              aware|
   +-------------------+-------------------+
   |2019-01-01 00:00:00|2019-01-01 00:00:00|
   +-------------------+-------------------+


    >>> pst_df.toPandas()
    naive      aware
    0 2019-01-01 2019-01-01
    >>> pst_df.toPandas().info()
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 1 entries, 0 to 0
    Data columns (total 2 columns):
    naive    1 non-null datetime64[ns]
    aware    1 non-null datetime64[ns]
    dtypes: datetime64[ns](2)
    memory usage: 96.0 bytes

Notice that, in addition to being a "time zone naive" timestamp, the 'aware'
value will now differ when converting to an epoch offset.  Spark does the conversion
by first converting to the session time zone (or system local time zone if
session time zones isn't set) and then localizes to remove the time zone
information.  This results in the timestamp being 8 hours before the original
time:

::

  >>> pst_df.toPandas()['aware'][0]
  Timestamp('2019-01-01 00:00:00')
  >>> pdf['aware'][0]
  Timestamp('2019-01-01 00:00:00.000000500-0800', tz='UTC-08:00')
  >>> (pst_df.toPandas()['aware'][0].timestamp()-pdf['aware'][0].timestamp())/3600
  -8.0

The same type of conversion happens with the data frame converted while
the session time zone was UTC.  In this case both naive and aware
represent different instants in time (the naive instant is due to
the change in session time zone between creating data frames):

::

  >>> utc_df.show()
  +-------------------+-------------------+
  |              naive|              aware|
  +-------------------+-------------------+
  |2018-12-31 16:00:00|2019-01-01 00:00:00|
  +-------------------+-------------------+

  >>> utc_df.toPandas()
  naive      aware
  0 2018-12-31 16:00:00 2019-01-01

Note that the surprising shift for aware doesn't happen
when the session time zone is UTC (but the timestamps
still become "time zone naive"):

::

  >>> spark.conf.set("spark.sql.session.timeZone", "UTC")
  >>> pst_df.show()
  +-------------------+-------------------+
  |              naive|              aware|
  +-------------------+-------------------+
  |2019-01-01 08:00:00|2019-01-01 08:00:00|
  +-------------------+-------------------+

  >>> pst_df.toPandas()['aware'][0]
  Timestamp('2019-01-01 08:00:00')
  >>> pdf['aware'][0]
  Timestamp('2019-01-01 00:00:00.000000500-0800', tz='UTC-08:00')
  >>> (pst_df.toPandas()['aware'][0].timestamp()-pdf['aware'][0].timestamp())/3600
  0.0
