This library provides date and time classes for the Arduino platform with support for time zones in the IANA TZ database. Date and time from one timezone can be converted to another timezone. The library also provides a SystemClock that can be synchronized from a more reliable external time source, such as an NTP server or a DS3231 RTC chip. This library can be an alternative to the Arduino Time (https://github.com/PaulStoffregen/Time) and Arduino Timezone (https://github.com/JChristensen/Timezone) libraries.
The AceTime classes are organized into roughly 4 bundles, placed in different C++ namespaces:
- date and time classes and types
ace_time::acetime_t
ace_time::DateStrings
ace_time::LocalTime
ace_time::LocalDate
ace_time::LocalDateTime
ace_time::TimeOffset
ace_time::OffsetDateTime
ace_time::ZoneProcessor
ace_time::BasicZoneProcessor
ace_time::ExtendedZoneProcessor
ace_time::TimeZone
ace_time::ZonedDateTime
ace_time::TimePeriod
ace_time::BasicZone
ace_time::ExtendedZone
ace_time::ZoneManager
ace_time::BasicZoneManager
ace_time::ExtendedZoneManager
ace_time::ManualZoneManager
- mutation helpers
ace_time::local_date_mutation::
ace_time::time_offset_mutation::
ace_time::time_period_mutation::
ace_time::zoned_date_time_mutation::
- TZ Database zone files
- data structures generated from the TZ Database zone files
- intended to be used as opaque data objects
ace_time::zonedb
(266 zones and 183 links)ace_time::zonedb::kZoneAfrica_Abidjan
ace_time::zonedb::kZoneAfrica_Accra
- ...
ace_time::zonedb::kZonePacific_Wake
ace_time::zonedb::kZonePacific_Wallis
ace_time::zonedbx
(386 zones and 207 links)ace_time::zonedbx::kZoneAfrica_Abidjan
ace_time::zonedbx::kZoneAfrica_Accra
- ...
ace_time::zonedbx::kZonePacific_Wake
ace_time::zonedbx::kZonePacific_Wallis
- system clock classes
ace_time::clock::Clock
ace_time::clock::DS3231Clock
ace_time::clock::NtpClock
ace_time::clock::StmRtcClock
(experimental as of v1.4)ace_time::clock::SystemClock
ace_time::clock::SystemClockCoroutine
ace_time::clock::SystemClockLoop
- internal helper classes (not normally used by app developers)
ace_time::basic::ZoneContext
ace_time::basic::ZoneEra
ace_time::basic::ZoneInfo
ace_time::basic::ZonePolicy
ace_time::basic::ZoneRule
ace_time::extended::ZoneContext
ace_time::extended::ZoneInfo
ace_time::extended::ZoneEra
ace_time::extended::ZonePolicy
ace_time::extended::ZoneRule
The "date and time" classes provide an abstraction layer to make it easier
to use and manipulate date and time fields. For example, each of the
LocalDateTime
, OffsetDateTime
and ZonedDateTime
classes provide the
toEpochSeconds()
method which returns the number of seconds from an epoch
date, the forEpochSeconds()
method which constructs the date and time fields
from the epoch seconds, the forComponents()
method which constructs the object
from the individual (year, month, day, hour, minute, second) components, and the
dayOfWeek()
method which returns the day of the week of the given date.
The Epoch in AceTime is defined to be 2000-01-01T00:00:00Z, in contrast to the
Epoch in Unix which is 1970-01-01T00:00:00Z. Internally, the current time is
represented as "seconds from Epoch" stored as a 32-bit signed integer
(acetime_t
aliased to int32_t
). The smallest 32-bit signed integer (-2^31
)
is used to indicate an internal Error condition, so the range of valid
acetime_t
value is -2^31+1
to 2^31-1
. Therefore, the range of dates that
the acetime_t
type can handle is 1931-12-13T20:45:53Z to 2068-01-19T03:14:07Z
(inclusive). (In contrast, the 32-bit Unix time_t
range is
1901-12-13T20:45:52Z to 2038-01-19T03:14:07Z which is the cause of the Year
2038 Problem).
The various date classes (LocalDate
, LocalDateTime
, OffsetDateTime
,
ZonedDateTime
) store the year component internally as a signed 8-bit integer
offset from the year 2000. The range of this integer is -128 to +127, but -128
is used to indicate an internal Error condition, so the actual range is -127 to
+127. Therefore, these classes can represent dates from 1873-01-01T00:00:00 to
2127-12-31T23:59:59 (inclusive). Notice that these classes can represent all
dates that can be expressed by the acetime_t
type, but the reverse is not
true. There are date objects that cannot be converted into a valid acetime_t
value. To be safe, users of this library should stay at least 1 day away from
the lower and upper limits of acetime_t
(i.e. stay within the year 1932 to
2067 inclusive).
The ZonedDateTime
class works with the TimeZone
class to implement the DST
transition rules defined by the TZ Database. It also allows conversions to other
timezones using the ZonedDateTime::convertToTimeZone()
method.
The library provides 2 sets of zoneinfo files created from the IANA TZ Database:
- zonedb/zone_infos.h contains
kZone*
declarations (e.g.kZoneAmerica_Los_Angeles
) for 270 zones and 182 links from the year 2000 until 2050. These zones have (relatively) simple time zone transition rules, which can handled by theBasicZoneProcessor
class. - zonedbx/zone_infos.h contains
kZone*
declarations (e.g.kZoneAfrica_Casablanca
) for 386 zones and 207 links in the TZ Database (essentially the entire database) from the year 2000 until 2050. These are intended to be used with theExtendedZoneProcessor
class.
The zoneinfo files (and their associated ZoneProcessor
classes) have a
resolution of 1 minute, which is sufficient to represent all UTC offsets and DST
shifts of all timezones after 1972 (Africa/Monrovia was the last timezone to
switch to UTC time on Jan 7, 1972). These zoneinfo files and the algorithms in
this library have been validated to match the UTC offsets calculated using 4
other date/time libraries:
- the Python pytz (https://pypi.org/project/pytz/) library from the year 2000 until 2037 (inclusive),
- the Python dateutil (https://pypi.org/project/python-dateutil/) library from the year 2000 until 2037 (inclusive),
- the Java JDK 11 java.time library from year 2000 to 2049 (inclusive),
- the C++11/14/17 Hinnant date (https://github.com/HowardHinnant/date) library from year 1975 to 2049 (inclusive).
Custom datasets with smaller or larger range of years may be generated by
developers using scripts provided in this library (although this is not
documented currently). The target application may be compiled against the custom
dataset instead of using zonedb::
and zonedbx::
zone files provided in this
library.
It is expected that most applications using AceTime will use only a small number
of timezones at the same time (1 to 4 zones have been extensively tested) and
that this set is known at compile-time. The C++ compiler will include only the
subset of zoneinfo files needed to support those timezones, instead of compiling
in the entire TZ Database. But on microcontrollers with enough memory, the
ZoneManager
can be used to load the entire TZ Database into the app and
the TimeZone
objects can be dynamically created as needed.
Each timezone in the TZ Database is identified by its fully qualified zone name
(e.g. "America/Los_Angeles"
). On small microcontroller environments, these
strings can consume precious memory (e.g. 30 bytes for
"America/Argentina/Buenos_Aires"
) and are not convenient to serialize over the
network or to save to EEPROM. Therefore, the AceTime library provides each
timezone with an alternative zoneId
identifier of type uint32_t
which is
guaranteed to be unique and stable. For example, the zoneId for
"America/Los_Angeles"
is provided by zonedb::kZoneIdAmerica_Los_Angeles
or
zonedbx::kZoneIdAmerica_Los_Angele
which both have the value 0xb7f7e8f2
. A
TimeZone
object can be saved as a zoneId
and then recreated using the
ZoneManager::createFromZoneId()
method.
The ace_time::clock
classes collaborate together to implement the
SystemClock which can obtain its time from various sources, such as a DS3231 RTC
chip, or an Network Time Protocol (NTP) server. Retrieving the current time
from accurate clock sources can be expensive, so the SystemClock
uses the
built-in millis()
function to provide fast access to a reasonably accurate
clock, but synchronizes to more accurate clocks periodically.
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.
The zoneinfo files 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
- 266 Zones
- 13 kB (8-bit processor)
- 17 kB (32-bit processor)
- 266 Zones and 183 Links
- 22 kB (8-bit processor)
- 27 kB (32-bit processor)
- 266 Zones
ExtendedZoneProcessor
- 386 Zones
- 22 kB (8-bit processor)
- 30 kB (32-bit processor)
- 386 Zones and 207 Links
- 30 kB (8-bit processor)
- 37 kB (32-bit processor)
- 386 Zones
Normally a small application will use only a small number of timezones. The
AceTime library with one timezone using the BasicZoneProcessor
and the
SystemClock
consumes:
- 9-10 kB of flash and 4-500 bytes of RAM on an 8-bit AVR processors,
- 6-23 kB of flash and 900-1800 bytes of RAM on a 32-bit processors.
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 an Arduino Pro Micro controller.
Conversion from date-time components (year, month, day, etc) to epochSeconds
(ZonedDateTime::toEpochSeconds()
) takes about:
- ~90 microseconds on an 8-bit AVR,
- ~17 microseconds on a SAMD21,
- ~4 microseconds on an STM32 Blue Pill,
- ~7 microseconds on an ESP8266,
- ~1.4 microseconds on an ESP32,
- ~0.4 microseconds on a Teensy 3.2.
Conversion from an epochSeconds to date-time components including timezone
(ZonedDateTime::forEpochSeconds()
) takes (assuming cache hits):
- ~600 microseconds on an 8-bit AVR,
- ~70 microseconds on an SAMD21,
- ~10-11 microseconds on an STM32 Blue Pill,
- ~27 microseconds on an ESP8266,
- ~2.5 microseconds on an ESP32,
- ~5-6 microseconds on a Teensy 3.2.
The creation of a TimeZone from its zoneName or its zoneId using a
BasicZoneManager
configured with a custom ZoneRegistry with 85 zones takes:
- 36-400 microseconds, for an 8-bit AVR,
- 4-14 microseconds, for a SAMD21
- 3-18 microseconds for an STM32 Blue Pill,
- 7-50 microseconds for an ESP8266,
- 0.6-3 microseconds for an ESP32,
- 3-10 microseconds for a Teensy 3.2.
Version: 1.6 (2021-02-17, TZ DB version 2021a)
Changelog: CHANGELOG.md
IMPORTANT CHANGE for v1.2: This library now depends on the the "AceCommon" library for some of its low-level routines. See the USER_GUIDE.md for installation instructions.
Here is a simple program (see examples/HelloDateTime) which demonstrates how to create and manipulate date and times in different time zones:
#include <AceTime.h>
using namespace ace_time;
// ZoneProcessor instances should be created statically at initialization time.
static BasicZoneProcessor pacificProcessor;
static BasicZoneProcessor londonProcessor;
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Wait until Serial is ready - Leonardo/Micro
auto pacificTz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles,
&pacificProcessor);
auto londonTz = TimeZone::forZoneInfo(&zonedb::kZoneEurope_London,
&londonProcessor);
// Create from components. 2019-03-10T03:00:00 is just after DST change in
// Los Angeles (2am goes to 3am).
auto startTime = ZonedDateTime::forComponents(
2019, 3, 10, 3, 0, 0, pacificTz);
Serial.print(F("Epoch Seconds: "));
acetime_t epochSeconds = startTime.toEpochSeconds();
Serial.println(epochSeconds);
Serial.print(F("Unix Seconds: "));
acetime_t unixSeconds = startTime.toUnixSeconds();
Serial.println(unixSeconds);
Serial.println(F("=== Los_Angeles"));
auto pacificTime = ZonedDateTime::forEpochSeconds(epochSeconds, pacificTz);
Serial.print(F("Time: "));
pacificTime.printTo(Serial);
Serial.println();
Serial.print(F("Day of Week: "));
Serial.println(
DateStrings().dayOfWeekLongString(pacificTime.dayOfWeek()));
// Print info about UTC offset
TimeOffset offset = pacificTime.timeOffset();
Serial.print(F("Total UTC Offset: "));
offset.printTo(Serial);
Serial.println();
// Print info about the current time zone
Serial.print(F("Zone: "));
pacificTz.printTo(Serial);
Serial.println();
// Print the current time zone abbreviation, e.g. "PST" or "PDT"
Serial.print(F("Abbreviation: "));
Serial.print(pacificTz.getAbbrev(epochSeconds));
Serial.println();
// Create from epoch seconds. London is still on standard time.
auto 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. "PST" or "PDT"
Serial.print(F("Abbreviation: "));
Serial.print(londonTz.getAbbrev(epochSeconds));
Serial.println();
Serial.println(F("=== Compare ZonedDateTime"));
Serial.print(F("pacificTime.compareTo(londonTime): "));
Serial.println(pacificTime.compareTo(londonTime));
Serial.print(F("pacificTime == londonTime: "));
Serial.println((pacificTime == londonTime) ? "true" : "false");
}
void loop() {
}
Running this should produce the following on the Serial port:
Epoch Seconds: 605527200
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
pacificTime.compareTo(londonTime): 0
pacificTime == londonTime: false
The examples/HelloZoneManager example shows how to
load the entire TZ Database into a BasicZoneManager
, then create 3 time zones
using 3 different ways: createForZoneInfo()
, createForZoneName()
, and
createForZoneId()
.
#include <AceTime.h>
using namespace ace_time;
// Create a BasicZoneManager with the entire TZ Database.
static const int CACHE_SIZE = 3;
static BasicZoneManager<CACHE_SIZE> manager(
zonedb::kZoneRegistrySize, zonedb::kZoneRegistry);
void setup() {
Serial.begin(115200);
while (!Serial); // Wait Serial is ready - Leonardo/Micro
// Create Los Angeles by ZoneInfo
auto pacificTz = manager.createForZoneInfo(&zonedb::kZoneAmerica_Los_Angeles);
auto pacificTime = ZonedDateTime::forComponents(
2019, 3, 10, 3, 0, 0, pacificTz);
pacificTime.printTo(Serial);
Serial.println();
// Create London by ZoneName
auto londonTz = manager.createForZoneName("Europe/London");
auto londonTime = pacificTime.convertToTimeZone(londonTz);
londonTime.printTo(Serial);
Serial.println();
// Create Sydney by ZoneId
auto sydneyTz = manager.createForZoneId(zonedb::kZoneIdAustralia_Sydney);
auto sydneyTime = pacificTime.convertToTimeZone(sydneyTz);
sydneyTime.printTo(Serial);
Serial.println();
}
void loop() {
}
This consumes about 25kB of flash, which means that it can run on an Arduino Nano or Micro . 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]
This is the example code for using the SystemClock
taken from
examples/HelloSystemClock.
#include <AceTime.h>
using namespace ace_time;
using namespace ace_time::clock;
// ZoneProcessor instances should be created statically at initialization time.
static BasicZoneProcessor pacificProcessor;
static SystemClockLoop systemClock(nullptr /*reference*/, nullptr /*backup*/);
void setup() {
delay(1000);
Serial.begin(115200);
while (!Serial); // Wait until Serial is ready - Leonardo/Micro
systemClock.setup();
// Creating timezones is cheap, so we can create them on the fly as needed.
auto pacificTz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles,
&pacificProcessor);
// Set the SystemClock using these components.
auto pacificTime = ZonedDateTime::forComponents(
2019, 6, 17, 19, 50, 0, pacificTz);
systemClock.setNow(pacificTime.toEpochSeconds());
}
void printCurrentTime() {
acetime_t now = systemClock.getNow();
// Create a time
auto pacificTz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles,
&pacificProcessor);
auto pacificTime = ZonedDateTime::forEpochSeconds(now, pacificTz);
pacificTime.printTo(Serial);
Serial.println();
}
// Do NOT use delay() here.
void loop() {
static acetime_t prevNow = systemClock.getNow();
systemClock.loop();
acetime_t now = systemClock.getNow();
if (now - prevNow >= 2) {
printCurrentTime();
prevNow = now;
}
}
This will start by setting the SystemClock to 2019-06-17T19:50:00-07:00, then printing the system time every 2 seconds:
2019-06-17T19:50:00-07:00[America/Los_Angeles]
2019-06-17T19:50:02-07:00[America/Los_Angeles]
2019-06-17T19:50:04-07:00[America/Los_Angeles]
...
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:
- README.md - this file
- USER_GUIDE.md
- Installation
- Documentation
- Motivation and Design Considerations
- Date and Time classes
- Mutations
- Error Handling
- Clocks
- Testing
- Benchmarks
- Comparison to Other Libraries
- Bugs and Limitations
- Doxygen docs hosted on GitHub Pages
The library is tested on the following boards:
- Arduino Nano clone (16 MHz ATmega328P)
- SparkFun Pro Micro clone (16 MHz ATmega32U4)
- SAMD21 M0 Mini (48 MHz ARM Cortex-M0+)
- STM32 Blue Pill (STM32F103C8, 72 MHz ARM Cortex-M3)
- NodeMCU 1.0 (ESP-12E module, 80 MHz ESP8266)
- WeMos D1 Mini (ESP-12E module, 80 MHz ESP8266)
- ESP32 dev board (ESP-WROOM-32 module, 240 MHz dual core Tensilica LX6)
- Teensy 3.2 (96 MHz ARM Cortex-M4)
I will occasionally test on the following hardware as a sanity check:
- Mini Mega 2560 (Arduino Mega 2560 compatible, 16 MHz ATmega2560)
- Teensy LC (48 MHz ARM Cortex-M0+)
The following boards are not supported:
- megaAVR (e.g. Nano Every)
- SAMD21 boards w/
arduino:samd
version >= 1.8.10 (e.g. MKRZero)
This library was developed and tested using:
- Arduino IDE 1.8.13
- Arduino CLI 0.14.0
- Arduino AVR Boards 1.8.3
- Arduino SAMD Boards 1.8.9
- SparkFun AVR Boards 1.1.13
- SparkFun SAMD Boards 1.8.1
- STM32duino 1.9.0
- ESP8266 Arduino 2.7.4
- ESP32 Arduino 1.0.4
- Teensydino 1.53
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 18.04 and 20.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.
If you find this library useful, consider starring this project on GitHub. The stars will let me prioritize the more popular libraries over the less popular ones.
If you have any questions, comments, bug reports, or feature requests, please file a GitHub ticket instead of emailing me unless the content is sensitive. (The problem with email is that I cannot reference the email conversation when other people ask similar questions later.) I'd love to hear about how this software and its documentation can be improved. I can't promise that I will incorporate everything, but I will give your ideas serious consideration.
- Created by Brian T. Park ([email protected]).
- Support an existing WiFi connection in
NtpClock
by denis-stepanov@ #24. - Support for STM32RTC through the
ace_time::clock::StmRtcClock
class by Anatoli Arkhipenko (arkhipenko@) #39.