The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Panda::Time - low-level and very efficient POSIX time/zone functions implementation in C.

DESCRIPTION

This module contains low-level C code implementing time functions from scratch. It was written because OS's time functions are too slow and have various small bugs.

Dates from -2147483648/01/01 00:00:00 till 2147483647/12/31 23:59:59 are supported.

Normally you don't need to use most of these functions directly from perl as it's interface cannot provide perfomance which these functions have at C level. You should use Panda::Date module. However you can write our own XS code using these functions or C++ Date class.

SYNOPSIS

    use Panda::Date;
    # ... work with Panda::Date in local zone of your server

    use Panda::Time 'tzset';
    tzset('Europe/Moscow');
    
    use Panda::Date;
    # ... work with Panda::Date in Europe/Moscow as local zone
    

FUNCTIONS

tzset([$zone])

Sets $zone as localzone. If you dont provide $zone, timezone of the server will be set ($ENV{TZ}, /etc/localtime, or whatever your OS considers to be localzone).

Does NOT affect POSIX:tzset(). Only this module's localtime/timelocal/etc functions and Panda::Date classes will follow this timezone.

    # change local zone to 'America/New_York'
    tzset('America/New_York');
    
    # the same
    local $ENV{TZ} = 'America/New_York';
    tzset();
    
    # change localzone back to the server's localzone (in case you didn't change $ENV{TZ})
    tzset(); # or tzset(undef) or tzset('')

If you don't want to change localzone, you don't have to call this function directly as it's called implicitly on-demand.

If you provide $zone and no such zone found in zones directory (or timezone file is corrupted), 'UTC0' is used.

tzget([$zone])

Returns information about timezone $zone (or about server's local zone if $zone is not provided). For information purposes only.

Example of data returned:

    {
        future => {
            hasdst => 1,
            outer => {
                end => {sec => 0, mon => 2, week => 2, hour => 2, day => 0, min => 0 },
                offset => -18000,
                isdst => 0,
                gmt_offset => -18000,
                abbrev => 'EST'
            },
            inner => {
                end => {week => 1, mon => 10, min => 0, hour => 2, day => 0, sec => 0},
                offset => -14400,
                abbrev => 'EDT',
                gmt_offset => -14400,
                isdst => 1
            }
        },
        name => 'America/New_York',
        is_local => 0,
        past => {
            abbrev => 'LMT',
            offset => -17762
        },
        transitions => [
            {
                offset => -17762,
                leap_delta => 0,
                abbrev => 'LMT',
                start => '-9223372036854775808',
                leap_corr => 0,
                gmt_offset => -17762,
                isdst => 0
            },
            {
                offset => -18000,
                leap_delta => 0,
                gmt_offset => -18000,
                isdst => 0,
                start => '-2717650800',
                abbrev => 'EST',
                leap_corr => 0
            },
            ...
        ]
    }

use_system_zones()

Use your OS's timezones dir. This is default behaviour if your OS has /usr/share/zoneinfo DB. Otherwise embedded zones are used by default.

If your OS doesn't have /usr/share/zoneinfo DB, this function warns and does nothing.

use_embed_zones()

Use timezone files which come with this module.

tzdir([$newdir])

Sets or returns current timezones directory. If there was an error (too long path, !exists, !readable, etc) returns false and leaves tzdir unchanged.

    say tzdir(); # prints /usr/share/zoneinfo (on UNIX)
    tzdir('/home/frank/myzones'); # use /home/frank/myzones as timezones DB
    say tzdir(); # prints /home/frank/myzones
    tzset('Europe/Moscow'); # set /home/frank/myzones/Europe/Moscow as localzone

available_zones()

Returns list of all available timezones (names) in tzdir().

tzname()

The name of localzone. Note that in some cases the real name of localzone is not known (for example when localzone is retrieved from /etc/localtime file, tzname() will return ':/etc/localtime')

gmtime($epoch)

Behaves exactly like perl's gmtime.

The returned year is in human-readable form (not year-1900). Month is [0-11]. The same applies for all further time functions.

localtime($epoch)

Behaves exactly like perl's localtime.

timegm($sec, $min, $hour, $day, $mon, $year, [$isdst])

Behaves exactly like POSIX's timegm.

timegmn($sec, $min, $hour, $day, $mon, $year)

Same as timegm() except for the arguments which have to be non-constant values because they are normalized during calculations.

timelocal($sec, $min, $hour, $day, $mon, $year, [$isdst])

Behaves exactly like POSIX's timelocal.

timelocaln($sec, $min, $hour, $day, $mon, $year, [$isdst])

Same as timelocal() except for the arguments which have to be non-constant values because they are normalized during calculations.

SUPPORTED OS

Tested on FreeBSD, Linux, MacOSX. I believe all of UNIX-like systems are supported.

Windows is NOT supported at the moment.

Timezones are supported in Olson DB format (V1,2,3).

C INTERFACE

SYNOPSIS

All functions/types/constants are in panda::time:: namespace (so actually you need C++ to use them).

    #include <stdio.h>
    #include <panda/time.h>
    using panda::time::tzset;
    using panda::time::localtime;
    
    tzset('Europe/Moscow');

    time_t epoch = 1000000000;    
    datetime date;
    localtime(epoch, &date);
    printf(
        "epoch %lli is %04d/%02d/%02d %02d:%02d:%02d, isdst=%d, GMT offset is %d, zone abbreviation is %s",
        epoch, date.year, date.mon+1, date.mday, date.hour, date.min, date.sec, date.isdst, date.gmtoff, date.zone
    );
    
    epoch = timelocal(&date);
    

FUNCTIONS

void tzset (const char* zone = NULL)

See "tzset([$zone])".

tz* tzget (const char* zone)

Returns timezone struct pointer which contains info about timezone 'zone' (or about server's local zone if zone == NULL or "").

You can then use this pointer to perform time calculations in any zone you want without setting local zone via tzset(). You can also have as many timezones in parralel as you want.

Remember that this pointer is only valid until next tzdir(newdir) and possibly tzset() call. If you want this zone pointer to be valid forever use tzcapture().

When you call tzget(zone) for the first time it reads and parses timezone file from disk. Futher calls with the same zone returns cached pointer.

tz* tzlocal ()

Same as tzget(NULL).

const char* tzdir ()

Returns current timezone DB directory.

bool tzdir (const char* newdir)

See "tzdir([$newdir])". tzdir(NULL) sets tzdir to tzsysdir().

const char* tzsysdir ()

Returns system timezones dir if any (usually /usr/share/zoneinfo), otherwise returns NULL.

void tzcapture (tz* zone)

Captures timezone struct so that it remains valid until tzfree(zone) call.

void tzfree (tz* zone)

Releases timezone struct so that it can be removed from memory if no longer used by any other consumers.

Remember: you must not call tzfree(zone) unless you've called tzcapture(zone).

void gmtime (time_t epoch, datetime* result)

Behaves like POSIX's gmtime_r() but much faster.

The returned year is in human-readable form (not year-1900). Month is [0-11]. The same applies for all further time functions.

time_t timegm (datetime* date)

Behaves like POSIX's timegm() but much faster.

time_t timegml (datetime* date)

More efficient (lite) version of timegm(), doesn't change (normalize) values in date.

void localtime (time_t epoch, datetime* result)

Behaves like POSIX's localtime_r() but much faster.

time_t timelocal (datetime* date)

Behaves like POSIX's timelocal() but much faster.

time_t timelocall (datetime* date)

More efficient (lite) version of timelocal(), doesn't change (normalize) values in date.

void anytime (time_t epoch, datetime* result, const tz* zone)

Performs epoch -> datetime calculations in timezone 'zone'.

The following two lines are equivalent:

    localtime(epoch, date);
    anytime(epoch, date, tzlocal());

time_t timeany (datetime* date, const tz* zone)

Performs datetime -> epoch calculations in timezone 'zone'.

The following two lines are equivalent:

    epoch = timelocal(date);
    epoch = timeany(date, tzlocal());
    

time_t timeanyl (datetime* date, const tz* zone)

More efficient (lite) version of timeany(), doesn't change (normalize) values in date.

igmtime(), itimegm(), itimegml(), ilocaltime(), itimelocal(), itimelocall(), ianytime(), itimeany(), itimeanyl()

Inline versions for even more perfomance.

size_t strftime (char* buf, size_t maxsize, const char* format, const datetime* timeptr)

Behaves like POSIX's strftime().

void dt2tm (struct tm &to, datetime &from), void tm2dt (datetime &to, struct tm &from)

Performs struct tm <-> struct datetime convertations

CAVEATS

While developing all the time functions from scratch and comparing results with POSIX's system functions i discovered that many operating systems have buggy implementations of localtime/timelocal functions which causes them to return wrong results in case of certain dates. Therefore in such cases the result of panda::time::* functions won't match with POSIX functions because panda::time handles all these cases correctly.

Bugs i discovered:

Linux and FreeBSD (and possibly more Unix-like systems)

timelocal cannot correctly handle forward time jump at last transition.
     For example Europe/Moscow, date "2011/03/27 02:00:00"
     Must return 1301180400 ("2011/03/27 03:00:00")
     In fact returns
       - linux: 1301176800 ("2011/03/27 01:00:00")
       - freebsd: -1
     If transition is not the last one, it works correctly:
     "2010/03/28 02:00:00" returns 1269730800 ("2010/03/28 03:00:00")
localtime/timelocal handles DST transitions in future (outside of transitions) incorrectly when using leap second zones
     $ TZ=right/Australia/Melbourne perl -E 'say scalar localtime 4284028799'
     Sun Oct  4 01:59:34 2105
     $ TZ=right/Australia/Melbourne perl -E 'say scalar localtime 4284028800'
     Sun Oct  4 02:59:35 2105

FreeBSD only

America/Anchorage timezone behaves like it has no POSIX string (no DST changes after last transition)
timelocal cannot handle dates before year 1900
Wrong forward jump normalization with non-DST transitions
     - Simple forward jump 1h somewhy normalized back
      CORRECT: epoch=-1539492257 (1921/03/21 00:15:43  MSD) from 1921/03/20 23:15:43 DST=-1 (Europe/Moscow)
      POSIX:   epoch=-1539495857 (1921/03/20 22:15:43  MSD) from 1921/03/20 23:15:43 DST=-1 (Europe/Moscow)
     - Forward jump 2h normalized just 1h
      CORRECT: epoch=-1627961251 (1918/06/01 01:03:17 MDST) from 1918/05/31 23:03:17 DST=-1 (Europe/Moscow)
      POSIX:   epoch=-1627964851 (1918/06/01 00:03:17 MDST) from 1918/05/31 23:03:17 DST=-1 (Europe/Moscow)
     - Simple forward jump 1h somewhy normalized 30min
      CORRECT: epoch=372787481 (1981/10/25 03:34:41 LHST) from 1981/10/25 02:34:41 DST=-1 (Australia/Lord_Howe)
      POSIX:   epoch=372785681 (1981/10/25 03:04:41 LHST) from 1981/10/25 02:34:41 DST=-1 (Australia/Lord_Howe)
     - Simple forward jump 1h somewhy normalized 2h
      CORRECT: epoch=449595541 (1984/04/01 01:39:01 CHOST) from 1984/04/01 00:39:01 DST=-1 (Asia/Choibalsan)
      POSIX:   epoch=449599141 (1984/04/01 02:39:01 CHOST) from 1984/04/01 00:39:01 DST=-1 (Asia/Choibalsan)
     - Forward jump 3h normalized 2h
      CORRECT: epoch=354905851 (1981/04/01 04:57:31 MAGST) from 1981/04/01 01:57:31 DST=-1 (Asia/Ust-Nera)
      POSIX:   epoch=354902251 (1981/04/01 03:57:31 MAGST) from 1981/04/01 01:57:31 DST=-1 (Asia/Ust-Nera)

Linux only

Complex bug with static variable deep inside POSIX code

Steps to reproduce: (TZ=Europe/Moscow, date strings are for compactness, actually 'struct tm' required)

    mktime("1998/10/25 03:-1:61"); // returns 909273601 (Sun Oct 25 03:00:01 1998) - that's ok
    mktime("2011/-2/1 00:00:00"); // returns 1285876800 (Fri Oct  1 00:00:00 2010) - that's ok
    // now run the first line again
    mktime("1998/10/25 03:-1:61"); // returns 909270001 (Sun Oct 25 02:00:01 1998) - OOPS
    // again and again
    mktime("1998/10/25 03:-1:61"); // returns 909270001 (Sun Oct 25 02:00:01 1998) - OOPS forever :(

PERFOMANCE

Tests were performed on MacOSX Lion, Core i7 3.2Ghz, clang 3.3.

    --------------------------------------------------------------------------------------------------------------------
    |         Function        | libpanda(inline) | libpanda(func) |  libc(MacOSX)  |   libc(Linux)  |   libc(FreeBSD)  |
    --------------------------------------------------------------------------------------------------------------------
    | gmtime(epoch, &date)    |      65 M/s      |     53 M/s     |     11 M/s     |     15 M/s     |       12 M/s     |
    | timegm(&date)           |      32 M/s      |     30 M/s     |    0.4 M/s     |     10 M/s     |     0.15 M/s     |
    | timegml(&date)          |     170 M/s      |    135 M/s     |       --       |       --       |        --        |
    | localtime(epoch, &date) |      30 M/s      |     26 M/s     |    5.5 M/s     |      7 M/s     |        3 M/s     |
    | timelocal(&date)        |      27 M/s      |     23 M/s     |    0.5 M/s     |    1.2 M/s     |      0.1 M/s     |
    | timelocall(&date)       |      70 M/s      |     50 M/s     |       --       |       --       |        --        |
    --------------------------------------------------------------------------------------------------------------------

AUTHOR

Pronin Oleg <syber@cpan.org>, Crazy Panda, CP Decision LTD

LICENSE

You may distribute this code under the same terms as Perl itself.