Date and time classes for Arduino supporting the IANA TZ Database time zones to convert epoch seconds to date and time components in different time zones.
MIT License
The AceTime library provides Date, Time, and TimeZone classes which can convert "epoch seconds" from the AceTime Epoch (default 2050-01-01 UTC) to human-readable local date and time fields. Those classes can also convert local date and time between different time zones defined by the IANA TZ database while accurately accounting for DST transitions.
The default AceTime epoch is 2050-01-01, but it can be adjusted by the client
application at runtime. The epoch second has the type acetime_t
which is a
32-bit signed integer, instead of a 64-bit signed integer. Using the smaller
32-bit integer allows the library to use less CPU and memory resources on 8-bit
and 32-bit microcontrollers without native 64-bit integer instructions. The
range of a 32-bit integer is about 136 years. To be safe, AceTime timezone
functions should be kept well within the bounds of this interval, for example,
straddling roughly +/- 60 years of the Epoch::currentEpochYear()
.
The library provides 3 pre-generated ZoneInfo Databases which are programmatically extracted from the IANA TZ database:
[2000,10000)
BasicZoneProcessor
BasicZoneManager
[2000,10000)
ExtendedZoneProcessor
and ExtendedZoneManager
[0001,10000)
CompleteZoneProcessor
and CompleteZoneManager
Client applications can choose to incorporate a subset of the zones provided by the above databases to save flash memory size of the application.
Support for Unix time using the Unix
epoch of 1970-01-01 is provided through conversion functions of the time_t
type. Only the 64-bit version of the time_t
type is supported to avoid the
Year 2038 Problem.
The companion library AceTimeClock
provides Clock classes to retrieve the time from more accurate sources, such as
an NTP server, or a
DS3231
RTC
chip. A special version of the Clock
class called the SystemClock
provides a
fast and accurate "epoch seconds" across all Arduino compatible systems. This
"epoch seconds" can be given to the classes in this library to convert it into
human readable components in different timezones. On the ESP8266 and ESP32, the
AceTime library can be used with the SNTP client and the C-library time()
function through the 64-bit time_t
value. See ESP8266 and ESP32
TimeZones below.
The primordial motivation for creating the AceTime library was to build a digital clock with an OLED or LED display, that would show the date and time of multiple timezones at the same time, while adjusting for any DST changes in the selected timezones automatically. Another major goal of the library is to keep the resource (flash and RAM) consumption as small as practical, to allow substantial portion of this library to run inside the 32kB of flash and 2kB of RAM limits of an Arduino Nano or a SparkFun Pro Micro dev board. To meet that goal, this library does not perform any dynamic allocation of memory. Everything it needs is allocated statically.
This library can be an alternative to the Arduino Time (https://github.com/PaulStoffregen/Time) and Arduino Timezone (https://github.com/JChristensen/Timezone) libraries.
Major Changes in v2.3: Add CompleteZoneProcessor
, CompleteZoneManager
,
and the zonedbc
database to support all timezones, for all transitions defined
in the IANA TZ database ([1844,2087]
), and extending the validity of timezone
calculations from [2000,10000)
to [0001,10000)
.
Version: 2.3.2 (2024-07-25, TZDB version 2024a)
Changelog: CHANGELOG.md
Migration: MIGRATING.md
User Guide: USER_GUIDE.md
See Also:
The latest stable release is available in the Arduino Library Manager in the IDE. Search for "AceTime". Click install. The Library Manager should automatically install AceTime and its dependent libraries:
The development version can be installed by cloning the above repos manually.
You can copy over the contents to the ./libraries
directory used by the
Arduino IDE. (The result is a set of directories named ./libraries/AceTime
,
./libraries/AceCommon
, ./libraries/AceSorting
). Or you can create symlinks
from ./libraries
to these directories. Or you can git clone
directly into
the ./libraries
directory.
The develop
branch contains the latest development.
The master
branch contains the stable releases.
The source files are organized as follows:
src/AceTime.h
- main header filesrc/ace_time/
- date and time classes (ace_time::
namespace)src/ace_time/common/
- shared classes and utilitiessrc/ace_time/testing/
- files used in unit tests (ace_time::testing
src/zoneinfo
- reading the zone databases, and normalizing thesrc/zonedb/
- files generated from TZ Database forBasicZoneProcessor
(ace_time::zonedb
namespace)src/zonedbtesting/
- limited subset of zonedb
for unit testssrc/zonedbx/
- files generated from TZ Database forExtendedZoneProcessor
(ace_time::zonedbx
namespace)src/zonedbxtesting/
- limited subset of zonedbx
for unit testssrc/zonedbc/
- files generated from TZ Database forCompleteZoneProcessor
(ace_time::zonedbc
namespace)src/zonedbctesting/
- limited subset of zonedbc
for unit testsexamples/
- example programs and benchmarks
ZonedDateTime
classExtendedZoneManager
classHelloZoneManager
, but using a custom zone registryzonedbx
registry.ExtendedZoneManager
, sortedZoneSorter
classes.The AceTime library depends on the following libraries:
Various programs in the examples/
directory have one or more of the following
external dependencies. The comment section near the top of the *.ino
file will
usually have more precise dependency information:
If you want to run the unit tests or validation tests using a Linux or MacOS machine, you need:
Here is a simple program (see examples/HelloDateTime) which demonstrates how to create and manipulate date and times in different time zones:
#include <Arduino.h>
#include <AceTime.h>
using namespace ace_time;
// ZoneProcessor instances should be created statically at initialization time.
static ExtendedZoneProcessor losAngelesProcessor;
static ExtendedZoneProcessor londonProcessor;
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Wait until Serial is ready - Leonardo/Micro
// TimeZone objects are light-weight and can be created on the fly.
TimeZone losAngelesTz = TimeZone::forZoneInfo(
&zonedbx::kZoneAmerica_Los_Angeles,
&losAngelesProcessor);
TimeZone londonTz = TimeZone::forZoneInfo(
&zonedbx::kZoneEurope_London,
&londonProcessor);
// Create from components. 2019-03-10T03:00:00 is just after DST change in
// Los Angeles (2am goes to 3am).
ZonedDateTime startTime = ZonedDateTime::forComponents(
2019, 3, 10, 3, 0, 0, losAngelesTz);
Serial.print(F("Epoch Seconds: "));
acetime_t epochSeconds = startTime.toEpochSeconds();
Serial.println(epochSeconds);
Serial.print(F("Unix Seconds: "));
int64_t unixSeconds = startTime.toUnixSeconds64();
Serial.println((int32_t) unixSeconds);
Serial.println(F("=== Los_Angeles"));
ZonedDateTime losAngelesTime = ZonedDateTime::forEpochSeconds(
epochSeconds, losAngelesTz);
Serial.print(F("Time: "));
losAngelesTime.printTo(Serial);
Serial.println();
Serial.print(F("Day of Week: "));
Serial.println(
DateStrings().dayOfWeekLongString(losAngelesTime.dayOfWeek()));
// Print info about UTC offset
TimeOffset offset = losAngelesTime.timeOffset();
Serial.print(F("Total UTC Offset: "));
offset.printTo(Serial);
Serial.println();
// Print info about the current time zone
Serial.print(F("Zone: "));
losAngelesTz.printTo(Serial);
Serial.println();
// Print the current time zone abbreviation, e.g. "PST" or "PDT"
ZonedExtra ze = losAngelesTz.getZonedExtra(epochSeconds);
Serial.print(F("Abbreviation: "));
SERIAL_PORT_MONITOR.print(ze.abbrev());
Serial.println();
// Create from epoch seconds. London is still on standard time.
ZonedDateTime londonTime = ZonedDateTime::forEpochSeconds(
epochSeconds, londonTz);
Serial.println(F("=== London"));
Serial.print(F("Time: "));
londonTime.printTo(Serial);
Serial.println();
// Print info about the current time zone
Serial.print(F("Zone: "));
londonTz.printTo(Serial);
Serial.println();
// Print the current time zone abbreviation, e.g. "GMT" or "BST"
ze = londonTz.getZonedExtra(epochSeconds);
Serial.print(F("Abbreviation: "));
SERIAL_PORT_MONITOR.print(ze.abbrev());
Serial.println();
Serial.println(F("=== Compare ZonedDateTime"));
Serial.print(F("losAngelesTime.compareTo(londonTime): "));
Serial.println(losAngelesTime.compareTo(londonTime));
Serial.print(F("losAngelesTime == londonTime: "));
Serial.println((losAngelesTime == londonTime) ? "true" : "false");
}
void loop() {
}
Running this should produce the following on the Serial port:
Epoch Seconds: -972396000
Unix Seconds: 1552212000
=== Los Angeles
Time: 2019-03-10T03:00:00-07:00[America/Los_Angeles]
Day of Week: Sunday
Total UTC Offset: -07:00
Zone: America/Los_Angeles
Abbreviation: PDT
=== London
Time: 2019-03-10T10:00:00+00:00[Europe/London]
Zone: Europe/London
Abbreviation: GMT
=== Compare ZonedDateTime
losAngelesTime.compareTo(londonTime): 0
losAngelesTime == londonTime: false
(The default epoch for AceTime is 2050-01-01, so a date in 2019 will return a negative epoch seconds.)
The examples/HelloZoneManager example shows how to
load the entire zonedbx
ZoneInfo Database into an ExtendedZoneManager
, then
create 3 time zones using 3 different ways: createForZoneInfo()
,
createForZoneName()
, and createForZoneId()
. This program requires a 32-bit
microcontroller environment because the ExtendedZoneManager
with the
zonedbx::kZoneAndLinkRegistry
consumes ~44 kB of flash which does not fit in
the 32 kB flash memory capacity of an Arduino Nano. If the ExtendedZoneManager
is replaced with the BasicZoneManager
and the smaller kZoneRegistry
is used
instead, the flash size goes down to about ~24 kB. Depending on the code size of
the rest of the application, this may fit inside an Arduino Nano.
#include <Arduino.h>
#include <AceTime.h>
using namespace ace_time;
// Create an ExtendedZoneManager with the entire TZ Database of Zone and Link
// entries. Cache size of 3 means that it can support 3 concurrent timezones
// without performance penalties.
static const int CACHE_SIZE = 3;
static ExtendedZoneProcessorCache<CACHE_SIZE> zoneProcessorCache;
static ExtendedZoneManager manager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache);
void setup() {
Serial.begin(115200);
while (!Serial); // Wait until ready - Leonardo/Micro
// Create America/Los_Angeles timezone by ZoneInfo.
TimeZone losAngelesTz = manager.createForZoneInfo(
&zonedbx::kZoneAmerica_Los_Angeles);
ZonedDateTime losAngelesTime = ZonedDateTime::forComponents(
2019, 3, 10, 3, 0, 0, losAngelesTz);
losAngelesTime.printTo(Serial);
Serial.println();
// Create Europe/London timezone by ZoneName.
TimeZone londonTz = manager.createForZoneName("Europe/London");
ZonedDateTime londonTime = losAngelesTime.convertToTimeZone(londonTz);
londonTime.printTo(Serial);
Serial.println();
// Create Australia/Sydney timezone by ZoneId.
TimeZone sydneyTz = manager.createForZoneId(zonedbx::kZoneIdAustralia_Sydney);
ZonedDateTime sydneyTime = losAngelesTime.convertToTimeZone(sydneyTz);
sydneyTime.printTo(Serial);
Serial.println();
}
void loop() {
}
It produces the following output:
2019-03-10T03:00:00-07:00[America/Los_Angeles]
2019-03-10T10:00:00+00:00[Europe/London]
2019-03-10T21:00:00+11:00[Australia/Sydney]
Here is a photo of the WorldClock (https://github.com/bxparks/clocks/tree/master/WorldClock) that supports 3 OLED displays with 3 timezones, and automatically adjusts the DST transitions for all 3 zones:
The full documentation of the following classes are given in the USER_GUIDE.md:
ace_time::acetime_t
ace_time::DateStrings
ace_time::LocalTime
ace_time::LocalDate
ace_time::LocalDateTime
ace_time::TimeOffset
ace_time::OffsetDateTime
ace_time::TimePeriod
ace_time::local_date_mutation::
ace_time::time_offset_mutation::
ace_time::time_period_mutation::
ace_time::offset_date_time_mutation::
ace_time::zoned_date_time_mutation::
ace_time::ZoneProcessor
ace_time::BasicZoneProcessor
ace_time::ExtendedZoneProcessor
ace_time::CompleteZoneProcessor
ace_time::TimeZone
ace_time::ZonedDateTime
ace_time::ZoneManager
ace_time::BasicZoneManager
ace_time::ExtendedZoneManager
ace_time::CompleteZoneManager
ace_time::ManualZoneManager
ZoneInfo
data structureuint32_t
identifier (e.g. 0xb7f7e8f2
)const ace_time::basic::ZoneInfo*
)
ace_time::zonedb::kZoneAfrica_Abidjan
ace_time::zonedb::kZonePacific_Wallis
uint32_t
)
ace_time::zonedb::kZoneIdAfrica_Abidjan
ace_time::zonedb::kZoneIdPacific_Wallis
const char*
)
"Africa/Abidjan"
"Pacific/Wallis"
const ace_time::extended::ZoneInfo*
)
ace_time::zonedbx::kZoneAfrica_Abidjan
ace_time::zonedbx::kZonePacific_Wallis
uint32_t
)
ace_time::zonedbx::kZoneIdAfrica_Abidjan
ace_time::zonedbx::kZoneIdPacific_Wallis
const char*
)
"Africa/Abidjan"
"Pacific/Wallis"
const ace_time::complete::ZoneInfo*
)
ace_time::zonedbc::kZoneAfrica_Abidjan
ace_time::zonedbc::kZonePacific_Wallis
uint32_t
)
ace_time::zonedbc::kZoneIdAfrica_Abidjan
ace_time::zonedbc::kZoneIdPacific_Wallis
const char*
)
"Africa/Abidjan"
"Pacific/Wallis"
The details of how the Date, Time and TimeZone classes are validated are given in AceTimeValidation.
The ZoneInfo Database and the algorithms in this library were tested against
other date/time libraries written in different programming languages. The
CompleteZoneProcessor
configured with the zonedbc
database was used for most
of the validation testing, because this combination covers all zones and links
valid over a 400-year interval [1800,2200)
. Some of these third party
libraries match AceTime exactly over the 400 year interval. Other libraries
contained bugs which prevented exact conformance with AceTime.
Conformant
These libraries match the AceTime library (using the
CompleteZoneProcessor
/CompleteZoneManager
with the zonedbc
database)
exactly: every DST transition, DST offset, STD offset, the epochsecond of the
transition, the converted data-time components, and the abbreviation. The match
was verified for all time zones and links, from 1800 until 2200.
Non Conformant
These third party libraries match AceTime for the most part, but seem to contain bugs in some parts of their code. I have not had the time to look into the cause of these bugs in these third party libraries.
java.time
time
package from 1800 to 2200
8-bit processors
Sizes of Objects:
sizeof(LocalDate): 4
sizeof(LocalTime): 4
sizeof(LocalDateTime): 8
sizeof(TimeOffset): 4
sizeof(OffsetDateTime): 12
sizeof(TimeZone): 5
sizeof(TimeZoneData): 5
sizeof(ZonedDateTime): 17
sizeof(ZonedExtra): 24
sizeof(TimePeriod): 4
Basic:
sizeof(basic::ZoneContext): 20
sizeof(basic::ZoneEra): 11
sizeof(basic::ZoneInfo): 13
sizeof(basic::ZoneRule): 9
sizeof(basic::ZonePolicy): 3
sizeof(basic::ZoneRegistrar): 5
sizeof(BasicZoneProcessor): 143
sizeof(BasicZoneProcessorCache<1>): 147
sizeof(BasicZoneManager): 7
sizeof(BasicZoneProcessor::Transition): 26
Extended:
sizeof(extended::ZoneContext): 20
sizeof(extended::ZoneEra): 11
sizeof(extended::ZoneInfo): 13
sizeof(extended::ZoneRule): 9
sizeof(extended::ZonePolicy): 3
sizeof(extended::ZoneRegistrar): 5
sizeof(ExtendedZoneProcessor): 553
sizeof(ExtendedZoneProcessorCache<1>): 557
sizeof(ExtendedZoneManager): 7
sizeof(ExtendedZoneProcessor::Transition): 49
sizeof(ExtendedZoneProcessor::TransitionStorage): 412
sizeof(ExtendedZoneProcessor::MatchingEra): 32
Complete:
sizeof(complete::ZoneContext): 20
sizeof(complete::ZoneEra): 15
sizeof(complete::ZoneInfo): 13
sizeof(complete::ZoneRule): 12
sizeof(complete::ZonePolicy): 3
sizeof(complete::ZoneRegistrar): 5
sizeof(CompleteZoneProcessor): 553
sizeof(CompleteZoneProcessorCache<1>): 557
sizeof(CompleteZoneManager): 7
sizeof(CompleteZoneProcessor::Transition): 49
sizeof(CompleteZoneProcessor::TransitionStorage): 412
sizeof(CompleteZoneProcessor::MatchingEra): 32
32-bit processors
Sizes of Objects:
sizeof(LocalDate): 4
sizeof(LocalTime): 4
sizeof(LocalDateTime): 8
sizeof(TimeOffset): 4
sizeof(OffsetDateTime): 12
sizeof(TimeZone): 12
sizeof(TimeZoneData): 8
sizeof(ZonedDateTime): 24
sizeof(ZonedExtra): 24
sizeof(TimePeriod): 4
Basic:
sizeof(basic::ZoneContext): 28
sizeof(basic::ZoneEra): 16
sizeof(basic::ZoneInfo): 24
sizeof(basic::ZoneRule): 9
sizeof(basic::ZonePolicy): 8
sizeof(basic::ZoneRegistrar): 8
sizeof(BasicZoneProcessor): 208
sizeof(BasicZoneProcessorCache<1>): 216
sizeof(BasicZoneManager): 12
sizeof(BasicZoneProcessor::Transition): 36
Extended:
sizeof(extended::ZoneContext): 28
sizeof(extended::ZoneEra): 16
sizeof(extended::ZoneInfo): 24
sizeof(extended::ZoneRule): 9
sizeof(extended::ZonePolicy): 8
sizeof(extended::ZoneRegistrar): 8
sizeof(ExtendedZoneProcessor): 720
sizeof(ExtendedZoneProcessorCache<1>): 728
sizeof(ExtendedZoneManager): 12
sizeof(ExtendedZoneProcessor::Transition): 60
sizeof(ExtendedZoneProcessor::TransitionStorage): 516
sizeof(ExtendedZoneProcessor::MatchingEra): 44
Complete:
sizeof(complete::ZoneContext): 28
sizeof(complete::ZoneEra): 20
sizeof(complete::ZoneInfo): 24
sizeof(complete::ZoneRule): 12
sizeof(complete::ZonePolicy): 8
sizeof(complete::ZoneRegistrar): 8
sizeof(CompleteZoneProcessor): 720
sizeof(CompleteZoneProcessorCache<1>): 728
sizeof(CompleteZoneManager): 12
sizeof(CompleteZoneProcessor::Transition): 60
sizeof(CompleteZoneProcessor::TransitionStorage): 516
sizeof(CompleteZoneProcessor::MatchingEra): 44
The ZoneInfo Database entries are stored in flash memory (using the PROGMEM
compiler directive) if the microcontroller allows it (e.g. AVR, ESP8266) so that
they do not consume static RAM. The
examples/MemoryBenchmark program shows the flash
memory consumption for the ZoneInfo data files are:
BasicZoneProcessor
(all zones and links)
ExtendedZoneProcessor
(all zones and links)
CompleteZoneProcessor
(all zones and links)
An example of more complex application is the
WorldClock (https://github.com/bxparks/clocks/tree/master/WorldClock)
which has 3 OLED displays over SPI, 3 timezones using BasicZoneProcessor
, a
SystemClock
synchronized to a DS3231 chip on I2C, and 2 buttons with
debouncing and event dispatching provided by the AceButton
(https://github.com/bxparks/AceButton) library. This application consumes about
24 kB, well inside the 28 kB flash limit of a SparkFun Pro Micro controller.
This library does not perform dynamic allocation of memory so that it can be
used in small microcontroller environments. In other words, it does not call the
new
operator nor the malloc()
function, and it does not use the Arduino
String
class. Everything it needs is allocated statically at initialization
time.
MemoryBenchmark was used to determine the size of the library for various microcontrollers (Arduino Nano to ESP32). Here are 2 samples:
Arduino Nano:
+----------------------------------------------------------------------+
| Functionality | flash/ ram | delta |
|----------------------------------------+--------------+--------------|
| baseline | 474/ 11 | 0/ 0 |
|----------------------------------------+--------------+--------------|
| LocalDateTime | 1108/ 21 | 634/ 10 |
| ZonedDateTime | 1444/ 30 | 970/ 19 |
| Manual ZoneManager | 1406/ 13 | 932/ 2 |
|----------------------------------------+--------------+--------------|
| Basic TimeZone (1 zone) | 7624/ 208 | 7150/ 197 |
| Basic TimeZone (2 zones) | 8170/ 357 | 7696/ 346 |
| BasicZoneManager (1 zone) | 7834/ 219 | 7360/ 208 |
| BasicZoneManager (all zones) | 19686/ 599 | 19212/ 588 |
| BasicZoneManager (all zones+links) | 25066/ 599 | 24592/ 588 |
|----------------------------------------+--------------+--------------|
| Basic ZoneSorterByName [1] | 8608/ 221 | 774/ 2 |
| Basic ZoneSorterByOffsetAndName [1] | 8740/ 221 | 906/ 2 |
|----------------------------------------+--------------+--------------|
| Extended TimeZone (1 zone) | 11326/ 623 | 10852/ 612 |
| Extended TimeZone (2 zones) | 11924/ 1187 | 11450/ 1176 |
| ExtendedZoneManager (1 zone) | 11506/ 629 | 11032/ 618 |
| ExtendedZoneManager (all zones) | 34508/ 1111 | 34034/ 1100 |
| ExtendedZoneManager (all zones+links) | 40540/ 1111 | 40066/ 1100 |
|----------------------------------------+--------------+--------------|
| Extended ZoneSorterByName [2] | 12276/ 631 | 770/ 2 |
| Extended ZoneSorterByOffsetAndName [2] | 12360/ 631 | 854/ 2 |
|----------------------------------------+--------------+--------------|
| Complete TimeZone (1 zone) | -1/ -1 | -1/ -1 |
| Complete TimeZone (2 zones) | -1/ -1 | -1/ -1 |
| CompleteZoneManager (1 zone) | -1/ -1 | -1/ -1 |
| CompleteZoneManager (all zones) | -1/ -1 | -1/ -1 |
| CompleteZoneManager (all zones+links) | -1/ -1 | -1/ -1 |
|----------------------------------------+--------------+--------------|
| Complete ZoneSorterByName [3] | -1/ -1 | 0/ 0 |
| Complete ZoneSorterByOffsetAndName [3] | -1/ -1 | 0/ 0 |
+---------------------------------------------------------------------+
ESP8266:
+----------------------------------------------------------------------+
| Functionality | flash/ ram | delta |
|----------------------------------------+--------------+--------------|
| baseline | 260089/27892 | 0/ 0 |
|----------------------------------------+--------------+--------------|
| LocalDateTime | 260613/27912 | 524/ 20 |
| ZonedDateTime | 261573/27928 | 1484/ 36 |
| Manual ZoneManager | 261553/27900 | 1464/ 8 |
|----------------------------------------+--------------+--------------|
| Basic TimeZone (1 zone) | 267117/28528 | 7028/ 636 |
| Basic TimeZone (2 zones) | 267533/28736 | 7444/ 844 |
| BasicZoneManager (1 zone) | 267277/28552 | 7188/ 660 |
| BasicZoneManager (all zones) | 283613/28552 | 23524/ 660 |
| BasicZoneManager (all zones+links) | 292141/28552 | 32052/ 660 |
|----------------------------------------+--------------+--------------|
| Basic ZoneSorterByName [1] | 268029/28552 | 752/ 0 |
| Basic ZoneSorterByOffsetAndName [1] | 268157/28552 | 880/ 0 |
|----------------------------------------+--------------+--------------|
| Extended TimeZone (1 zone) | 269653/29152 | 9564/ 1260 |
| Extended TimeZone (2 zones) | 270069/29880 | 9980/ 1988 |
| ExtendedZoneManager (1 zone) | 269813/29160 | 9724/ 1268 |
| ExtendedZoneManager (all zones) | 301077/29160 | 40988/ 1268 |
| ExtendedZoneManager (all zones+links) | 310645/29160 | 50556/ 1268 |
|----------------------------------------+--------------+--------------|
| Extended ZoneSorterByName [2] | 270549/29160 | 736/ 0 |
| Extended ZoneSorterByOffsetAndName [2] | 270613/29160 | 800/ 0 |
|----------------------------------------+--------------+--------------|
| Complete TimeZone (1 zone) | 270145/29508 | 10056/ 1616 |
| Complete TimeZone (2 zones) | 271425/30236 | 11336/ 2344 |
| CompleteZoneManager (1 zone) | 270289/29516 | 10200/ 1624 |
| CompleteZoneManager (all zones) | 350449/29516 | 90360/ 1624 |
| CompleteZoneManager (all zones+links) | 360017/29516 | 99928/ 1624 |
|----------------------------------------+--------------+--------------|
| Complete ZoneSorterByName [3] | 271041/29516 | 752/ 0 |
| Complete ZoneSorterByOffsetAndName [3] | 271089/29516 | 800/ 0 |
+---------------------------------------------------------------------+
AutoBenchmark was used to determine the CPU time consume by various features of the classes in this library. Two samples are shown below:
Arduino Nano:
+--------------------------------------------------+----------+
| Method | micros |
|--------------------------------------------------+----------|
| EmptyLoop | 5.000 |
|--------------------------------------------------+----------|
| LocalDate::forEpochDays() | 241.000 |
| LocalDate::toEpochDays() | 52.000 |
| LocalDate::dayOfWeek() | 49.000 |
|--------------------------------------------------+----------|
| OffsetDateTime::forEpochSeconds() | 361.000 |
| OffsetDateTime::toEpochSeconds() | 78.000 |
|--------------------------------------------------+----------|
| ZonedDateTime::toEpochSeconds() | 75.000 |
| ZonedDateTime::toEpochDays() | 63.000 |
| ZonedDateTime::forEpochSeconds(UTC) | 391.000 |
|--------------------------------------------------+----------|
| ZonedDateTime::forEpochSeconds(Basic_nocache) | 1710.000 |
| ZonedDateTime::forEpochSeconds(Basic_cached) | 707.000 |
| ZonedDateTime::forEpochSeconds(Extended_nocache) | -1.000 |
| ZonedDateTime::forEpochSeconds(Extended_cached) | -1.000 |
| ZonedDateTime::forEpochSeconds(Complete_nocache) | -1.000 |
| ZonedDateTime::forEpochSeconds(Complete_cached) | -1.000 |
|--------------------------------------------------+----------|
| ZonedDateTime::forComponents(Basic_nocache) | 2252.000 |
| ZonedDateTime::forComponents(Basic_cached) | 1254.000 |
| ZonedDateTime::forComponents(Extended_nocache) | -1.000 |
| ZonedDateTime::forComponents(Extended_cached) | -1.000 |
| ZonedDateTime::forComponents(Complete_nocache) | -1.000 |
| ZonedDateTime::forComponents(Complete_cached) | -1.000 |
|--------------------------------------------------+----------|
| ZonedExtra::forEpochSeconds(Basic_nocache) | 1386.000 |
| ZonedExtra::forEpochSeconds(Basic_cached) | 378.000 |
| ZonedExtra::forEpochSeconds(Extended_nocache) | -1.000 |
| ZonedExtra::forEpochSeconds(Extended_cached) | -1.000 |
| ZonedExtra::forEpochSeconds(Complete_nocache) | -1.000 |
| ZonedExtra::forEpochSeconds(Complete_cached) | -1.000 |
|--------------------------------------------------+----------|
| ZonedExtra::forComponents(Basic_nocache) | 2276.000 |
| ZonedExtra::forComponents(Basic_cached) | 1277.000 |
| ZonedExtra::forComponents(Extended_nocache) | -1.000 |
| ZonedExtra::forComponents(Extended_cached) | -1.000 |
| ZonedExtra::forComponents(Complete_nocache) | -1.000 |
| ZonedExtra::forComponents(Complete_cached) | -1.000 |
|--------------------------------------------------+----------|
| BasicZoneRegistrar::findIndexForName(binary) | 118.000 |
| BasicZoneRegistrar::findIndexForIdBinary() | 47.000 |
| BasicZoneRegistrar::findIndexForIdLinear() | 299.000 |
|--------------------------------------------------+----------|
| ExtendedZoneRegistrar::findIndexForName(binary) | -1.000 |
| ExtendedZoneRegistrar::findIndexForIdBinary() | -1.000 |
| ExtendedZoneRegistrar::findIndexForIdLinear() | -1.000 |
|--------------------------------------------------+----------|
| CompleteZoneRegistrar::findIndexForName(binary) | -1.000 |
| CompleteZoneRegistrar::findIndexForIdBinary() | -1.000 |
| CompleteZoneRegistrar::findIndexForIdLinear() | -1.000 |
+--------------------------------------------------+----------+
Iterations_per_run: 1000
ESP8266:
+--------------------------------------------------+----------+
| Method | micros |
|--------------------------------------------------+----------|
| EmptyLoop | 4.500 |
|--------------------------------------------------+----------|
| LocalDate::forEpochDays() | 7.500 |
| LocalDate::toEpochDays() | 4.000 |
| LocalDate::dayOfWeek() | 3.500 |
|--------------------------------------------------+----------|
| OffsetDateTime::forEpochSeconds() | 12.000 |
| OffsetDateTime::toEpochSeconds() | 7.000 |
|--------------------------------------------------+----------|
| ZonedDateTime::toEpochSeconds() | 6.500 |
| ZonedDateTime::toEpochDays() | 5.500 |
| ZonedDateTime::forEpochSeconds(UTC) | 13.500 |
|--------------------------------------------------+----------|
| ZonedDateTime::forEpochSeconds(Basic_nocache) | 141.500 |
| ZonedDateTime::forEpochSeconds(Basic_cached) | 21.500 |
| ZonedDateTime::forEpochSeconds(Extended_nocache) | 354.500 |
| ZonedDateTime::forEpochSeconds(Extended_cached) | 28.000 |
| ZonedDateTime::forEpochSeconds(Complete_nocache) | 407.000 |
| ZonedDateTime::forEpochSeconds(Complete_cached) | 28.000 |
|--------------------------------------------------+----------|
| ZonedDateTime::forComponents(Basic_nocache) | 159.000 |
| ZonedDateTime::forComponents(Basic_cached) | 46.000 |
| ZonedDateTime::forComponents(Extended_nocache) | 241.500 |
| ZonedDateTime::forComponents(Extended_cached) | 2.500 |
| ZonedDateTime::forComponents(Complete_nocache) | 354.000 |
| ZonedDateTime::forComponents(Complete_cached) | 2.500 |
|--------------------------------------------------+----------|
| ZonedExtra::forEpochSeconds(Basic_nocache) | 134.500 |
| ZonedExtra::forEpochSeconds(Basic_cached) | 11.000 |
| ZonedExtra::forEpochSeconds(Extended_nocache) | 308.000 |
| ZonedExtra::forEpochSeconds(Extended_cached) | 18.000 |
| ZonedExtra::forEpochSeconds(Complete_nocache) | 396.000 |
| ZonedExtra::forEpochSeconds(Complete_cached) | 17.500 |
|--------------------------------------------------+----------|
| ZonedExtra::forComponents(Basic_nocache) | 184.500 |
| ZonedExtra::forComponents(Basic_cached) | 48.500 |
| ZonedExtra::forComponents(Extended_nocache) | 268.000 |
| ZonedExtra::forComponents(Extended_cached) | 29.000 |
| ZonedExtra::forComponents(Complete_nocache) | 332.500 |
| ZonedExtra::forComponents(Complete_cached) | 5.000 |
|--------------------------------------------------+----------|
| BasicZoneRegistrar::findIndexForName(binary) | 18.000 |
| BasicZoneRegistrar::findIndexForIdBinary() | 6.500 |
| BasicZoneRegistrar::findIndexForIdLinear() | 43.000 |
|--------------------------------------------------+----------|
| ExtendedZoneRegistrar::findIndexForName(binary) | 24.500 |
| ExtendedZoneRegistrar::findIndexForIdBinary() | 6.000 |
| ExtendedZoneRegistrar::findIndexForIdLinear() | 50.500 |
|--------------------------------------------------+----------|
| CompleteZoneRegistrar::findIndexForName(binary) | 18.500 |
| CompleteZoneRegistrar::findIndexForIdBinary() | 6.500 |
| CompleteZoneRegistrar::findIndexForIdLinear() | 43.000 |
+--------------------------------------------------+----------+
Iterations_per_run: 2000
Tier 1: Fully supported
These boards are tested on each release:
Tier 2: Should work
These boards should work but I don't test them as often:
Tier 3: May work, but not supported
Tier Blacklisted
The following boards are not supported and are explicitly blacklisted to allow the compiler to print useful error messages instead of hundreds of lines of compiler errors:
This library was developed and tested using:
This library is not compatible with:
It should work with PlatformIO but I have not tested it.
The library works on Linux or MacOS (using both g++ and clang++ compilers) using the EpoxyDuino (https://github.com/bxparks/EpoxyDuino) emulation layer.
I use Ubuntu 22.04 for the vast majority of my development. I expect that the library will work fine under MacOS and Windows, but I have not tested them.
In the beginning, I created a digital clock using an Arduino Nano board, a small OLED display, and a DS3231 RTC chip. Everything worked, it was great. Then I wanted the clock to figure out the Daylight Saving Time (DST) automatically. And I wanted to create a clock that could display multiple timezones. Thus began my journey down the rabbit hole of timezones.
In full-featured operating systems (e.g. Linux, MacOS, Windows) and languages with timezone library support (e.g. Java, Python, JavaScript, C#, Go), the user has the ability to specify the Daylight Saving time (DST) transitions using 2 ways:
EST+5EDT,M3.2.0/2,M11.1.0/2
) that can be parsed programmatically, orAmerica/Los_Angeles
or Europe/London
) which identifies a set of timeThe problem with the POSIX format is that it is somewhat difficult for a human to understand, and the programmer must manually update this string when a timezone changes its DST transition rules. Also, there is no historical information in the POSIX string, so date and time written in the past cannot be accurately expressed. As far as I know, POSIX timezone strings can support only 2 DST transitions per year. However, there are a handful of timezones which have (or used to have) 4 timezone transitions in a single year, which cannot be represented by a POSIX string. Most Arduino timezone libraries use the POSIX format (e.g. ropg/ezTime or JChristensen/Timezone) for simplicity and smaller memory footprint.
The libraries that incorporate the full IANA TZ Database are often far too large
to fit inside the resource constrained Arduino environments. The AceTime library
has been optimized to reduce the flash memory size of the library as much as
possible. The application can choose to load only of the smallest subset of the
TZ Database that is required to support the selected timezones (1 to 4 have been
extensively tested). Dynamic lookup of the time zone is possible using the
ZoneManager
, and the app develop can customize it with the list of zones that
are compiled into the app. On microcontrollers with more than about 32kB of
flash memory (e.g. ESP8266, ESP32, Teensy 3.2) and depending on the size of the
rest of the application, it may be possible to load the entire IANA TZ database.
This will allow the end-user to select the timezone dynamically, just like
desktop-class machines.
When new versions of the database are released (several times a year), I can regenerate the zone files, recompile the application, and it will instantly use the new transition rules, without the developer needing to create a new POSIX string.
The AceTime library is inspired by and borrows from:
The names and API of AceTime classes are heavily borrowed from the Java JDK 11
java.time
package. Some important differences come from the fact that in Java, most
objects are reference objects and created on the heap. To allow AceTime to work
on an Arduino chip with only 2kB of RAM and 32kB of flash, the AceTime C++
classes perform no heap allocations (i.e. no calls to operator new()
or
malloc()
). Many of the smaller classes in the library are expected to be used
as "value objects", in other words, created on the stack and copied by value.
Fortunately, the C++ compilers are extremely good at optimizing away unnecessary
copies of these small objects. It is not possible to remove all complex memory
allocations when dealing with the TZ Database. In the AceTime library, I managed
to move most of the complex memory handling logic into the ZoneProcessor
class
hierarchy. These are relatively large objects which are meant to be opaque
to the application developer, created statically at start-up time of
the application, and never deleted during the lifetime of the application.
The Arduino Time Library uses a set of
C functions similar to the traditional C/Unix library
methods (e.g makeTime()
and
breaktime()
). It also uses the Unix epoch of 1970-01-01T00:00:00Z and a
int32_t
type as its time_t
to track the number of seconds since the epoch.
That means that the largest date it can handle is 2038-01-19T03:14:07Z. AceTime
uses an epoch that starts on 2000-01-01T00:00:00Z using the same int32_t
as
its ace_time::acetime_t
, which means that maximum date increases to
2068-01-19T03:14:07Z. AceTime is also quite a bit faster than the Arduino Time
Library (although in most cases, performance of the Time Library is not an
issue): AceTime is 2-5X faster on an ATmega328P, 3-5X faster on the
ESP8266, 7-8X faster on the ESP32, and 7-8X faster on the Teensy ARM
processor.
AceTime aims to be the smallest library that can run on the basic Arduino platform (e.g. Nano with 32kB flash and 2kB of RAM) that fully supports all timezones in the TZ Database at compile-time. Memory constraints of the smallest Arduino boards may limit the number of timezones supported by a single program at runtime to 1-3 timezones. The library also aims to be as portable as possible, and supports AVR microcontrollers, as well as ESP8266, ESP32 and Teensy microcontrollers.
The AceTime library can be substantially faster than the equivalent methods in
the Arduino Time Library. The
ComparisonBenchmark.ino program compares the
CPU run time of LocalDateTime::forEpochSeconds()
and
LocalDateTime::toEpochSeconds()
with the equivalent breakTime()
and
makeTime()
functions of the Arduino Time Library. Details are given in the
ComparisonBenchmark/README.md file.
Two examples for the Arduino Nano and ESP8266 are shown below:
Nano
+----------------------------------------+----------+
| Method | micros |
|----------------------------------------+----------|
| EmptyLoop | 5.000 |
|----------------------------------------+----------|
| LocalDateTime::forEpochSeconds() | 270.000 |
| breakTime() | 594.500 |
|----------------------------------------+----------|
| LocalDateTime::toEpochSeconds() | 66.500 |
| makeTime() | 344.500 |
+----------------------------------------+----------+
ESP8266
+----------------------------------------+----------+
| Method | micros |
|----------------------------------------+----------|
| EmptyLoop | 0.800 |
|----------------------------------------+----------|
| LocalDateTime::forEpochSeconds() | 13.100 |
| breakTime() | 42.600 |
|----------------------------------------+----------|
| LocalDateTime::toEpochSeconds() | 4.500 |
| makeTime() | 24.800 |
+----------------------------------------+----------+
Some version of the standard Unix/C library <time.h>
is available in some
Arduino platforms, but not others:
gmtime()
to convert time_t
integer into datestruct tm
,mk_gmtime()
to convert components into a time_t
time_t
integer is unsigned, and starts at 2000-01-01T00:00:00 UTCtime()
value does not auto-increment. The system_tick()
function<time.h>
library.<time.h>
library.
time()
function automatically increments through thesystem_get_time()
system call.time()
function.configTime()
functions to configure the behavior of theTZ.h
containing pre-calculated POSIX timezone strings.These libraries are all based upon the traditional C/Unix library methods which can be difficult to understand.
The ESP8266 platform provides a
cores/esp8266/TZ.h
file which contains a list of pre-generated POSIX timezone strings. These can be
passed into the configTime()
function that initializes the SNTP service.
The ESP32 platform does not provide a TZ.h
file as far as I can tell. I
believe the same POSIX strings can be passed into its configTime()
function.
But POSIX timezone strings have limitations that I described in
Motivation, and the C-style library functions are often
confusing and hard to use.
Application developers can choose to use the built-in SNTP service and the
time()
function on the ESP8266 and ESP32 as the source of accurate clock, but
take advantage of the versatility and power of the AceTime library for timezone
conversions. AceTime v1.10 adds the forUnixSeconds64()
and
toUnixSeconds64()
methods in various classes to make it far easier to interact
with the 64-bit time_t
integers returned by the time()
function on these
platforms.
See EspTime for an example of how to integrate AceTime with
the built-in SNTP service and time()
function on the ESP8266 and ESP32. See
also the
EspSntpClock
class in the AceTimeClock project which provides a thin-wrapper around this
service on the ESP platforms.
The ezTime is a library that seems to be composed of 2 parts: A client library that runs on the microcontroller and a server part that provides a translation from the timezone name to the POSIX DST transition string. Unfortunately, this means that the controller must have network access for this library to work. I wanted to create a library that was self-contained and could run on an Arduino Nano with just an RTC chip without a network shield.
The Micro Time Zone is a pure-C library
that can compile on the Arduino platform. It contains a limited subset of the TZ
Database encoded as C structs and determines the DST transitions using the
encoded structs. It supports roughly of 45 zones with just a 3kB tzinfo
database. The initial versions of AceTime, particularly the BasicZoneProcessor
class was directly inspired by this library. It would be interesting to run this
library to the same set of "validation" unit tests that checks the AceTime logic
and see how accurate this library is. One problem with Micro Time Zone library
is that it loads the entire tzinfo database for all 45 time zones, even if only
one zone is used. Therefore, the AceTime library will consume less flash memory
for the database part if only a handful of zones are used. But the AceTime
library contains more algorithmic code so will consume more flash memory. It is
not entirely clear which library is smaller for 1-3 time zones. (This may be an
interesting investigation the future.)
The names and functionality of most the date and time classes (LocalTime
,
LocalDate
, LocalDateTime
, OffsetDateTime
, and ZonedDateTime
) were
inspired by the architecture of the Java 11
java.time
package. However, there were many parts of the java.time
package that were not
appropriate for a microcontroller environment with small memory. Those were
implemented in other ways. There were other parts that seemed better implemented
by Joda-Time or Noda
Time). I picked the ones that I liked.
Those other libraries (java.time
, Joda-Time, and Noda Time) all provide
substantially more fine-grained class hierarchies to provider better
type-safety. For example, those libraries just mentioned provided an Instant
class, a Duration
class, an Interval
class. The java.time
package also
provides other fine-grained classes such as OffsetTime
, OffsetDate
, Year
,
Month
, MonthDay
classes. For the AceTime library, I decided to avoid
providing too many classes. The API of the library is already too large, I did
not want to make them larger than necessary.
The date package by Howard Hinnant is
based upon the <chrono>
standard library and consists of several libraries of
which date.h
and tz.h
are comparable to AceTime. Modified versions of these
libraries were voted into the C++20 standard.
Unfortunately these libraries are not suitable for an Arduino microcontroller environment because:
tz.h
library has the option of downloading the TZ Database files overlibcurl
to the OS filesystem then parsing the files, or<iostream>
and<chrono>
which don't exist on most Arduino platforms.The Hinnant date libraries were invaluable for writing the
HinnantBasicTest
and
HinnantExtendedTest
validation tests which compare the AceTime algorithms to the Hinnant Date
algorithms. For all times zones between the years 2000 until 2100, the AceTime
date-time components (ZonedDateTime
) and abbreviations (ZonedExtra
)
calculated from the given epochSeconds match the results from the Hinnant Date
libraries.
The cctz library from Google is also based on
the <chrono>
library. I have not looked at this library closely because I
assumed that it would not fit inside an Arduino controller. Hopefully I will
get some time to take a closer look in the future.
If you have any questions, comments, or feature requests for this library, please use the GitHub Discussions for this project. If you have bug reports, please file a ticket in GitHub Issues. Feature requests should go into Discussions first because they often have alternative solutions which are useful to remain visible, instead of disappearing from the default view of the Issue tracker after the ticket is closed.
Please refrain from emailing me directly unless the content is sensitive. The problem with email is that I cannot reference the email conversation when other people ask similar questions later.