Python - Our key to Efficiency

mxDateTime - Date/time types for Python

Introduction

    These types were created to provide a consistent way of transferring date and time data between Python and databases. Apart from handling date before the Unix epoch (1.1.1970) they also correctly work with dates beyond the Unix time limit (currently with Unix time values being encoded using 32bit integers, the limit is reached in 2038) and thus is Year 2000 and Year 2038 safe.

    The primary absolute date/time type DateTime uses the following internal format:

    Absolute date
    This is a C long defined as being the number of days in the Gregorian calendar since the day before January 1 in the year 1 (0001-01-01, the Christian Epoch (CE)), thus the Gregorian date 0001-01-01 corresponds to absolute date 1. Note that the Julian Epoch lies two days before the Gregorian one.

    Absolute time
    This is a C double defined as the number of seconds since midnight (0:00:00.00) of the day expressed by the above value.

    The Epoch used by the module is January 1st of the year 1 at midnight (0:00:00.00) in the Gregorian calendar. This date corresponds to absolute day 1 and absolute time 0. Dates before the Epoch are handled by extrapolating the calendars using negative years as basis (the year 1 BCE corresponds to the year 0, 2 BCE is represented as year -1 and so on).

    For the purpose of storing absolute time differences, the package provides a second type called DateTimeDelta. The internal representation for this type is seconds and stored in a signed C double.

    To handle relative time deltas a third object type is available: RelativeDateTime. This object is currently implemented in Python and may be used to store relative time deltas (see below for an exact description). It's main purpose is providing an intuitive way to calculate e.g. the "first of next month".

    Designing the types wasn't as easy as expected, since many criteria had to be taken into account. Here are some of them and their implementation:

    Time Zones, Daylight Savings Time (DST) and Leap Seconds

    Time zones are among the most difficult to handle issues when it comes to implementing and using types for date and time. We chose to move the time zone handling functionality out of the C implementation and into Python. This means that the types know nothing about the time zones of the values they store and calculations are done using the raw data.

    If you need to store and use these informations in calculations, you can "subclass" the types to implement your ideas rather than having to stick to what the C implementation defines. The included ODMG submodule is an example of how this can be done.

    Leap seconds are not supported either. You can implement classes respecting these by "subclassing" DateTime and DateTimeDelta and then overriding the calculation methods with methods that work on Unix ticks values (provided the underlying C lib knows about leap seconds -- most don't and the POSIX standard even invorces not to use leap seconds).

    Calendars

    The module supports two calendars, the Gregorian (default and needed for most conversions) and the Julian, which is handy for dates prior to the year 1582 when the calendar was revised by Pope Gregory XIII.

    Construction of Julian dates can be done using either the JulianDateTime() constructor or indirect through the .Julian() method of DateTime instances. To check which calendar a DateTime instance uses, query the calendar instance attribute.

    Note that Julian dates output the Julian date through the instances date attributes and broken down values. Not all conversions are available on instances using the Julian calendar. Even though in the Julian calendar days start at noon (12:00:00.0), mxDateTime will use the Gregorian convention of using the date for the period from 00:00:00.0 to 23:59:59.99 of that day. (This may change in future versions, though.)

    Both calendars use mathematical models as basis -- they do not account for the many inaccuracies that occurred during their usage history. For this reason, the .absdate values should be interpreted with care, esp. for dates using the Julian calendar. As a result of the mathematical models, the Epochs in the calendars differ by a few days. This was needed in order to synchronize the calendars in the switching year 1582 (I'm still not 100% sure whether this is correct or not: JulianDate(1,1,1) lies two days before Date(1,1,1)).

    Conversion from and to other formats

    For the purpose of converting the stored values to Unix ticks (number of seconds since the Unix epoch; the C lib also uses this representation) we assume that the values are given in local time. This assumption had to be made because the C lib provides no standard way to convert a broken down date/time value in any other way into a ticks value.

    Conversions to COM dates and tuples are done without any assumption on the time zone. The raw values are used.

    Conversion from other formats to DateTime instances is always done by first calculating the corresponding absolute time and date values (which are also used as basis for calculations).

    Rounding errors

    The internal representation of date/times behaves much like floats do in Python, i.e. rounding errors can occur when doing calculations. There is a special compare function included (cmp()) in the package that allows you to compare two date/time values using a given accuracy, e.g. cmp(date1,date2,0.5) will allow 12:00:00.5 and 12:00:01.0 to compare equal.

    Special care has been taken to prevent these rounding errors from occurring for COM dates. If you create a DateTime instance using a COM date, then the value returned by the .COMDate() method is guaranteed to be exactly the same as the one used for creation. The same is true for creation using absolute time and absolute date and broken down values.

    Immutability

    One other thing to keep in mind when working with DateTime and DateTimeDelta instances is that they are immutable (like tuples). Once an instance is created you can not change its value. Instead, you will have to create a new instance with modified values. The advantage of having immutable objects is that they can be used as dictionary keys.

    UTC and GMT

    UTC (a mix of: Temps Universel Coordonné and Coordinated Universal Time) and GMT (Greenich Mean Time) are two names for more or less the same thing: they both refer to the international universal time which is used throughout the world to coordinate events in time regardless of time zone, day light savings time or other local time alterations. See the Calendar FAQ for more infos.

    The mx.DateTime package uses these two names interchangeably. Sometimes API only refer to one name for simplicity. The name preference (GMT or UTC) is often chosen according to common usage.

    Interaction with other types

    DateTime and DateTimeDelta instances can be compared and hashed, making them compatible to the dictionary implementation Python uses (they can be used as keys). The copy protocol, simple arithmetic and pickleing are also supported (ee below for details).

    String formats

    DateTime and DateTimeDelta instances know how to output themselves as ISO8601-strings. The format is very simple: YYYY-MM-DD HH:MM:SS.ss for DateTime instances and [-][DD:]HH:MM:SS.ss for DateTimeDelta instances (the DD-part (days) is only given if the absolute delta value is greater than 24 hours). Customized conversion to strings can be done using the strftime-methods or the included submodules.

    String parsing is supported through the strptime() constructor which implements a very strict parsing scheme and the included submodules (e.g. ISO and ARPA), which allow a little more freedom.

    Speed and Memory

    Comparing the types to time-module based routines is not really possible, since the used strategies differ. You can compare them to tuple-based date/time classes though: DateTime[Delta] are much faster on creation, use less storage and are faster to convert to the supported other formats than any equivalent tuple-based implementation written in Python.

    Creation of time-module values using time.mktime() is much slower than doing the same thing with DateTime(). The same holds for the reverse conversion (using time.localtime()).

    The storage size of ticks (floats, which the time module uses) is about 1/3 of the size a DateTime instance uses. This is mainly due to the fact that DateTime instances cache the broken down values for fast access.

    To summarize: DateTime[Delta] are faster, but also use more memory than traditional time-module based techniques.

    Background and Sources on the Web

    Here is a small list of links I used as starting points to find some of the date/time related information included in this package:

Interface

Date/Time Arithmetic

    The three objects DateTime, DateTimeDelta and RelativeDateTime can be used to do simple date/time arithmetic. Addition and subtraction are supported and result in the expected results. In addition to handling arithmetic using only the two types, mixed arithmetic with numbers is also understood to a certain extent:

    Argument 1 Argument 2 Result
    DateTime object v DateTime object w
    v - w
    returns a DateTimeDelta object representing the time difference;
    v + w
    is not defined.
    DateTime object v A number w
    v - w
    returns a new DateTime object with a date/time decremented by w days (floats can be used to indicate day fractions);
    v + w
    works accordingly;
    Note: you can use the object oneDay to get similar effects in a more intuitive way.
    v cmp w
    Converts v to Unix ticks and returns the result of comparing the ticks value to the number w. Note: the ticks conversion assumes that the stored value is given in local time. Also note that the comparison will only yield correct results if the DateTime instance is placed on the left of the comparison operator (this is because of the coercion quirks mentioned below).
    DateTime object v DateTimeDelta object w
    v - w
    returns a new DateTimeDelta object with a date/time decremented by w's value;
    v + w
    works accordingly.
    DateTime object v RelativeDateTime object w
    v + w
    returns a new DateTime object with a date/time adjusted according to w's settings;
    v - w
    works accordingly.
    RelativeDateTime object v A number w
    v * w
    returns a new RelativeDateTime object with all deltas multiplied by float(w) (w * v works in the same way);
    v / w
    returns a new RelativeDateTime object with all deltas divided by float(w);
    DateTimeDelta object v DateTime object w

    No operations defined.

    DateTimeDelta object v A number w
    v - w
    returns a new DateTimeDelta object with a time delta value decremented by w seconds (can be given as float to indicate fractions of a second);
    v + w
    works accordingly;
    Note: you can use the object oneSecond to get similar effects in a more intuitive way;
    v * w
    returns a new DateTimeDelta object with a time delta value multiplied by float(w) (w * v works in the same way);
    v / w
    returns a new DateTimeDelta object with a time delta value divided by float(w);
    v cmp w
    Converts v to a signed float representing the delta in seconds and returns the result of comparing the seconds value to the number w.Note that the comparison will only yield correct results if the DateTimeDelta instance is placed on the left of the comparison operator (this is because of the coercion quirks mentioned below).
    DateTimeDelta object v DateTimeDelta object w
    v + w
    returns a new DateTimeDelta object for the sum of the two time deltas ((v+w).seconds == v.seconds + w.seconds);
    v - w
    works accordingly;
    v / w
    returns a float equal to v.seconds / w.seconds.

    Notes:

    Operation and argument order are important because of the different ways arguments are coerced. Use parenthesis to make your intent clear or you will get unwanted results.

    Due to a flaw in the C interface for coercion in the interpreter, it is not possible to do proper handling of mixed type arithmetic for types which don't coerce to a common type (without creating temporary objets all the time). The module uses a workaround, but unfortunately the order of the operands is lost along the way. Under normal circumstances you won't notice this defect, but be warned since e.g. oneDay - 1 == 1 - oneDay, yet oneDay - oneSecond != oneSecond - oneDay.

    Comparing RelativeDateTime instances does not work.

    Adding/Subtracting DateTime instances causes the result to inherit the calendar of the left operand.

Submodules

Examples of Use

    For an example of how to use the two types to develop other date/time classes (e.g. ones that support time zones or other calendars), see the included ODMG module. It defines types similar to those of the ODMG standard.

    Here is a little countdown script:

    #!/usr/local/bin/python -u
    """ Y2000.py - The year 2000 countdown.
    """
    from mx.DateTime import *
    from time import sleep
    
    while 1:
        d = Date(2000,1,1) - now()
        print 'Y2000... time left: %2i days %2i hours '
    	  '%2i minutes %2i seconds\r' % \
    	  (d.day,d.hour,d.minute,d.second),
        sleep(1)

    This snippet demonstrates some of the possible string representations for DateTime instances:

    >>> from mx.DateTime import *
    
    >>> ISO.str(now())
    '1998-06-14 11:08:27+0200'
    
    >>> ARPA.str(now())
    'Sun, 14 Jun 1998 11:08:33 +0200'
    
    >>> now().strftime()
    'Sun Jun 14 11:08:51 1998'
    
    >>> str(now())
    '1998-06-14 11:09:17.82'

    More examples are available in the Examples subdirectory of the package.

Supported Data Types in the C-API

    Please have look at the file mxDateTime.h for details. Interfacing is provided through a Python C object for ticks, struct tm, COM doubles, Python tuples and direct input either by giving absolute date/time or a broken down tuple. To access the module, do the following (note the similarities with Python's way of accessing functions from a module):
    #include "mxDateTime.h"
    
    ...
        PyObject *v;
    
        /* Import the mxDateTime module */
        if (mxDateTime_ImportModuleAndAPI())
    	goto onError;
    
        /* Access functions from the exported C API through mxDateTime */
        v = mxDateTime.DateTime_FromAbsDateAndTime(729376, 49272.0);
        if (!v)
    	goto onError;
    
        /* Type checking */
        if (mxDateTime_Check(v))
            printf("Works.\n");
    
        Py_DECREF(v);
    ...
          

Package Structure

    [DateTime]
           Doc/
           [Examples]
                  AtomicClock.py
                  CommandLine.py
                  Y2000.py
                  alarm.py
                  lifespan.py
           [mxDateTime]
                  test.py
           ARPA.py
           DateTime.py
           Feasts.py
           ISO.py
           LazyModule.py
           Locale.py
           NIST.py
           ODMG.py
           Parser.py
           Timezone.py
           timegm.py
          

    Names with trailing / are plain directories, ones with []-brackets are Python packages, ones with ".py" extension are Python submodules.

    The package imports all symbols from the extension module and also registers the types so that they become compatible to the pickle and copy mechanisms in Python.

Support

What I'd like to hear from you...

    • Is there anything important still missing ?

Copyright & License

History & Future