Library for time zone conversions and automatic DST adjustments

I like the library and I may use it for an upcomming project but it really makes my head hurt
because I have 30 years of using unix time functions burned into my head.
With the unix time functions you never calculate a time_t value for a local time.
There is typically a single system time_t (which is always utc) and then you convert that to a struct tm
using localtime() where you apply the timezone offsets and any potential DST offset to get
the broken down time values.

This library takes a different approach of converting from the normal/system time_t to a local time_t.

Then there is the differences that Michael did when he created the Time library
Michael deviated from the unix time functions by changing some of the paramters from being zero
based to being one based.
His tmElements_t wDay and Month are 1 based wherease the struct tm tm_wday and tm_mon
are zero based.

While it call can be mad to work, it always makes my head hurt when I use the Arduino Time stuff...

But nice job on the library, given the environment.

--- bill

Bill, thanks for the feedback. Time stuff in general makes my head hurt! I only did a tiny bit of Unix and it was so long ago that it no longer counts. So for better or worse, it wasn't an influence when writing this library XD

If I were to call localtime on a Unix machine, whose local time would I get? Whatever zone the machine was set for?

(I was reading about localtime here, looks like a nice site, don't think I'd come across it before.)

On thing that might be useful for unix types,
would be to add localTime() & localTime_r() functions to the TimeZone object
that returns a pointer to a filled in struct TimeElements
Then uses could do something like:

 utc = now();
 myTZ.localTime_r(&utc, &tm);

and then have the full TimeElements available.
This could be an addition to the existing code as you
you wouldn't have to change the current code to add it.
In order to support localTime() you would have to have to reserve
the space for the TimeElements structure, wherease for localTime_r
would simply write over the users variable.
But they are pretty small easy to write functions that can simply
use the existing Time and Timezone functions to fill in the TimeElements
structure.

The only real bummer is that Michael changed some of the tm elements
to be 1 based vs 0 based when he created his Arduino Time library.
And that can't be change now without likely breaking lots of existing code.

--- bill

Oh yep forgot about the user TZ variable. I was thinking that some extensions to the library along the lines you suggest wouldn't be too difficult. Actually I'm pretty used to dealing with dates and times as serial numbers, it's really the only way to go. And for the most flexibility and generality, any date/time should be stored as UTC/GMT/Zulu, this was my primary motivation with the library, to be able to set an RTC to UTC, and then also not have to worry about the daylight time changes.

Indeed the aliens will be perplexed. There have been various more or less interesting suggestions for "better" systems of keeping time but I think there is little incentive to adopting them given the investment and degree of entrenchment of the current system.

I tried library with GPS time and it works well until arduino power on and it is attached to the computer, when i power it off and on arduino after some time the bare arduino library does not sync with GPS, it starts where it stopped so i got two different times. GPS time works well but library time stayed behind as long power offed?

On restart library should sync with GPS? but it is not doing?

itech2014:
I tried library with GPS time and it works well until arduino power on and it is attached to the computer, when i power it off and on arduino after some time the bare arduino library does not sync with GPS, it starts where it stopped so i got two different times. GPS time works well but library time stayed behind as long power offed?

On restart library should sync with GPS? but it is not doing?

If you are referring to the Timezone library, it just does conversions, e.g. you give it UTC and it gives back local time. The Timezone library works in conjunction with the Arduino Time library, and the Time library has a function, setSyncProvider(), to allow it to synchronize with an external time standard like GPS, an RTC, NTP, or whatever.

bperrybap:
The only real bummer is that Michael changed some of the tm elements
to be 1 based vs 0 based when he created his Arduino Time library.
And that can't be change now without likely breaking lots of existing code.

--- bill

I just became aware of this post and wanted to add context to the reason for the change.
My original time library did follow Unix convention with days and months zero based (see: Arduino Playground - HomePage)

Back in December 2009, There was discussion in the developers mailing list about including these time functions in the Arduino core. In that thread, David Mellis made the point that that an Arduino time API should be closer to Processing (where the days and months start from 1). His reasoning was that people moving from Processing to Arduino were less likely to understand and deal with API differences than those coming from a Unix background. In the absence of any objections from the developer community at the time, the API was altered to be closer to Processing and the rest is history.

It is not always an easy task to create APIs that fully satisfy beginners and boffins, and when compromises need to be made, in the Arduino world, it is right that beginners get preferential consideration.

Michael

Michael,
Thank you for the bit of history.
It is always nice to see where things originated.
Sometimes I really shake my head at some of the decisions
that the processing & arduino teams have made.

--- bill

Is there a way to use one rule that may be changed within the program? I've tried, but can't seem to find a solution. Thanks in advance for help on this.

bperrybap:
Michael deviated from the unix time functions by changing some of the paramters from being zero
based to being one based.
His tmElements_t wDay and Month are 1 based wherease the struct tm tm_wday and tm_mon
are zero based.

The way I see it, it is evil for a programming language (or standard library) to insist on zero-based month numbering. If you have the kind of strictly compartmentalized mind necessary never to get confused between that and the month-numbering system used by non-programmers, then good for you. But, for everyone else, it's a trap.

Consider the change to 1-based month numbering to be a "bug fix" for a decades-old bug.

(BTW, in some languages such as Chinese, the month names are the numbers.)

Hi, i would like to know if it's possible to disable DST during normal loop process ex: using a switch. i tried to change
Timezone myTZ(myDST, mySTD); to Timezone myTZ(mySTD, mySTD); during normal operation but it dont work

Thanks

madex12345:
Hi, i would like to know if it's possible to disable DST during normal loop process ex: using a switch. i tried to change
Timezone myTZ(myDST, mySTD); to Timezone myTZ(mySTD, mySTD); during normal operation but it dont work

Thanks

  1. Please post your code.

  2. It might be easier to create your own functions for time manipulation. That is what I did for my own projects.

Hi,

I just used the timezone library with the ESP8266 platform and had problems due to conflicts with the xtensa time.h header that comes with the esp8266 compiler.

The solution (for me!) was to rename and edit the code as follows:
In the library directory for the Time library (.....\Arduino\libraries\Time)

Rename Time.h to TimeArd.h

In TimeArd.h change the include define to
#ifndef _TimeArd_h
#ifdef __cplusplus
#define _TimeArd_h

In Time.cpp change to #include "TimeArd.h"
In DateStrings.cpp change to #include "TimeArd.h"

It then compiled and worked fine.

Acuario:
conflicts with the xtensa time.h header that comes with the esp8266 compiler.

This is a known issue. The author of the Time library has moved the code to a file named TimeLib.h in recent versions of the library but left Time.h for backwards compatibility. So all you need to do to fix the issue is to install the current version of the Time library and then change occurrences of:

#include <Time.h>

to:

#include <TimeLib.h>

There is a pull request to make this change to the Timezone library:

but unfortunately JChristensen hasn't merged it.

This issue will also occur for Arduino IDE 1.6.10/Arduino AVR Boards 1.6.12 and newer as well as any other core with time.h on a case insensitive operating system.

Hi
I'm hoping this is just what I'm looking for.
Do we comment out the time zones not required in the worldclock?

Many thanks

Hello everyone,

sorry for the lite OT but I'm using this library in my project and it is working beautifully, so I wanted to say thank you to the developer!
I am using an AT tiny 85 for my project, in case any of you is interested ;D :

It's an automatic light switch, based in light conditions and time of the day. So the DST was the last thing missing in my code!

Happy Halloween!! :smiling_imp:

pert:
This is a known issue. The author of the Time library has moved the code to a file named TimeLib.h in recent versions of the library but left Time.h for backwards compatibility.

It is combination of Windows not supporting upper/lower case and the way the IDE creates the -I include list.
IMO, the IDE should be placing the Arduino libraries ahead of the -I for the core headers.
If this were done then the problem would also go away.

Currently because the gcc tools are not really installed properly when the IDE is installed, the IDE must call the gcc tools with full paths and specify the include paths for the tools using -I paths.
When the gcc tools are installed properly, no -I paths are needed for the compiler and system headers and any additional -I headers will be searched before the system headers.

Because the IDE uses -I to specify the compiler and system headers, and those are before the Arduino libraries on the command line, they get searched before the Arduino headers.

In the case of an OS like Windows that doesn't support proper upper/lower case file names, you can get a collision between Time.h and time.h
And since the system includes were included using -I paths that were before the Arduino library -I paths, time.h vs Time.h was picked.
Had the IDE placed the -I paths for the compiler and the system headers after the Arduino headers, the compiler would have picked Time.h

I believe that the IDE can even be smart enough to avoid the case insensitivity so that if a sketch included <time.h> it would not include the Time library (since it doesn't have a file called time.h in it) and therefore not put it on the -I include path.

The combination of moving compiler and system includes after Arduino libraries and making the IDE smart enough to check for exact header file name matches would prevent this problem with no changes to any library or sketch code.

--- bill

Hey - Great library. This just saved me a lot of work.

However...
When I convert the current UTC time to local, then display it, the year is coming out as 47 rather than 2017, a difference of 1970 years.
I know time_t is based at 1970, but shouldn't this already be accounted for rather than having to use tm.Year+1970?

Thanks!

Jack
With over 85 timezone rules around the world, because of this there is just not enough memory to enter them all in arduino.

i am trying to make a web changeable clock where you would enter your timezone and your country rules like:

    -7,1,2,1,3,2,1,1,11,2
    -7,0    if no DST called for

    TZ, DST, SpringWK, SpringDOW, SpringMonth, SpringHR, FallWK, FallDOW FallMonth, FallHR  
      
    if DST = 0 both change rules offset would be set equal like the Arizona rule

Stored in JSON and parsed, calculated and passed as variables into 1 rule.

   TimeChangeRule DT = { "DT" , DTwk, DTdow, DTmonth, DThr, GMT_DST   };  //UTC +- X-60 minutes
   TimeChangeRule ST = { "ST" , STwk, STdow, STmonth, SThr, GMToffset };    //UTC +- X minutes
   Timezone       DST( myDT, myST );

i can write to the struct with DT.offset = GMToffset; etc... just fine.
the JSON, parsing, calculating and web already work.

my issue is how to "REBUILD" the Timezone object after writing to the struct.

ie; Timezone DST( DT, ST );

test code shows the struct changes but the time stays the same as GMT ( I'm using NTP)

a fixed rule works but not a calculated rule even though the numbers are the same when printed.

     TimeChangeRule usMDT = {"MDT", Second, dowSunday, Mar, 2, -360};
     TimeChangeRule usMST = {"MST", First, dowSunday, Nov, 2, -420};

if i declare the object in setup() its not global and loop() cant see it.

How can i access the "Timezone object" directly?

I added a method so that rules can be changed. I'm not sure if it's the best way to do it, but it does work.

/*----------------------------------------------------------------------*
 * Arduino TimeZone Library v1.0                                        *
 * Jack Christensen Mar 2012                                            *
 * This work is licensed under the Creative Commons Attribution-        *
 * ShareAlike 3.0 Unported License. To view a copy of this license,     *
 * visit http://creativecommons.org/licenses/by-sa/3.0/ or send a       *
 * letter to Creative Commons, 171 Second Street, Suite 300,            *
 * San Francisco, California, 94105, USA.                               *
 *----------------------------------------------------------------------*/

#include "TimeZone.h"

/*----------------------------------------------------------------------*
 * Create a Timezone object from the given time change rules.           *
 *----------------------------------------------------------------------*/
TimeZoneOne::TimeZone()
{
    //instantiate object
}
/*----------------------------------------------------------------------*
 
/*----------------------------------------------------------------------*
 * Convert the given UTC time to local time, standard or                *
 * daylight time, as appropriate.                                       *
 *----------------------------------------------------------------------*/

time_t TimeZone::toLocal(time_t utc)
{
	_dst = _dstSave;
	_std = _stdSave;
    //recalculate the time change points if needed
 if (year(utc) != year(_dstUTC)) calcTimeChanges(year(utc));
    if (utcIsDST(utc))
        return utc + _dst.offset * SECS_PER_MIN;
    else
        return utc + _std.offset * SECS_PER_MIN;
}

/*----------------------------------------------------------------------*
 * Convert the given local time to UTC time.                            *
 *                                                                      *
 * WARNING:                                                             *
 * This function is provided for completeness, but should seldom be     *
 * needed and should be used sparingly and carefully.                   *
 *                                                                      *
 * Ambiguous situations occur after the Standard-to-DST and the         *
 * DST-to-Standard time transitions. When changing to DST, there is     *
 * one hour of local time that does not exist, since the clock moves    *
 * forward one hour. Similarly, when changing to standard time, there   *
 * is one hour of local times that occur twice since the clock moves    *
 * back one hour.                                                       *
 *                                                                      *
 * This function does not test whether it is passed an erroneous time   *
 * value during the Local -> DST transition that does not exist.        *
 * If passed such a time, an incorrect UTC time value will be returned. *
 *                                                                      *
 * If passed a local time value during the DST -> Local transition      *
 * that occurs twice, it will be treated as the earlier time, i.e.      *
 * the time that occurs before the transistion.                         *
 *                                                                      *
 * Calling this function with local times during a transition interval  *
 * should be avoided!                                                   *
 *----------------------------------------------------------------------*/
time_t TimeZone::toUTC(time_t local)
{
    //recalculate the time change points if needed
    if (year(local) != year(_dstLoc)) calcTimeChanges(year(local));

    if (locIsDST(local))
        return local - _dst.offset * SECS_PER_MIN;
    else
        return local - _std.offset * SECS_PER_MIN;
}

/*----------------------------------------------------------------------*
 * Determine whether the given UTC time_t is within the DST interval    *
 * or the Standard time interval.                                       *
 *----------------------------------------------------------------------*/
boolean TimeZone::utcIsDST(time_t utc)
{
    //recalculate the time change points if needed
    if (year(utc) != year(_dstUTC)) calcTimeChanges(year(utc));
    if (_stdUTC > _dstUTC)    //northern hemisphere
        return (utc >= _dstUTC && utc < _stdUTC);
    else                      //southern hemisphere
        return !(utc >= _stdUTC && utc < _dstUTC);
}

/*----------------------------------------------------------------------*
 * Determine whether the given Local time_t is within the DST interval  *
 * or the Standard time interval.                                       *
 *----------------------------------------------------------------------*/
boolean TimeZone::locIsDST(time_t local)
{
    //recalculate the time change points if needed
    if (year(local) != year(_dstLoc)) calcTimeChanges(year(local));

    if (_stdLoc > _dstLoc)    //northern hemisphere
        return (local >= _dstLoc && local < _stdLoc);
    else                      //southern hemisphere
        return !(local >= _stdLoc && local < _dstLoc);
}

/*----------------------------------------------------------------------*
 * Calculate the DST and standard time change points for the given      *
 * given year as local and UTC time_t values.                           *
 *----------------------------------------------------------------------*/
void TimeZone::calcTimeChanges(int yr)
{
    _dstLoc = toTime_t(_dst, yr);
    _stdLoc = toTime_t(_std, yr);
    _dstUTC = _dstLoc - _std.offset * SECS_PER_MIN;
    _stdUTC = _stdLoc - _dst.offset * SECS_PER_MIN;
}

/*----------------------------------------------------------------------*
 * Convert the given DST change rule to a time_t value                  *
 * for the given year.                                                  *
 *----------------------------------------------------------------------*/
time_t TimeZone::toTime_t(TimeChangeRule r, int yr)
{
    tmElements_t tm;
    time_t t;
    uint8_t m, w;            //temp copies of r.month and r.week

    m = r.month;
    w = r.week;
    if (w == 0) {            //Last week = 0
        if (++m > 12) {      //for "Last", go to the next month
            m = 1;
            yr++;
        }
        w = 1;               //and treat as first week of next month, subtract 7 days later
    }

    tm.Hour = r.hour;
    tm.Minute = 0;
    tm.Second = 0;
    tm.Day = 1;
    tm.Month = m;
    tm.Year = yr - 1970;
    t = makeTime(tm);        //first day of the month, or first day of next month for "Last" rules
    t += (7 * (w - 1) + (r.dow - weekday(t) + 7) % 7) * SECS_PER_DAY;
    if (r.week == 0) t -= 7 * SECS_PER_DAY;    //back up a week if this is a "Last" rule
    return t;
}

// update rules

void TimeZone::setRule(TimeChangeRule dstStart, TimeChangeRule stdStart)
{
    	_dst = dstStart;
    	_std = stdStart;
	_dstSave = dstStart;
	_stdSave = stdStart;
}