The primary purpose of AceTime classes is to convert between an integer
representing the number of seconds since the AceTime Epoch (2050-01-01T00:00:00
UTC) and the equivalent human-readable components in different timezones.
The epoch year is adjustable using the Epoch::currentEpochYear(year)
. This
sets the epoch to be {year}-01-01T00:00:00 UTC
The epoch seconds is represented by an int32_t
integer (instead of an
int64_t
used in most modern timezone libraries) to save resources on 8-bit
processors. The range of a 32-bit integer is about 132 years which allows most
features of the AceTime library to work across about a 100-year interval
straddling the current epoch year.
The IANA TZ database is programmatically generated into the src/zonedb
and
src/zonedbx
subdirectory from the raw IANA TZ files. The database entries are
valid from the years [2000,10000)
. By adjusting the currentEpochYear()
, the
library will work across any 100 year interval across the 8000 year range of the
TZ database.
Version: 2.2.1 (2023-03-24, TZDB 2023b)
Related Documents:
- README.md: introductory background
- Doxygen docs hosted on GitHub
- Overview
- Headers and Namespaces
- Date and Time Classes
- TimeZone Classes
- ZoneInfo Database
- Zone Sorting
- Print To String
- Mutations
- Error Handling
- Bugs and Limitations
The Date, Time, and TimeZone classes provide an abstraction layer to make it easier to use and manipulate date and time fields, in different time zones. It is difficult to organize the various parts of this library in the most easily digestible way, but perhaps they can be categorized into three parts:
- Simple Date and Time classes for converting date and time fields to and from the "epoch seconds",
- TimeZone related classes which come in 2 forms:
- classes that extend the simple Date and Time classes that account for time
zones which can be described in a time zone database (e.g.
TimeZone
,ZonedDateTime
,ZoneProcessor
) - classes and types that manage the TZ Database and provide access to its
data (e.g.
ZoneManager
,ZoneInfo
,ZoneId
)
- classes that extend the simple Date and Time classes that account for time
zones which can be described in a time zone database (e.g.
- The ZoneInfo Database generated from the IANA TZ Database that contains UTC offsets and the rules for determining when DST transitions occur for a particular time zone
First we start with LocalDate
and LocalTime
classes which capture the simple
date and time fields respectively. They combine together to form the
LocalDateTime
class which contains all date and time fields.
The TimeOffset
class represents a simple shift in time, for example, +1h or
-4:30 hours. It can be used to represent a UTC offset, or a DST offset. The
TimeOffset
class combines with the LocalDateTime
class to form the
OffsetDateTime
classes which represents a date and time that has been shifted
from UTC some offset.
Both the LocalDateTime
and OffsetDateTime
(and later ZonedDateTime
)
classes provide the toEpochSeconds()
method which returns the number of
seconds from an epoch date, the forEpochSeconds()
method which constructs the
,ate and time fields from the epoch seconds. They also provide the
forComponents()
method which constructs the object from the individual (year,
month, day, hour, minute, second) components.
The Epoch in AceTime defaults to 2050-01-01T00:00:00 UTC, in contrast to the
Epoch in Unix which is 1970-01-01T00:00:00 UTC. 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 about 132 years, and the largest date is
2118-01-20T03:14:07 UTC. (In contrast, the 32-bit Unix time_t
range is
1901-12-13T20:45:52 UTC to 2038-01-19T03:14:07 UTC which is the cause of the
Year 2038 Problem).
The various date classes (LocalDate
, LocalDateTime
, OffsetDateTime
) store
the year component internally as a signed 16-bit integer valid from year 1 to
year 9999. 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.
Most timezone related functions of the library use the int32_t
epochseconds
for its internal calculations, so the date range should be constrained to +/- 68
years of the current epoch. The timezone calculations require some additional
buffers at the edges of the range (1-3 years), so the actual range of validity
is about +/- 65 years. To be very conservative, client applications are advised
to limit the date range to about 100 years, in other words, about +/- 50 years
from the current epoch year. Using the default epoch year of 2050, the
recommended range is [2000,2100)
.
The TimeZone
class a real or abstract place or region whose local time is
shifted from UTC by some amount. It is combined with the OffsetDateTime
class
to form the ZonedDateTime
class. The ZonedDateTime
allows conversions to
other timezones using the ZonedDateTime::convertToTimeZone()
method.
The TimeZone
object can be defined using the data and rules defined by the
IANA TZ Database. AceTime provides 2
different algorithms to process this database:
BasicZoneProcessor
- simpler and smaller, but supports only about 70% of the timezones defined by the IANA TZ Database
ExtendedZoneProcessor
- bigger and more complex and handles the entire TZ database
Access to the two sets data in the ZoneInfo Database is provided by:
BasicZoneManager
:- contains a registry of the basic ZoneInfo data structures
- holds a cache of
BasicZoneProcessor
ExtendedZoneManager
:- contains a registry of the extended ZoneInfo data structures
- the holds a cache of
ExtendedZoneProcessor
The official IANA TZ Database is processed and converted into an internal
AceTime database that we will call the ZoneInfo Database (to distinguish it from
the IANA TZ Database). The ZoneInfo Database contains statically defined C++
data structures, which each timezone in the TZ Database being represented by a
ZoneInfo
data structure.
Two slightly different sets of ZoneInfo entries are generated, under 2 different directories, using 2 different C++ namespaces to avoid cross-contamination:
- zonedb/zone_infos.h
- intended for
BasicZoneProcessor
orBasicZoneManager
- 266 zones and 183 links (as of version 2021a) from the year 2000 until 10000, about 70% of the full IANA TZ Database
- contains
kZone*
declarations (e.g.kZoneAmerica_Los_Angeles
) - contains
kZoneId*
identifiers (e.g.kZoneIdAmerica_Los_Angeles
) - slightly smaller and slightly faster
- intended for
- zonedbx/zone_infos.h
- intended for
ExtendedZoneProcessor
orExtendedZoneManager
- all 386 zones and 207 links (as of version 2021a) in the IANA TZ Database from the year 2000 until 10000
- contains
kZone*
declarations (e.g.kZoneAfrica_Casablanca
) - contains
kZoneId*
identifiers (e.g.kZoneIdAfrica_Casablanca
)
- intended for
The internal helper classes which are used to encode the ZoneInfo Database information are defined in the following namespaces. They are not expected to be used by application developers under normal circumstances, so these are listed here for reference:
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 ZoneInfo entries (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 seems like the last timezone
to conform to a one-minute resolution on Jan 7, 1972).
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 entries needed to support those timezones, instead of
compiling in the entire ZoneInfo Database. But on microcontrollers with enough
memory, the ZoneManager
can be used to load the entire ZoneInfo Database into
the app and the TimeZone
objects can be dynamically created as needed.
Each timezone in the ZoneInfo 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.
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 BasicZoneManager::createForZoneId()
or ExtendedZoneManager::createForZoneId()
method.
Only a single header file AceTime.h
is required to use this library.
To use the AceTime classes without prepending the namespace prefixes, use
the following using
directive:
#include <AceTime.h>
using namespace ace_time;
To use the Basic ZoneInfo data structures needed by BasicZoneProcessor
and
BasicZoneManager
, you will need:
using namespace ace_time::zonedb;
To use the Extended ZoneInfo data structures needed by ExtendedZoneProcessor
and ExtendedZoneManager
, you will need:
using namespace ace_time::zonedbx;
The following C++ namespaces are usually internal implementation details which are not normally needed by the end users:
ace_time::basic
: for creating custom zone registries forBasicZoneManager
ace_time::extended
: for creating custom zone registries forExtendedZoneManager
ace_time::internal
One of the fundamental types in AceTime is the acetime_t
defined as:
namespace ace_time {
typedef int32_t acetime_t;
}
This represents the number of seconds since the Epoch. In AceTime, the
Epoch is defined by default to be 2050-01-01 00:00:00 UTC time. In contrast, the
Unix Epoch is defined to be 1970-01-01 00:00:00 UTC. Since acetime_t
is a
32-bit signed integer, the largest value is 2,147,483,647. Therefore, the
largest date that can be represented as an epoch seconds is 2118-01-20T03:14:07
UTC. However for various reasons, client applications are recommended to stay
within a 100-year interval [2000,2100)
.
The acetime_t
is analogous to the time_t
type in the standard C library,
with several major differences:
- The
time_t
does not exist on all Arduino platforms. - Some Arduino platforms and older Unix platforms use a 32-bit
int32_t
to representtime_t
. - Modern implementations (e.g. ESP8266 and ESP32) use a 64-bit
int64_t
to representtime_t
to prevent the "Year 2038" overflow problem. Unfortunately, AceTime does use 64-bit integers internally to avoid consuming flash memory on 8-bit processors. - Most
time_t
implementations uses the Unix Epoch of 1970-01-01 00:00:00 UTC. AceTime uses an epoch of 2050-01-01 00:00:00 UTC (by default).
It is possible to convert between a time_t
and an acetime_t
by adding or
subtracting the number of seconds between the 2 Epoch dates. This value is given
by Epoch::secondsToCurrentEpochFromUnixEpoch64()
which returns an int64_t
value to allow epoch years greater than 2028. If the date is within +/- 50 years
of the current epoch year, then the resulting epoch seconds will fit inside a
int32_t
integer. Helper methods are available on various classes to avoid
manual conversion between these 2 epochs: forUnixSeconds64()
and
toUnixSeconds64()
.
Starting with v2, the AceTime epoch is an adjustable parameter which is no
longer hard coded to 2000-01-01 (v1 default) or 2050-01-01 (v2 default). There
are a number of static functions on the Epoch
class that support this feature:
namespace ace_time {
class Epoch {
public:
// Get the current epoch year.
static int16_t currentEpochYear();
// Set the current epoch year.
static int16_t currentEpochYear(int16_t epochYear);
// The number of days from the converter epoch (2000-01-01T00:00:00) to
// the current epoch ({yyyy}-01-01T00:00:00).
static int32_t daysToCurrentEpochFromConverterEpoch();
// The number of days from the Unix epoch (1970-01-01T00:00:00)
// to the current epoch ({yyyy}-01-01T00:00:00).
static int32_t daysToCurrentEpochFromUnixEpoch();
// The number of seconds from the Unix epoch (1970-01-01T00:00:00)
// to the current epoch ({yyyy}-01-01T00:00:00).
static int64_t secondsToCurrentEpochFromUnixEpoch64();
// Return the lower limit year which generates valid epoch seconds for the
// current epoch.
static int16_t epochValidYearLower();
// Return the upper limit year which generates valid epoch seconds for the
// current epoch.
static int16_t epochValidYearUpper();
};
}
Normally, the current epoch year is expected to be unchanged using the default
2050, or changed just once at the initialization phase of the application.
However in rare situations, it may be necessary for the client app to call
Epoch::currentEpochYear()
during its runtime. When this occurs, it is
important to invalidate the zone processor cache, as explained in Zone
Processor Cache Invalidation.
The LocalDate
and LocalTime
represent date and time components, without
reference to a particular time zone. They are not expected to be commonly used
by the end-users, but they are available if needed. The significant parts of the
class definitions are:
namespace ace_time {
class LocalTime {
public:
static const acetime_t kInvalidSeconds = INT32_MIN;
static LocalTime forComponents(uint8_t hour, uint8_t minute,
uint8_t second);
static LocalTime forSeconds(acetime_t seconds);
bool isError() const;
uint8_t hour() const;
void hour(uint8_t hour);
uint8_t minute() const;
void minute(uint8_t month);
uint8_t second() const;
void second(uint8_t second);
acetime_t toSeconds() const;
int8_t compareTo(const LocalTime& that) const;
void printTo(Print& printer) const;
...
};
class LocalDate {
public:
static const int16_t kInvalidYear = INT16_MIN;
static const int16_t kMinYear = 0;
static const int16_t kMaxYear = 10000;
static const int32_t kInvalidEpochDays = INT32_MIN;
static const int32_t kInvalidEpochSeconds = INT32_MIN;
static const int64_t kInvalidUnixSeconds64 = INT64_MIN;
static const int32_t kMinEpochSeconds = INT32_MIN + 1;
static const int32_t kMaxEpochSeconds = INT32_MAX;
static const uint8_t kMonday = 1;
static const uint8_t kTuesday = 2;
static const uint8_t kWednesday = 3;
static const uint8_t kThursday = 4;
static const uint8_t kFriday = 5;
static const uint8_t kSaturday = 6;
static const uint8_t kSunday = 7;
static LocalDate forComponents(int16_t year, uint8_t month, uint8_t day);
static LocalDate forEpochDays(int32_t epochDays);
static LocalDate forEpochSeconds(acetime_t epochSeconds);
static LocalDate forUnixDays(int32_t unixDays);
static LocalDate forUnixSeconds64(int64_t unixSeconds);
int16_t year() const;
void year(int16_t year);
uint8_t month() const;
void month(uint8_t month);
uint8_t day() const;
void day(uint8_t day);
uint8_t dayOfWeek() const;
bool isError() const;
int32_t toEpochDays() const {
acetime_t toEpochSeconds() const {
int32_t toUnixDays() const {
int64_t toUnixSeconds64() const {
int8_t compareTo(const LocalDate& that) const {
void printTo(Print& printer) const;
...
};
}
You can use them like this:
#include <AceTime.h>
using namespace ace_time;
...
// LocalDate that represents 2019-05-20
auto localDate = LocalDate::forComponents(2019, 5, 20);
// LocalTime that represents 13:00:00
auto localTime = LocalTime::forComponents(13, 0, 0);
You can ask the LocalDate
to determine its day of the week, which returns
an integer where 1=Monday
and 7=Sunday
per
ISO 8601:
uint8_t dayOfWeek = localDate.dayOfWeek();
To convert the dayOfweek()
numerical code to a human-readable string for
debugging or display, we can use the DateStrings
class:
namespace ace_time {
class DateStrings {
public:
static const uint8_t kBufferSize = 10;
static const uint8_t kShortNameLength = 3;
const char* monthLongString(uint8_t month);
const char* monthShortString(uint8_t month);
const char* dayOfWeekLongString(uint8_t dayOfWeek);
const char* dayOfWeekShortString(uint8_t dayOfWeek);
};
}
The DateStrings
object uses an internal buffer to hold the generated
human-readable strings. That makes this class stateful, which means that we need
to handle its lifecycle carefully. The recommended usage of this object is to
create an instance the stack, call one of the dayOfWeek*String()
or
month*String()
methods, copy the resulting string somewhere else (e.g. print
it to Serial), then allow the DateStrings
object to go out of scope and
reclaimed from the stack. The class is not meant to be created and persisted for
a long period of time, unless you are sure that nothing else will reuse the
internal buffer between calls.
#include <AceTime.h>
using namespace ace_time;
...
auto localDate = LocalDate::forComponents(2019, 5, 20);
uint8_t dayOfWeek = localDate.dayOfWeek();
Serial.println(DateStrings().dayOfWeekLongString(dayOfWeek));
Serial.println(DateStrings().dayOfWeekShortString(dayOfWeek));
The dayOfWeekShortString()
method returns the
first 3 characters of the week day (i.e. "Mon", "Tue", "Wed", "Thu",
"Fri", "Sat", "Sun").
Similarly the LocalDate::month()
method returns an integer code where
1=January
and 12=December
. This integer code can be translated into English
strings using DateStrings().monthLongString()
:
uint8_t month = localDate.month();
Serial.println(DateStrings().monthLongString(month));
Serial.println(DateStrings().monthShortString(month));
The monthShortString()
method returns the first 3 characters of the month
(i.e. "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
"Nov", "Dec").
Caveat: The DateStrings
class supports only the English language. If you
need to convert to another language, you need to write the conversion class
yourself, possibly by copying the implementation details of the DateStrings
class.
A LocalDateTime
object holds both the date and time components
(year, month, day, hour, minute, second). Internally, it is implemented as a
combination of LocalDate
and LocalTime
and supports essentially all
operations on those classes. It does not support the notion of timezone.
namespace ace_time {
class LocalDateTime {
public:
static LocalDateTime forComponents(int16_t year, uint8_t month,
uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
static LocalDateTime forEpochSeconds(acetime_t epochSeconds);
static LocalDateTime forUnixSeconds64(int64_t unixSeconds);
static LocalDateTime forDateString(const char* dateString);
bool isError() const;
int16_t year() const; // 1 - 9999
void year(int16_t year);
uint8_t month() const; // 1 - 12
void month(uint8_t month);
uint8_t day() const; // 1 - 31
void day(uint8_t day);
uint8_t hour() const; // 0 - 23
void hour(uint8_t hour);
uint8_t minute() const; // 0 - 59
void minute(uint8_t minute);
uint8_t second() const; // 0 - 59 (no leap seconds)
void second(uint8_t second);
uint8_t dayOfWeek() const; // 1=Monday, 7=Sunday
const LocalDate& localDate() const;
const LocalTime& localTime() const;
int32_t toEpochDays() const;
acetime_t toEpochSeconds() const;
int32_t toUnixDays() const;
int64_t toUnixSeconds64() const;
int8_t compareTo(const LocalDateTime& that) const;
void printTo(Print& printer) const;
...
};
}
Here is a sample code that extracts the number of seconds since AceTime Epoch
(2050-01-01T00:00:00 UTC) using the toEpochSeconds()
method:
// 2018-08-30T06:45:01-08:00
auto localDateTime = LocalDateTime::forComponents(2018, 8, 30, 6, 45, 1);
acetime_t epoch_seconds = localDateTime.toEpochSeconds();
We can go the other way and create a LocalDateTime
from the Epoch Seconds:
auto localDateTime = LocalDateTime::forEpochSeconds(1514764800L);
localDateTime.printTo(Serial); // prints "2018-01-01T00:00:00"
Both printTo()
and forDateString()
are expected to be used only for
debugging. The printTo()
prints a human-readable representation of the date in
ISO 8601 format (yyyy-mm-ddThh:mm:ss)
to the given Print
object. The most common Print
object is the Serial
object which prints on the serial port. The forDateString()
parses the
ISO 8601 formatted string and returns the LocalDateTime
object.
The TimePeriod
class can be used to represents a difference between two
XxxDateTime
objects, if the difference is not too large. Internally, it is
implemented as 3 unsigned uint8_t
integers representing the hour, minute and
second components. There is a 4th signed int8_t
integer that holds the sign
(-1 or +1) of the time period. The largest (or smallest) time period that can be
represented by this class is +/- 255h59m59s, corresponding to +/- 921599
seconds.
namespace ace_time {
class TimePeriod {
public:
static const int32_t kInvalidPeriodSeconds = INT32_MIN;
static const int32_t kMaxPeriodSeconds = 921599;
static TimePeriod forError(int8_t sign = 0);
explicit TimePeriod(uint8_t hour, uint8_t minute, uint8_t second,
int8_t sign = 1);
explicit TimePeriod(int32_t seconds = 0);
uint8_t hour() const;
void hour(uint8_t hour);
uint8_t minute() const;
void minute(uint8_t minute);
uint8_t second() const;
void second(uint8_t second);
int8_t sign() const;
void sign(int8_t sign);
int32_t toSeconds() const;
bool isError() const;
int8_t compareTo(const TimePeriod& that) const;
void printTo(Print& printer) const;
};
}
This class was created to show the difference between 2 dates in a
human-readable format, broken down by hours, minutes and seconds. For example,
we can print out a countdown to a target LocalDateTime
from the current
LocalDateTime
like this:
LocalDateTime current = ...;
LocalDateTime target = ...;
acetime_t diffSeconds = target.toEpochSeconds() - current.toEpochSeconds();
TimePeriod timePeriod(diffSeconds);
timePeriod.printTo(Serial)
The largest absolutely value of diffSeconds
supported by this class is
TimePeriod::kMaxPeriodSeconds
which is 921599, which corresponds to
255h59m59s
. Calling the TimePeriod(int32_t)
constructor outside of the +/- 921599
range will return an object whose isError()
returns true
.
You can check the TimePeriod::sign()
method to determine which one of the 3
cases apply. The printTo()
method prints the following:
- generic error:
sign() == 0
,printTo()
prints<Error>
- overflow:
sign() == 1
,printTo()
prints<+Inf>
- underflow:
sign() == -1
,printTo()
prints<-Inf>
It is sometimes useful to directly create a TimePeriod
object that represents
an error condition. The TimePeriod::forError(int8_t sign = 0)
factory method
uses the sign
parameter to distinguish 3 different error types described
above. By default with no arguments, it create a generic error object with sign == 0
.
Calling TimePeriod::toSeconds()
on an error object returns
TimePeriod::kInvalidPeriodSeconds
regardless of the sign
value. However,
you can call the TimePeriod::sign()
method to distinguish among the 3
different error conditions.
A TimeOffset
class represents an amount of time shift from a reference point.
This is usually used to represent a timezone's standard UTC offset or its DST
offset in the summer. The time resolution of this class changed from 15 minutes
(using a single byte int8_t
implementation prior to v0.7) to 1 minute (using a
2-byte int16_t
implementation since v0.7). The range of an int16_t
is
[-32768, +32767], but -32768 is used to indicate an error condition, so the
actual range is [-32767, +32767] minutes. In practice, the range of values
actually used is probably within [-48, +48] hours, or [-2880, +2800] minutes
namespace ace_time {
class TimeOffset {
public:
static TimeOffset forHours(int8_t hours);
static TimeOffset forMinutes(int16_t minutes);
static TimeOffset forHourMinute(int8_t hour, int8_t minute);
int16_t toMinutes() const;
int32_t toSeconds() const;
void toHourMinute(int8_t& hour, int8_t& minute) const;
bool isZero() const;
bool isError() const;
void printTo(Print& printer) const;
};
}
A TimeOffset
can be created using the factory methods:
auto offset = TimeOffset::forHours(-8); // -08:00
auto offset = TimeOffset::forMinutes(135); // +02:15
auto offset = TimeOffset::forHourMinute(-2, -30); // -02:30
If the time offset is negative, then both the hour and minute components of
forHourMinute()
must be negative. (The duplication of the negative sign allows
the creation of UTC-00:15, UTC-00:30 and UTC-00:45.)
A TimeOffset
instance can be converted into different formats:
int32_t seconds = offset.toSeconds();
int16_t minutes = offset.toMinutes();
int8_t hour;
int8_t minute;
offset.toHourMinute(&hour, &minute);
When a method in some class (e.g. OffsetDateTime
or ZonedDateTime
below)
returns a TimeOffset
, it is useful to indicate an error condition by returning
the special value created by the factory method TimeOffset::forError()
. This
special error marker has the property that TimeOffset::isError()
returns
true
. Internally, this is an instance whose internal integer is -32768.
The convenience method TimeOffset::isZero()
returns true
if the offset has a
zero offset. This is often used to determine if a timezone is currently
observing Daylight Saving Time (DST).
An OffsetDateTime
is an object that can represent a LocalDateTime
which is
offset from the UTC time zone by a fixed amount. Internally the OffsetDateTime
is an aggregation of LocalDateTime
and TimeOffset
. Use this class for
creating and writing timestamps for events which are destined for logging for
example. This class does not know about Daylight Saving Time transitions.
namespace ace_time {
class OffsetDateTime {
public:
static OffsetDateTime forComponents(int16_t year, uint8_t month,
uint8_t day, uint8_t hour, uint8_t minute, uint8_t second,
TimeOffset timeOffset);
static OffsetDateTime forEpochSeconds(acetime_t epochSeconds,
TimeOffset timeOffset);
static OffsetDateTime forUnixSeconds64(int64_t unixSeconds,
TimeOffset timeOffset);
static OffsetDateTime forDateString(const char* dateString);
bool isError() const;
int16_t year() const;
void year(int16_t year);
uint8_t month() const;
void month(uint8_t month);
uint8_t day() const;
void day(uint8_t day);
uint8_t hour() const;
void hour(uint8_t hour);
uint8_t minute() const;
void minute(uint8_t minute);
uint8_t second() const;
void second(uint8_t second);
uint8_t dayOfWeek() const;
const LocalDate& localDate() const;
const LocalTime& localTime() const;
TimeOffset timeOffset() const;
void timeOffset(TimeOffset timeOffset);
OffsetDateTime convertToTimeOffset(TimeOffset timeOffset) const;
int32_t toEpochDays() const;
acetime_t toEpochSeconds() const;
int32_t toUnixDays() const;
int64_t toUnixSeconds64() const;
int8_t compareTo(const OffsetDateTime& that) const;
void printTo(Print& printer) const;
};
}
We can create the object using the forComponents()
method:
// 2018-01-01 00:00:00+00:15
auto offsetDateTime = OffsetDateTime::forComponents(
2018, 1, 1, 0, 0, 0, TimeOffset::forHourMinute(0, 15));
int32_t epochDays = offsetDateTime.toEpochDays();
acetime_t epochSeconds = offsetDateTime.toEpochSeconds();
offsetDateTime.printTo(Serial); // prints "2018-01-01 00:00:00+00:15"
Serial.println(epochDays); // prints 6574
Serial.println(epochSeconds); // prints 568079100
We can create an OffsetDateTime
object from the seconds from Epoch using
the forEpochSeconds()
method:
auto offsetDateTime = OffsetDateTime::forEpochSeconds(
568079100, TimeOffset::forHourMinute(0, 15));
Both printTo()
and forDateString()
are expected to be used only for
debugging. The printTo()
prints a human-readable representation of the date in
ISO 8601 format
(yyyy-mm-ddThh:mm:ss+/-hh:mm) to the given Print
object. The most common
Print
object is the Serial
object which prints on the serial port. The
forDateString()
parses the ISO 8601 formatted string and returns the
OffsetDateTime
object.
These classes build upon the simpler classes described above to provide functionality related to time zones. These classes bridge the gap between the information encoded in the ZoneInfo Database for the various time zones and the expected date and time fields appropriate for those time zones.
A "time zone" is often used colloquially to mean 2 different things:
- An offset from the UTC time by a fixed amount, or
- A geographical or political region whose local time is offset from the UTC time using various transition rules.
Both meanings of "time zone" are supported by the TimeZone
class using
3 different types as defined by the value of getType()
:
TimeZone::kTypeManual
(1): a fixed base offset and optional DST offset from UTCBasicZoneProcessor::kTypeBasic
(3): utilizes aBasicZoneProcessor
which can be encoded with (relatively) simple rules from the ZoneInfo DatabaseExtendedZoneProcessor::kTypeExtended
(4): utilizes aExtendedZoneProcessor
which can handle all zones and links in the ZoneInfo Database
The class hierarchy of TimeZone
is shown below, where the arrow means
"is-subclass-of" and the diamond-line means "is-aggregation-of". This is an
internal implementation detail of the TimeZone
class that the application
developer will not normally need to be aware of all the time, but maybe this
helps make better sense of the usage of the TimeZone
class. A TimeZone
can
hold a reference to:
- nothing (
kTypeManual
), - one
BasicZoneProcessor
object, (kTypeBasic
), or - one
ExtendedZoneProcessor
object (kTypeExtended
)
0..1
TimeZone <>-------- ZoneProcessor
^
|
.-----+-----.
| |
BasicZoneProcessor ExtendedZoneProcessor
Here is the class declaration of TimeZone
:
namespace ace_time {
class TimeZone {
public:
static const uint8_t kTypeError = 0;
static const uint8_t kTypeManual = 1;
static const uint8_t kTypeReserved = 2;
static TimeZone forTimeOffset(
TimeOffset stdOffset,
TimeOffset dstOffset = TimeOffset());
static TimeZone forHours(int8_t stdHours, int8_t dstHours = 0);
static TimeZone forMinutes(int16_t stdMinutes, int16_t dstMinutes = 0);
static TimeZone forHourMinute(
int8_t stdHour,
int8_t stdMinute,
int8_t dstHour = 0,
int8_t dstMinute = 0);
static TimeZone forZoneInfo(
const basic::ZoneInfo* zoneInfo,
BasicZoneProcessor* zoneProcessor);
static TimeZone forZoneInfo(
const extended::ZoneInfo* zoneInfo,
ExtendedZoneProcessor* zoneProcessor);
static TimeZone forUtc();
TimeZone(); // same as forUtc()
bool isError() const;
uint8_t getType() const;
uint32_t getZoneId() const;
OffsetDateTime getOffsetDateTime(const LocalDateTime& ldt) const;
OffsetDateTime getOffsetDateTime(acetime_t epochSeconds) const;
ZonedExtra getZonedExtra(const LocalDateTime& ldt) const;
ZonedExtra getZonedExtra(acetime_t epochSeconds) const;
// for kTypeManual only
TimeOffset getStdOffset() const;
TimeOffset getDstOffset() const;
bool isUtc() const;
bool isDst() const;
TimeZoneData toTimeZoneData() const;
void printTo(Print& printer) const;
void printShortTo(Print& printer) const;
};
}
The TimeZone class is an immutable value type. It can be passed around by value, but since it is between 5 bytes (8-bit processors) and 12 bytes (32-bit processors) big, it may be slightly more efficient to pass by const reference, then save locally by-value when needed. The ZonedDateTime holds the TimeZone object by-value.
The following methods apply only to instances of the type kTypeManual
:
forUtc()
- create a
TimeZone
instance for UTC+00:00
- create a
forTimeOffset(stdOffset, dstOffset)
- create a
TimeZone
instance usingTimeOffset
- create a
forHours(stdHours, dstHours)
- create a
TimeZone
instance using hours offset
- create a
forMinutes(stdMinutes, dstMinutes)
- create a
TimeZone
instance using minutes offset
- create a
isUtc()
:- returns true if the instance is a UTC time zone instance
- returns false if not
kTypeManual
isDst()
:- returns true if the dstOffset is not zero
- returns false if not
kTypeManual
The following methods apply to a kTypeBasic
or kTypeExtended
:
forZoneInfo(zoneInfo, zoneProcessor)
- Create an instance of from the given
ZoneInfo*
pointer (e.g.basic::kZoneAmerica_Los_Angeles
, orextended::kZoneAmerica_Los_Angeles
)
- Create an instance of from the given
getZoneId()
- Returns a
uint32_t
integer which is a unique and stable identifier for the IANA timezone. The zoneId identifier can be used to save and restore theTimeZone
. See the ZoneManager subsection below.
- Returns a
The following methods apply to any type of TimeZone
:
getOffsetDateTime(localDateTime)
- Returns the best guess of the
OffsetDateTime
at the given local date time. This method is used byZonedDateTime::forComponents()
and is exposed mostly for debugging. - The
fold
parameter of thelocalDateTime
will be used by theExtendedZoneProcessor
to disambiguate date-time in the gap or overlap selecting the first (0) or second (1) transition line. - The
BasicZoneProcessor
does not support thefold
parameter so will ignore it.
- Returns the best guess of the
getOffsetDateTime(epochSeconds)
- Returns the
OffsetDateTime
that matches the givenepochSeconds
. - The
OffsetDateTime::fold
parameter indicates whether the date-time occurred the first time (0), or the second time (1)
- Returns the
getZonedExtra(localDateTime)
- Returns the
ZonedExtra
instance at the givenlocalDateTime
. ZonedExtra
contains additional information about the timezone, such as theZonedExtra::stdOffset()
,ZonedExtra::dstOffset()
, and theZonedExtra::abbrev()
- It may be more convenient to use the
ZonedExtra::forLocalDateTime()
factory method instead. See ZonedExtra section below.
- Returns the
getZonedExtra(epochSeconds)
- Returns the
ZonedExtra
instance at the givenepochSeconds
. - It may be more convenient to use the
ZonedExtra::forEpochSeconds()
factory method instead. See ZonedExtra section below.
- Returns the
printTo()
- Prints the fully-qualified unique name for the time zone. For example,
"UTC"
,"-08:00"
,"-08:00(DST)"
,"America/Los_Angeles"
.
- Prints the fully-qualified unique name for the time zone. For example,
printShortTo()
- Similar to
printTo()
except that it prints the last component of the IANA TZ Database zone names. - In other words,
"America/Los_Angeles"
is printed as"Los_Angeles"
. This is helpful for printing on small width OLED displays.
- Similar to
The default constructor creates a TimeZone
in UTC time zone with no
offset. This is also identical to the forUtc()
method:
TimeZone tz; // UTC+00:00
auto tz = TimeZone::forUtc(); // identical to above
To create TimeZone
instances with other offsets, use the forTimeOffset()
factory method, or starting with v1.4, use the forHours()
, forMinutes()
and
forHourMinute()
convenience methods:
// UTC-08:00, no DST shift
auto tz = TimeZone::forTimeOffset(TimeOffset::forHours(-8));
auto tz = TimeZone::forHours(-8); // identical to above
// UTC-04:30, no DST shift
auto tz = TimeZone::forTimeOffset(TimeOffset::forHourMinute(-4, -30));
auto tz = TimeZone::forHourMinute(-4, -30); // identical to above
// UTC-03:30 with a 01:00 DST shift, so effectively UTC-02:30
auto tz = TimeZone::forTimeOffset(
TimeOffset::forHourMinute(-3, -30),
TimeOffset::forHourMinute(1, 0));
auto tz = TimeZone::forHourMinute(-3, -30, 1, 0); // identical to above
The TimeZone::isUtc()
, TimeZone::isDst()
and TimeZone::setDst(bool)
methods work only if the TimeZone
is a kTypeManual
.
This TimeZone is created using two objects:
- the
basic::ZoneInfo
data objects contained in zonedb/zone_infos.h - an external instance of
BasicZoneProcessor
needed for calculating zone transitions
BasicZoneProcessor zoneProcessor;
void someFunction() {
auto tz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles,
&zoneProcessor);
...
}
The ZoneInfo entries were generated by a script using the IANA TZ Database. This
header file is already included in <AceTime.h>
so you don't have to explicitly
include it. As of version 2021a of the database, it contains 266 Zone and 183
Link entries and whose time change rules are simple enough to be supported by
BasicZoneProcessor
. The bottom of the zone_infos.h
header file lists 117
zones whose zone rules are too complicated for BasicZoneProcessor
.
The zone names are normalized so that the ZoneInfo variable is a valid C++ name:
- a
+
(plus) character in the zone name is replaced with_PLUS_
to avoid conflict with a-
(minus) character (e.g.GMT+0
becomesGMT_PLUS_0
) - any remaining non-alphanumeric character (
0-9a-zA-Z_
) are replaced with an underscore (_
) (e.g.GMT-0
becomesGMT_0
)
Some examples of ZoneInfo
entries supported by zonedb
are:
zonedb::kZoneAmerica_Los_Angeles
(America/Los_Angeles
)zonedb::kZoneAmerica_New_York
(America/New_York
)zonedb::kZoneAustralia_Darwin
(Australia/Darwin
)zonedb::kZoneEurope_London
(Europe/London
)zonedb::kZoneGMT_PLUS_10
(GMT+10
)zonedb::kZoneGMT_10
(GMT-10
)
The following example creates a TimeZone
which describes
America/Los_Angeles
. A TimeZone
instance is normally expected to be just
passed into a ZonedDateTime
object through a factory method, but there are
a few ways that a TimeZone
object can be used directly. See the
ZonedExtra section below for more information:
#include <AceTime.h>
using namespace ace_time;
...
BasicZoneProcessor zoneProcessor;
void someFunction() {
...
auto tz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles,
&zoneProcessor);
// 2018-03-11T01:59:59-08:00 was still in STD time
{
auto dt = OffsetDateTime::forComponents(2018, 3, 11, 1, 59, 59,
TimeOffset::forHours(-8));
acetime_t epochSeconds = dt.toEpochSeconds();
ZonedExtra ze = tz.getZonedExtra(epochSeconds);
TimeOffset offset = ze.timeOffset(); // returns -08:00
}
// one second later, 2018-03-11T02:00:00-08:00 was in DST time
{
auto dt = OffsetDateTime::forComponents(2018, 3, 11, 2, 0, 0,
TimeOffset::forHours(-8));
acetime_t epochSeconds = dt.toEpochSeconds();
ZonedExtra ze = tz.getZonedExtra(epochSeconds);
TimeOffset offset = ze.timeOffset(); // returns -07:00
}
...
}
This TimeZone is created using two objects:
- the
extended::ZoneInfo
data objects contained in zonedbx/zone_infos.h - an external instance of
ExtendedZoneProcessor
needed for calculating zone transitions
ExtendedZoneProcessor zoneProcessor;
void someFunction() {
auto tz = TimeZone::forZoneInfo(&zonedbx::kZoneAmerica_Los_Angeles,
&zoneProcessor);
...
}
(Notice that we use the zonedbx::
namespace instead of the zonedb::
namespace. Although the data structures in the 2 namespaces are identical
currently (v1.2) but the values inside the data structure fields are not
the same, and they are interpreted differently.)
As of version 2021a of the IANA TZ Database, all 386 Zone and 207 Link entries
from the following TZ files are supported: africa
, antarctica
, asia
,
australasia
, backward
, etcetera
, europe
, northamerica
, southamerica
.
There are 3 files which are not processed (backzone
, systemv
, factory
)
because they don't seem to contain anything useful.
The zone infos which can be used by ExtendedZoneProcessor
are in the
zonedbx::
namespace instead of the zonedb::
namespace. Some examples of the
zone infos which exists in zonedbx::
but not in zonedb::
are:
zonedbx::kZoneAfrica_Casablanca
zonedbx::kZoneAmerica_Argentina_San_Luis
zonedbx::kZoneAmerica_Indiana_Petersburg
zonedbx::kZoneAsia_Hebron
zonedbx::kZoneEurope_Moscow
The following example creates a TimeZone
which describes
America/Los_Angeles
. A TimeZone
instance is normally expected to be just
passed into a ZonedDateTime
object through a factory method, but there are
a few ways that a TimeZone
object can be used directly. See the
ZonedExtra section below for more information:
ExtendedZoneProcessor zoneProcessor;
void someFunction() {
...
TimeZone tz = TimeZone::forZoneInfo(&zonedbx::kZoneAmerica_Los_Angeles,
&zoneProcessor);
// 2018-03-11T01:59:59-08:00 was still in STD time
{
auto dt = OffsetDateTime::forComponents(2018, 3, 11, 1, 59, 59,
TimeOffset::forHours(-8));
acetime_t epochSeconds = dt.toEpochSeconds();
ZonedExtra ze = tz.getZonedExtra(epochSeconds);
TimeOffset offset = ze.timeOffset(); // returns -08:00
}
// one second later, 2018-03-11T02:00:00-08:00 was in DST time
{
auto dt = OffsetDateTime::forComponents(2018, 3, 11, 2, 0, 0,
TimeOffset::forHours(-8));
acetime_t epochSeconds = dt.toEpochSeconds();
ZonedExtra ze = tz.getZonedExtra(epochSeconds);
TimeOffset offset = ze.timeOffset(); // returns -07:00
}
...
}
There are 3 major types of TimeZone
objects:
kTypeManual
: STD and DST offsets are fixedkTypeBasic
: usesBasicZoneProcessor
kTypeExtended
: usesExtendedZoneProcessor
The kTypeManual
was added mostly for completeness and for testing purposes. I
do not expect most applications to use the kTypeManual
, because the primary
purpose of the AceTime library to provide access to the timezones defined by the
IANA TZ database, and the kTypeManual
timezone does not provide that
functionality.
The advantage of ExtendedZoneProcessor
over BasicZoneProcessor
is that
ExtendedZoneProcessor
will always support all time zones and links in the IANA
TZ Database. The BasicZoneProcessor
supports only a limited subset of zones
which have certain simplifying properties. It is not uncommon for zones that
used to be supported by BasicZoneProcessor
to change its DST rules in a
subsequent TZ DB release and becomes incompatible with the BasicZoneProcessor
.
The ExtendedZoneProcessor
does not have this problem.
The ExtendedZoneProcessor
is also more accurate than BasicZoneProcessor
during DST gaps when using the forComponents()
factory methods, because the
zonedbx::
entries contain transition information which are missing in the
zonedb::
entries due to space constraints. The ExtendedZoneProcessor
provides complete control which LocalDateTime
is selected during a gap or
overlap using the fold
parameter. The BasicZoneProcessor
ignores the fold
parameter and makes educated guesses when the LocalDateTime
falls in a gap or
an overlap.
The biggest difference between BasicZoneProcessor
and ExtendedZoneProcessor
is the amount of flash and static memory consumed. The ExtendedZoneProcessor
consumes 4 times more static memory than
BasicZoneProcessor
and is a bit slower. For most 32-bit processors, this will
not be an issue, so the ExtendedZoneProcessor
is recommended. For 8-bit
processors, the BasicZoneProcessor
consumes a lot less resources, so if your
timezone is supported, then it may be the appropriate choice. In most cases, I
think the ExtendedZoneProcessor
should be preferred unless memory resources
are so constrained that BasicZoneProcessor
must be used.
Instead of managing the BasicZoneProcessor
or ExtendedZoneProcessor
manually, you can use the ZoneManager
to manage a database of ZoneInfo
entries, and a cache of multiple ZoneProcessor
s, and bind the TimeZone
to
its ZoneInfo
and its ZoneProcessor
more dynamically through the
ZoneManager
. See the section ZoneManager below for more
information.
A ZonedDateTime
is an OffsetDateTime
associated with a given TimeZone
.
All internal types of TimeZone
are supported, the ZonedDateTime
class itself
does not care which one is used. You should use the ZonedDateTime
when
interacting with human beings, who are aware of timezones and DST transitions.
It can also be used to convert time from one timezone to anther timezone.
namespace ace_time {
class ZonedDateTime {
public:
static const acetime_t kInvalidEpochSeconds = LocalTime::kInvalidSeconds;
static ZonedDateTime forComponents(int16_t year, uint8_t month, uint8_t day,
uint8_t hour, uint8_t minute, uint8_t second, const TimeZone& timeZone);
static ZonedDateTime forEpochSeconds(acetime_t epochSeconds,
const TimeZone& timeZone);
static ZonedDateTime forUnixSeconds64(int64_t unixSeconds,
const TimeZone& timeZone);
static ZonedDateTime forDateString(const char* dateString);
explicit ZonedDateTime();
bool isError() const;
int16_t year() const;
void year(int16_t year);
uint8_t month() const;
void month(uint8_t month);
uint8_t day() const;
void day(uint8_t day);
uint8_t hour() const;
void hour(uint8_t hour);
uint8_t minute() const;
void minute(uint8_t minute);
uint8_t second() const;
void second(uint8_t second);
uint8_t dayOfWeek() const;
TimeOffset timeOffset() const;
const TimeZone& timeZone() const;
ZonedDateTime convertToTimeZone(const TimeZone& timeZone) const;
int32_t toEpochDays() const;
acetime_t toEpochSeconds() const;
int32_t toUnixDays() const;
int64_t toUnixSeconds64() const;
int8_t compareTo(const ZonedDateTime& that) const;
void printTo(Print& printer) const;
...
};
}
There are 2 main factory methods for constructing this object:
ZonedDateTime::forComponents()
ZonedDateTime::forEpochSeconds()
Here is an example of how these can be used:
ExtendedZoneProcessor zoneProcessor;
void someFunction() {
...
auto tz = TimeZone::forZoneInfo(
&zonedbx::kZoneAmerica_Los_Angeles,
&zoneProcessor);
// Create instance for 2018-01-01T00:00:00-08:00[America/Los_Angeles]
// using forComponents().
auto zdt = ZonedDateTime::forComponents(2018, 1, 1, 0, 0, 0, tz);
acetime_t epochSeconds = zdt.toEpochSeconds();
Serial.println(epochSeconds); // prints 568108800
// Create an instance one day later using forEpochSeconds()
acetime_t oneDayAfterSeconds = epochSeconds + 86400;
auto zdtPlus1d = ZonedDateTime::forEpochSeconds(oneDayAfterSeconds, tz);
// Should print "2018-01-01T00:00:00-08:00[America/Los_Angeles]"
zdt.printTo(Serial);
Serial.println();
// Should print "2018-01-02T00:00:00-08:00[America/Los_Angeles]"
zdtPlus1d.printTo(Serial);
Serial.println();
...
}
The printTo()
prints a human-readable representation of the date in
ISO 8601 format
(yyyy-mm-ddThh:mm:ss+/-hh:mm) to the given Print
object. The most common
Print
object is the Serial
object which prints on the serial port. The
forDateString()
parses the ISO 8601 formatted string and returns the
ZonedDateTime
object.
The third factory method, ZonedDateTime::forDateString()
, converts a
human-readable ISO 8601 date format into an instance of a ZonedDateTime
.
However, this is intended to be used only for debugging so its functionality is
limited as follows.
Caveat: The parser for forDateString()
looks only at the UTC offset. It
does not recognize the IANA TZ Database identifier (e.g.
[America/Los_Angeles]
). To handle the time zone identifier correctly, the
library needs to load the entire TZ Database into memory and use the
ZoneManager
to manage the BasicZoneProcessor
or ExtendedZoneProcessor
objects dynamically. But the dataset is too large to fit on most AVR
microcontrollers with only 32kB of flash memory, so we currently do not support
this dynamic lookup. The ZonedDateTime::timeZone()
will return Manual
TimeZone
whose TimeZone::getType()
returns TimeZone::kTypeManual
.
You can convert a given ZonedDateTime
object into a representation in a
different time zone using the DateTime::convertToTimeZone()
method:
static BasicZoneProcessor processorLosAngeles;
static BasicZoneProcessor processorZurich;
void someFunction() {
...
auto tzLosAngeles = TimeZone::forZoneInfo(
&zonedb::kZoneAmerica_Los_Angeles, &processorLosAngeles);
auto tzZurich = TimeZone::forZoneInfo(
&zonedb::kZoneEurope_Zurich, &processorZurich);
// Europe/Zurich, 2018-01-01T09:20:00+01:00
auto zurichTime = ZonedDateTime::forComponents(
2018, 1, 1, 9, 20, 0, tzZurich);
// Convert to America/Los_Angeles, 2018-01-01T01:20:00-08:00
auto losAngelesTime = zurichTime.convertToTimeZone(tzLosAngeles);
...
}
The two ZonedDateTime
objects will return the same value for epochSeconds()
because that is not affected by the time zone. However, the various date time
components (year, month, day, hour, minute, seconds) will be different.
The conversion from an epochSeconds to date-time components using
ZonedDateTime::forEpochSeconds()
is an expensive operation
that requires the computation of the relevant DST transitions for the given
epochSeconds or date-time components. To improve performance, the
BasicZoneProcessor
and ExtendedZoneProcessor
implement internal transition
caching based on the year
component. This optimizes the most commonly expected
use case where the epochSeconds is incremented by a clock (e.g. SystemClock
)
every second, and is converted to human-readable date-time components once a
second. According to AutoBenchmark, the cache
improves performance by a factor of 2-3X (8-bit AVR) to 10-20X (32-bit
processors) on consecutive calls to forEpochSeconds()
with the same year
.
The most important feature of the AceTime library is the conversion from
epochSeconds
to ZonedDateTime
and vise versa. The ZonedDateTime
object
contains the most common parameters that is expected to be needed by the user,
the Gregorian date components (provided by LocalDateTime
) and the total UTC
offset at the specific instance (provided by ZonedDateTime::timeOffset()
).
To keep memory size of the ZonedDateTime
class reasonable, it does not contain
some of the less common information that the end-user may wish to know. The
extra time zone parameters are contained in the ZonedExtra
class, which look
like this:
namespace ace_time {
class ZonedExtra {
public:
static const uint8_t kTypeNotFound = 0;
static const uint8_t kTypeExact = 1;
static const uint8_t kTypeGap = 2;
static const uint8_t kTypeOverlap = 3;
static ZonedExtra forError();
static ZonedExtra forComponents(
int16_t year, uint8_t month, uint8_t day,
uint8_t hour, uint8_t minute, uint8_t second,
const TimeZone& tz, uint8_t fold = 0);
static ZonedExtra forEpochSeconds(
acetime_t epochSeconds, const TimeZone& tz);
static ZonedExtra forLocalDateTime(
const LocalDateTime& ldt, const TimeZone& tz);
explicit ZonedExtra() {}
explicit ZonedExtra(
uint8_t type,
int16_t stdOffsetMinutes,
int16_t dstOffsetMinutes,
int16_t reqStdOffsetMinutes,
int16_t reqDstOffsetMinutes,
const char* abbrev);
bool isError() const;
uint8_t type() const;
TimeOffset timeOffset() const; // stdOffset + dstOffset
TimeOffset stdOffset() const;
TimeOffset dstOffset() const;
TimeOffset reqTimeOffset() const; // reqStdOffst + reqDstOffset
TimeOffset reqStdOffset() const;
TimeOffset reqDstOffset() const;
const char* abbrev() const;
};
}
The ZonedExtra
instance is usually created through the 2 static factory
methods on the ZonedExtra
class:
ZonedExtra::forEpochSeconds(epochSeconds, tz)
ZonedExtra::forComponents(int16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, const TimeZone& tz, uint8_t fold = 0)
Often the ZonedDateTime
will be created first from the epochSeconds, then the
ZonedExtra
will be created to access additional information about the time zone at that particular epochSeconds (e.g. abbreviation):
ExtendedZoneProcessor zoneProcessor;
TimeZone tz = TimeZone::forZoneInfo(
&zonedbx::kZoneAmerica_Los_Angeles,
&zoneProcessor);
acetime_t epochSeconds = ...;
ZonedDateTime zdt = ZonedDateTime::forEpochSeconds(epochSeconds, tz);
ZonedExtra ze = ZonedExtra::forEpochSeconds(epochSeconds, tz);
The ZonedExtra::type()
parameter identifies whether the given time instant
corresponded to a DST gap, or a DST overlap, or an exact match with no
duplicates.
The ZonedExtra::stdOffset()
is the standard offset of the timezone at the
given time instant. For example, for America/Los_Angeles
this will return
-08:00
(unless the standard offset is changed through something like a
"permanent DST").
The ZonedExtra::dstOffset()
is the DST offset that pertains to the given
time instant. For example, for America/Los_Angeles
this will return 01:00
during summer DST, and 00:00
during normal times.
The ZonedExtra::timeOffset()
is a convenience method that returns the sum of
stdOffset() + dstOffset()
. This value is identical to the total UTC offset
returned by ZonedDateTime::timeOffset()
.
The ZonedExtra::abbrev()
is the short abbreviation that is in effect at the
given time instant. For example, for America/Los_Angeles
, this returns "PST"
or "PDT'. The abbreviation is copied into a small char
buffer inside the
ZonedExtra
object, so the pointer returned by abbrev()
is safe to use during
the life of the ZonedExtra
object.
The ZonedExtra::reqStdOffset()
and ZonedExtra::reqDstOffset()
are relevant
and different from the corresponding stdOffset()
and dstOffset()
only if the
type()
is kTypeGap
. This occurs only if the ZonedExtra::forComponents()
factory method is used. Following the algorithm described in Python PEP
495, the provided localDateTime is
imaginary during a gap so must be mapped to a real local time using the
LocalDateTime::fold
parameter. When fold=0
, the transition line before the
gap is extended forward until it hits the given LocalDateTime
. When fold=1
,
the transition line after the gap is extended backwards until it hits the given
LocalDateTime
. The reqStdOffset()
and reqDstOffset()
are then derived from
the transition line that is used to convert the provided LocalDateTime
instance to epochSeconds
. The epochSeconds
is then normalized by converting
it back to LocalDateTime
using the stdOffset()
and dstOffset()
which
matches the epochSeconds
.
The isError()
method returns true if the given LocalDateTime
or
epochSeconds
represents an error condition.
The TimeZone::forZoneInfo()
methods are simple to use but have the
disadvantage that the BasicZoneProcessor
or ExtendedZoneProcessor
needs to
be created manually for each TimeZone
instance. This works well for a single
time zone, but if you have an application that needs 3 or more time zones, this
can become cumbersome. Also, it is difficult to reconstruct a TimeZone
dynamically, say, from its fully qualified name (e.g. "America/Los_Angeles"
).
The ZoneManager
solves these problems by implementing 2 features:
- It supports a registry of
ZoneInfo
objects, so that aTimeZone
can be created using itszoneName
string,zoneInfo
pointer, orzoneId
integer. - It supports the use of cache of
ZoneProcessors
that can be mapped to a particular zone as needed.
Three implementations of the ZoneManager
are provided. Prior to v1.9, they
were organized into a hierarchy with a top-level ZoneManager
interface with
pure virtual functions. However, in v1.9, the top-level ZoneManager
interface
was removed and all functions became nonvirtual. This saves about 1100-1200
bytes of flash on AVR processors.
namespace ace_time{
class BasicZoneManager {
public:
BasicZoneManager(
uint16_t registrySize,
const basic::ZoneInfo* const* zoneRegistry,
BasicZoneProcessorCacheBase& zoneProcessorCache
);
uint16_t zoneRegistrySize() const;
TimeZone createForZoneName(const char* name);
TimeZone createForZoneId(uint32_t id);
TimeZone createForZoneIndex(uint16_t index);
TimeZone createForTimeZoneData(const TimeZoneData& d);
TimeZone createForZoneInfo(const basic::ZoneInfo* zoneInfo);
uint16_t indexForZoneName(const char* name) const;
uint16_t indexForZoneId(uint32_t id) const;
BasicZoneProcessor* getZoneProcessor(const char* name);
BasicZone getZoneForIndex(uint16_t index) const;
};
class ExtendedZoneManager {
public:
ExtendedZoneManager(
uint16_t registrySize,
const extended::ZoneInfo* const* zoneRegistry,
BasicZoneProcessorCacheBase& zoneProcessorCache
);
uint16_t zoneRegistrySize() const;
TimeZone createForZoneInfo(const extended::ZoneInfo* zoneInfo);
TimeZone createForZoneId(uint32_t id);
TimeZone createForZoneIndex(uint16_t index);
TimeZone createForTimeZoneData(const TimeZoneData& d);
TimeZone createForZoneInfo(const extended::ZoneInfo* zoneInfo);
uint16_t indexForZoneName(const char* name) const;
uint16_t indexForZoneId(uint32_t id) const;
ExtendedZoneProcessor* getZoneProcessor(const char* name);
ExtendedZone getZoneForIndex(uint16_t index) const;
};
class ManualZoneManager {
public:
uint16_t zoneRegistrySize() const;
TimeZone createForTimeZoneData(const TimeZoneData& d);
};
}
The constructors for BasicZoneManager
and ExtendedZoneManager
take a
zoneRegistry
and its zoneRegistrySize
, and optionally the linkRegistry
and
linkRegistrySize
parameters. The AceTime library comes with a set of
pre-defined default Zone and Link registries which are defined by the following
header files. These header files are automatically included in the <AceTime.h>
header:
- zonedb/zone_registry.h
- Zones and Links supported by
BasicZoneManager
ace_time::zonedb
namespace
- Zones and Links supported by
- zonedbx/zone_registry.h
- Zones and Links supported by
ExtendedZoneManager
ace_time::zonedbx
namespace
- Zones and Links supported by
The BasicZoneManager
and the ExtendedZoneManager
classes need to be given an
instance of a BasicZoneProcessorCache<CACHE_SIZE>
or
ExtendedZoneProcessorCache<CACHE_SIZE>
object.
BasicZoneProcessorCache<CACHE_SIZE> basicZoneProcessorCache;
ExtendedZoneProcessorCache<CACHE_SIZE> extendedZoneProcessorCache;
These used to be defined internally inside the BasicZoneManager
and
ExtendedZoneManager
classes. But when they were refactored to be
non-polymorphic to save flash memory, it was easier to extract the
ZoneProcessorCache objects into separate classes to be passed into the
ZoneManager classes.
The CACHE_SIZE
template parameter is an integer that specifies the size of the
internal cache. This should be set to the number of time zones that your
application is expected to use at the same time. If your app never changes its
time zone after initialization, then this can be <1>
(although in this case,
you may not even want to use the ZoneManager
). If your app allows the user to
dynamically change the time zone (e.g. from a menu of time zones), then this
should be at least <2>
(to allow the system to compare the old time zone to
the new time zone selected by the user). In general, the CACHE_SIZE
should be
set to the number of timezones displayed to the user concurrently, plus an
additional 1 if the user is able to change the timezone dynamically.
If you decide to use the default registries, there are 4 possible configurations of the ZoneManager constructors as shown below. The following also shows the number of zones and links supported by each configuration, as well as the flash memory consumption of each configuration, as determined by MemoryBenchmark. These numbers are correct as of v1.6 with TZDB version 2021a:
static const uint8_t CACHE_SIZE = 2; // tuned for application
BasicZoneProcessorCache<CACHE_SIZE> basicZoneProcessorCache;
ExtendedZoneProcessorCache<CACHE_SIZE> extendedZoneProcessorCache;
// BasicZoneManager, Zones and Links
// 266 zones, 183 links
// 25.7 kB (8-bits)
// 33.2 kB (32-bits)
BasicZoneManager zoneManager(
zonedb::kZoneAndLinkRegistrySize,
zonedb::kZoneAndLinkRegistry,
basicZoneProcessorCache);
// ExtendedZoneManager, Zones and Fat Links
// 386 Zones, 207 fat Links
// 38.2 kB (8-bits)
// 48.7 kB (32-bits)
ExtendedZoneManager zoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
extendedZoneProcessorCache);
Once the ZoneManager
is configured with the appropriate registries, you can
use one of the createForXxx()
methods to create a TimeZone
as shown in the
subsections below.
It is possible to create your own custom Zone and Link registries. See the Custom Zone Registry subsection below.
The ZoneManager
allows creation of a TimeZone
using the fully qualified
zone name:
ExtendedZoneProcessorCache<CACHE_SIZE> zoneProcessorCache;
ExtendedZoneManager zoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache);
void someFunction() {
TimeZone tz = zoneManager.createForZoneName("America/Los_Angeles");
if (tz.isError()) {
// handle error
}
...
}
Of course, you probably wouldn't actually do this because the same functionality
could be done more efficiently using the createForZoneInfo()
like this:
ExtendedZoneProcessorCache<CACHE_SIZE> zoneProcessorCache;
ExtendedZoneManager zoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache);
void someFunction() {
TimeZone tz = zoneManager.createForZoneInfo(zonedb::kZoneAmerica_Los_Angeles);
...
}
I think the only time the createForZoneName()
might be useful is if
the user was allowed to type in the zone name, and you wanted to create a
TimeZone
from the string typed in by the user.
Each zone in the zonedb::
and zonedbx::
database is given a unique
and stable zoneId. There are at least 3 ways to extract this zoneId:
- the
kZoneId{zone name}
constants insrc/zonedb/zone_infos.h
andsrc/zonedbx/zone_infos.h
:const uint32_t kZoneIdAmerica_New_York = 0x1e2a7654; // America/New_York
const uint32_t kZoneIdAmerica_Los_Angeles = 0xb7f7e8f2; // America/Los_Angeles
- ...
- the
TimeZone::getZoneId()
method:uint32_t zoneId = tz.getZoneId();
- the
ZoneInfo
pointer using theBasicZone()
helper object:uint32_t zoneId = BasicZone(&zonedb::kZoneAmerica_Los_Angeles).zoneId();
uint32_t zoneId = ExtendedZone(&zonedbx::kZoneAmerica_Los_Angeles).zoneId();
The ZoneManager::createForZoneId()
method returns the TimeZone
object
corresponding to the given zoneId
:
ExtendedZoneProcessorCache<CACHE_SIZE> zoneProcessorCache;
ExtendedZoneManager zoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache);
void someFunction() {
TimeZone tz = zoneManager.createForZoneId(kZoneIdAmerica_New_York);
if (tz.isError() {
// handle error
}
...
}
If the ZoneManager
cannot find the zoneId
in its internal zone registry,
then the TimeZone::forError()
is returned. The application developer should
check for this, and substitute a reasonable default TimeZone when this happens.
This situation is not unique to the zoneId. The same problem would occur if the
fully qualified zone name was used.
The ZoneId is created using a hash of the fully qualified zone name. It is
guaranteed to be unique and stable by the tzcompiler.py
tool that generated
the zonedb::
and zonedbx::
data sets. By "unique", I mean that
no 2 time zones will have the same zoneId. By "stable", it means that once
a zoneId has been assigned to a fully qualified zone name, it will remain
unchanged forever in the database.
The zoneId
has an obvious advantage over the fully qualified zoneName
for
storage purposes. It is far easier to save a 4-byte zoneId (e.g. 0xb7f7e8f2
)
rather than a variable length string (e.g. "America/Los_Angeles"
).
Since the zoneId
is derived from just the zoneName, a TimeZone
created by
the BasicZoneManager
has the same zoneId as one created using the
ExtendedZoneManager
if it has the same name. This means that a TimeZone can be
saved using a BasicZoneManager
but recreated using an ExtendedZoneManager
. I
am not able to see how this could be an issue, but let me know if you find this
to be a problem.
Another useful feature of ZoneManager::createForZoneId()
over
createForZoneInfo()
is that createForZoneId()
lives at the root
ZoneManager
interface. In contrast, there are 2 different versions of
createForZoneInfo()
which live in the corresponding implementation classes
(BasicZoneManager
and ExtendedZoneManager
) because each version needs a
different ZoneInfo
type (basic::ZoneInfo
and extended::ZoneInfo
). If your
code has a reference or pointer to the top-level ZoneManager
interface, then
it will be far easier to create a TimeZone
using createForZoneId()
. You do
pay a penalty in efficiency because createForZoneId()
must scan the database,
where as createForZoneInfo()
does not perform a search since it has direct
access to the ZoneInfo
data structure.
The ZoneManager::createForZoneIndex()
creates a TimeZone
from its integer
index into the Zone registry, from 0 to registrySize - 1
. This is useful when
you want to show the user with a menu of zones from the ZoneManager
and allow
the user to select one of the options.
The ZoneManager::indexForZoneName()
and ZoneManager::indexForZoneId()
are
two useful methods to convert an arbitrary time zone reference (either
by zoneName or zoneId) into an index into the registry.
The ZoneManager::createForTimeZoneDAta()
creates a TimeZone
from an instance
of TimeZoneData
. The TimeZoneData
can be retrieved from
TimeZone::toTimeZoneData()
method. It contains the minimum set of identifiers
of a TimeZone
object in a format that can be serialized easily, for example,
to EEPROM.
The ManualZoneManager
is a type of ZoneManager
that implements only the
createForTimeZoneData()
method, and handles only TimeZoneData::kTypeManual
.
In other words, it can only create TimeZone
objects with fixed standard and
DST offsets.
This class reduces the amount of conditional code (using #if
statements)
needed in applications which are normally targeted to use BasicZoneManager
and
ExtendedZoneManager
, but are sometimes targeted to small-memory
microcontrollers (typically AVR chips), for testing purposes for example. This
class allows many of the function and constructor signatures to remain the same,
reducing the amount of conditional code.
If an application is specifically targeted to a low-memory chip, and it is known
at compile-time that only TimeZone::kTypeManual
are supported, then you should
not need to use the ManualZoneManager
. You can use TimeZone::forTimeOffset()
factory method directory.
(Added in v1.10)
Better control over DST gaps and overlaps was added in v1.10 using the techniques described by the PEP 495 document in Python 3.6.
- An additional parameter called
fold
was added to theLocalTime
,LocalDateTime
,OffsetDateTime
, andZonedDateTime
classes. - Support for the
fold
parameter was added toExtendedZoneProcessor
. TheBasicZoneProcessor
does not support thefold
parameter and will ignore it.
As a quick background, when a timezone changes its DST offset in the spring or fall, it creates either a gap (the UTC offset increases by one hour), or an overlap (the UTC offset decreases by one hour) in the local representation of the time. For example, in the "America/Los_Angeles" timezone (aka "US/Pacific"), the wall clock goes from 2am to 3am in the spring (spring forward) and goes back from 2am to 1am in the fall (fall back). In the spring, there are local time instances which are illegal because they never existed, and in the fall, there are local time instances which occur twice.
Different date-time libraries in different languages handle these situations slightly differently. For example,
- Java 11 java.time package
- returns the
ZonedDateTime
shifted forward by one hour during a gap - returns the earlier
ZonedDateTime
during an overlap - choices offered with additional methods:
ZonedDateTime.withEarlierOffsetAtOverlap()
ZonedDateTime.withLaterOffsetAtOverlap()
- returns the
- C++ Hinnant date library
- throws an exception in a gap or overlap if a specifier
choose::earliest
orchoose::latest
is not specified
- throws an exception in a gap or overlap if a specifier
- Noda Time
- throws
AmbiguousTimeException
orSkippedTimeException
- can specify an
Offset
toZonedDateTime
class to resolve ambiguity
- throws
- Python datetime
- uses a
fold
parameter to resolve ambiguous or non-existent time - see PEP 495
- uses a
The AceTime library cannot use exceptions because the Arduino C++ environment does not support exceptions. I chose to follow the techniques described by Python PEP 495 because it is well-documented, easy to understand, and relatively simple to implement.
An optional fold
parameter was added to various constructors and factory
methods. The default is fold=0
if not specified. The fold()
accessor and
mutator methods were added to the various classes as well.
namespace ace_time {
class LocalTime {
public:
static LocalTime forComponents(uint8_t hour, uint8_t minute,
uint8_t second, uint8_t fold = 0);
uint8_t fold() const;
void fold(uint8_t fold);
[...]
};
class LocalDateTime {
public:
static LocalDateTime forComponents(int16_t year, uint8_t month,
uint8_t day, uint8_t hour, uint8_t minute, uint8_t second,
uint8_t fold = 0);
uint8_t fold() const;
void fold(uint8_t fold);
[...]
};
class OffsetDateTime {
public:
static OffsetDateTime forComponents(int16_t year, uint8_t month,
uint8_t day, uint8_t hour, uint8_t minute, uint8_t second,
TimeOffset timeOffset, uint8_t fold = 0) {
uint8_t fold() const;
void fold(uint8_t fold);
[...]
};
class ZonedDateTime {
public:
static ZonedDateTime forComponents(int16_t year, uint8_t month, uint8_t day,
uint8_t hour, uint8_t minute, uint8_t second,
const TimeZone& timeZone, uint8_t fold = 0) {
uint8_t fold() const;
void fold(uint8_t fold);
[...]
};
}
There are 2 main factory methods on ZonedDateTime
: forEpochSeconds()
and
forComponents()
. The fold
parameter is an output parameter for
forEpochSeconds()
, and an input parameter for forComponents()
. The
mapping functionality of these methods are described in detail in the PEP 495
document, but here is an ASCII diagram for reference:
^
LocalDateTime |
| (overlap) /
2am | /| /
| / | /
1am | / |/
| /
| /
3am | |
| (gap)|
2am | |
| /
| /
| /
+----------------------------------------->
spring-forward fall-backward
UTC/epochSeconds
The forEpochSeconds()
takes the UTC/epochSeconds value and maps it to the
LocalDateTime axis. It is a single-valued function which is defined for all
values of epochSeconds, even with a DST shift forward or backward. The fold
parameter is an output of the forEpochSeconds()
function. During an overlap,
a ZonedDataTime
can occur twice. The earlier occurrence is returned with
fold==0
, and the later occurrence is returned with fold==1
. For all other
cases where there is only a unique occurrence, the fold
parameter is set to 0.
The forComponents()
takes the LocalDateTime value and maps it to the
UTC/epochSeconds axis. During a gap, there are certain LocalDateTime
components which do not exist and are illegal. During an overlap, there are 2
epochSeconds which can correspond to the given LocalDateTime. The fold
parameter is an input parameter to the forComponents()
in both cases.
The impact of the fold
parameter is as follows:
Normal: Not a gap or overlap. The forComponents()
method ignores the
fold
parameter if there is no ambiguity about the local date-time components.
The returned ZonedDateTime
object will contain a fold()
value that preserves
the input fold
parameter.
Overlap: If ZonedDateTime::forComponents()
is called with during an
overlap of LocalDateTime
(e.g. 2:30am during a fall back from 2am to 3am), the
factory method uses the user-provided fold
parameter to select the following:
fold==0
- Selects the earlier Transition element and returns the earlier LocalDateTime.
- So 01:30 is interpreted as 01:30-07:00.
fold==1
- Selects the later Transition element and returns the later LocalDateTime.
- So 01:30 is interpreted as 01:30-08:00.
Gap: If ZonedDateTime::forComponents()
is called with a LocalDateTime
that does not exist (e.g. 2:30am during a spring forward night from 2am to 3am),
the factory method normalizes the resulting ZonedDateTime
object so that the
components of the object are legal. The algorithm for normalization depends on
the fold
parameter. The 2:30am
value becomes either 3:30am
or 1:30am
in
the following, and perhaps counter-intuitive, manner:
fold==0
- Selects the earlier Transition element, extended forward to apply to the given LocalDateTime,
- Which maps to the later UTC/epochSeconds,
- Which becomes normalized to the later ZonedDateTime which has the later UTC offset.
- So 02:30 is interpreted as 02:30-08:00, which is normalized to
03:30-07:00, and the
fold
after normalization is set to 1 to indicate that the later transition was selected.
fold==1
- Selects the later Transition element, extended backward to apply to the given LocalDateTime,
- Which maps to the earlier UTC/epochSeconds,
- Which becomes normalized to the earlier ZonedDateTime which has the earlier UTC offset.
- So 02:30 is interpreted as 02:30-07:00, which is normalized to
01:30-08:00, and the
fold
after normalization is set to 0 to indicate that the earlier transition was selected.
The time shift during a gap seems to be the opposite of the shift during an overlap, but internally this is self-consistent. Just as importantly, this follows the same logic as PEP 495.
Note that the fold
parameter flips its value (from 0 to 1, or vise versea) if
forComponents()
is called in the gap. Currently, this is the only publicly
exposed mechanism for detecting that a given date-time is in the gap.
The fold
parameter has no effect on most existing methods. It is ignored in
all comparison operators:
operator==()
,operator!=()
ignore thefold
operator<()
,operator>()
, etc. ignore thefold
compareTo()
ignores thefold
It impacts the behavior the factory methods of LocalTime
, LocalDateTime
,
OffsetDateTime
only trivially, causing the fold
value to be passed into the
internal holding variable:
LocalTime::forSeconds()
LocalTime::forComponents()
LocalDateTime::forEpochSeconds()
LocalDateTime::forComponents()
OffsetDateTime::forEpochSeconds()
OffsetDateTime::forComponents()
The fold
parameter has significant impact only on the ZonedDateTime
factory
methods, and only if the ExtendeZoneProcessor
is used:
ZonedDateTime::forEpochSeconds()
ZonedDateTime::forComponents()
The fold
parameter is not exposed through any of the existing printTo()
and
printShortTo()
methods. It can only be accessed and changes by the fold()
accessor and mutator methods.
A more subtle, but important semantic change, is that the fold
parameter
preserves information during gaps and overlaps. This means that we can do
round-trip conversions of ZonedDateTime
properly. We can start with
epochSeconds, convert to components, then back to epochSeconds, and get back the
same epochSeconds. Without the fold
parameter, this round-trip was not
guaranteed during an overlap.
According to MemoryBenchmark, adding support for
fold
increased flash usage of ExtendedZoneProcessor
by about 600 bytes on
AVR processors and 400-600 bytes on 32-bit processors. (The BasicZoneProcessor
which ignores the fold
parameter increased by ~150 bytes on AVR processors,
because of the overhead of copying the internal fold
parameter in various
objects.) The static memory footprint of various classes increased by one byte
on AVR processors, and 2-4 bytes on 32-bit processors due to 32-bit alignment.
According to AutoBenchmark, the performance of various
functions did not change at all, except for ZonedDataTime::forComponents()
,
which became 5X faster on AVR processors and 1.5-3X faster on 32-bit
processors. This is because the fold
parameter tells us exactly when the
internal normalization process is required, which allows us to skip the
normalization step in the common case outside the gap. Within the gap, the
forComponents()
method performs about the same as before.
Here are some examples taken from ZonedDateTimeExtendedTest:
ExtendedZoneProcessorCache<1> zoneProcessorCache;
ExtendedZoneManager extendedZoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache);
TimeZone tz = extendedZoneManager.createForZoneInfo(
&zonedbx::kZoneAmerica_Los_Angeles);
// During fall back. This is the second occurrence of this local time, so should
// print:
// 2022-11-06T01:29:00-08:00[America/Los_Angeles]
// fold=1
acetime_t epochSeconds = 721042140;
auto dt = ZonedDateTime::forEpochSeconds(epochSeconds, tz);
Serial.printTo(dt); Serial.println();
Serial.print("fold="); Serial.println(dt.fold());
// During spring forward. In the gap, fold=0 selects earlier transition,
// so selects -08:00 offset, which gets normalized to -07:00, so should print:
// 2022-03-13T03:29:00-07:00[America/Los_Angeles]
dt = ZonedDateTime::forComponents(2022, 3, 13, 2, 29, 0, tz, 0 /*fold*/);
Serial.printTo(dt); Serial.println();
// During spring forward. In the gap, fold=1 selects later transition,
// so selects -07:00 offset, which gets normalized to -08:00, so should print:
// 2022-03-13T01:29:00-08:00[America/Los_Angeles]
dt = ZonedDateTime::forComponents(2022, 3, 13, 2, 29, 0, tz, 1 /*fold*/);
Serial.printTo(dt); Serial.println();
Normally, the current epoch year will be configured using
Epoch::currentEpochYear(year)
only once during the lifetime of the
application. In rare situations, the application may choose to change the
current epoch year in the middle of its lifetime. When this happens, it is
important to invalidate the time zone transition cache maintained inside the
BasicZoneProcessor
or ExtendedZoneProcessor
classes.
If the application is using the TimeZone
class directly with an associated
BasicZoneProcessor
or ExtendedZoneProcessor
, then the following methods must
be called after calling Epoch::currentEpochYear(year)
:
BasicZoneProcessor processor(...);
processor.resetTransitionCache();
ExtendedZoneProcessor processor(...);
processor.resetTransitionCache();
If the application is using the BasicZoneManager
or ExtendedZoneManager
class to create the TimeZone
objects, then the
ZoneManager::resetZoneProcessors()
must be called after calling
Epoch::currentEpochYear(year)
:
BasicZoneManager manager(...);
manager.resetZoneProcessors();
ExtendedZoneManager manager(...);
manager.resetZoneProcessors();
The data structures that describe the zoneinfo database are in src/zoneinfo directory. The exact nature of how the zoneinfo files are stored and retrieved is an implementation detail that is subject to periodic improvements.
Starting with v0.4, zoneinfo entries are stored in in flash memory instead of static RAM using the PROGMEM keyword on microcontrollers which support this feature.
The following classes represent the various objects stored in PROGMEM
, and are
defined in the zoneinfo/ZoneInfo.h
and zoneinfo/ZonePolicy.h
files:
ZoneRule
ZonePolicy
: a collection ofZoneRule
ZoneEra
ZoneInfo
: a collection ofZoneEra
Information stored in PROGMEM
must be retrieved using special functions (e.g.
pgm_read_byte()
, pgm_read_word()
, etc). A thin layer of indirection is
provided to hide the implementation details of these access functions. The
abstraction layer is provided by zoneinfo/Brokers.h
:
ZoneRuleBroker
ZonePolicyBroker
ZoneEraBroker
ZoneInfoBroker
There are 2 sets of these broker classes, duplicated into 2 different C++ namespaces:
ace_time::basic::ZoneRuleBroker
- ...
ace_time::extended::ZoneRuleBroker
- ...
The separate namespaces allows compile-time verification that the correct
zonedb[x]
database is used with the correct BasicZoneProcessor
or
ExtendedZoneProcessor
.
There are 4 zonedb databases provided by default:
zonedb
: forBasicZoneProcessor
zonedbx
: forExtendedZoneProcessor
tzonedb
: for unit teststzonedbx
: for unit tests
The zonedb/
entries do not support all the timezones in the IANA TZ Database.
If a zone is excluded, the reason for the exclusion can be found at the
bottom of the zonedb/zone_infos.h file.
The criteria for selecting the Basic zonedb
entries are embedded
in the transformer.py
script and summarized in
BasicZoneProcessor.h:
- the DST offset is a multiple of 15-minutes (all current timezones satisfy this)
- the STDOFF offset is a multiple of 1-minute (all current timezones satisfy this)
- the AT or UNTIL fields must occur at one-year boundaries (this is the biggest filter)
- the LETTER field must contain only a single character
- the UNTIL time suffix can only be 'w' (not 's' or 'u')
- there can be only one DST transition in a single month
As of version v1.11.4 (with TZDB 2022b), this database contains 237 Zone entries and 215 Link entries, supported from the year 2000 to 2049 (inclusive).
The goal of the zonedbx/
entries is to support all zones listed in the TZ
Database. Currently, as of version 2021a of the IANA TZ Database, this goal is
met from the year 2000 to 2049 inclusive. Some restrictions of this database
are:
- the DST offset is a multiple of 15-minutes ranging from -1:00 to 2:45 (all timezones from about 1972 support this)
- the STDOFF offset is a multiple of 1-minute
- the AT and UNTIL fields are multiples of 1-minute
- the LETTER field can be arbitrary strings
As of version v1.11.4 (with TZDB 2022b), this database contains all 356 Zone entries and 239 Link entries, supported from the year 2000 to 2049 (inclusive).
The IANA TZ Database is updated continually. As of this writing, the latest
stable version is 2021a. When a new version of the database is released, I
regenerate the ZoneInfo entries under the src/zonedb/
and
src/zonedbx/
directories.
The current TZ Database version can be programmatically accessed using the
kTzDatabaseVersion
constant:
#include <AceTime.h>
using namespace ace_time;
void printVersionTzVersions() {
Serial.print("zonedb TZ version: ");
Serial.println(zonedb::kTzDatabaseVersion); // e.g. "2020d"
Serial.print("zonedbx TZ version: ");
Serial.println(zonedbx::kTzDatabaseVersion); // e.g. "2020d"
}
It is technically possible for the 2 versions to be different, but since they are generated by the same set of scripts, I expect they will always be the same.
As mentioned above, both the zonedb
and zonedbx
databases are generated with
a specific startYear
and untilYear
range. If you try to create a
ZonedDateTime
object outside of the year range, the constructed object will be
ZonedDateTime::forError()
whose isError()
method returns true
.
Applications can access the valid startYear
and untilYear
of the zonedb
or
zonedbx
databases through the kZoneContext
data structure:
#include <AceTime.h>
using namespace ace_time;
void printStartAndUntilYears() {
Serial.print("zonedb: startYear: ");
Serial.print(zonedb::kZoneContext.startYear); // e.g. 2000
Serial.print("; untilYear: ");
Serial.println(zonedb::kZoneContext.untilYear); // e.g. 2100
Serial.print("zonedbx: startYear: ");
Serial.print(zonedbx::kZoneContext.startYear); // e.g. 2000
Serial.print("; untilYear: ");
Serial.println(zonedbx::kZoneContext.untilYear); // e.g. 2100
}
I looked into supporting some sort of "graceful degradation" mode of the
ZonedDateTime
class, where creating instances before startYear
or after
untilYear
would actually succeed, even though those instances would have some
undefined errors due to incorrect or missing DST offsets. However, so much of
the code in BasicZoneProcessor
and ExtendedZonedProcessor
depend on the
intricate details of the ZoneInfo entries being in a valid state, I could not
guarantee that a catastrophic situation (e.g. infinite loop) could be avoided
outside of the safe zone. Therefore, attempting to create a ZonedTimeDate
object outside of the supported startYear
and untilYear
range will always
return an error object. Applications should either check the year range first
before creating a ZonedDateTime
object, or check the
ZonedDateTime::isError()
method after creation.
The basic::ZoneInfo
and extended::ZoneInfo
(and its related data structures)
objects are meant to be opaque and simply passed into the TimeZone
class (which in turn, passes the pointer into the BasicZoneProcessor
and
ExtendedZoneProcessor
objects.) The internal formats of the ZoneInfo
structures may change without warning, and users of this library should not
access its internal data members directly.
Two helper classes, BasicZone
and ExtendedZone
, provide stable access to
some of the internal fields:
namespace ace_time {
class BasicZone {
public:
BasicZone(const basic::ZoneInfo* zoneInfo);
bool isNull() const;
uint32_t zoneId() const;
int16_t stdOffsetMinutes() const;
ace_common::KString kname() const;
void printNameTo(Print& printer) const;
void printShortNameTo(Print& printer) const;
};
class ExtendedZone {
public:
ExtendedZone(const extended::ZoneInfo* zoneInfo);
bool isNull() const;
uint32_t zoneId() const;
int16_t stdOffsetMinutes() const;
ace_common::KString kname() const;
void printNameTo(Print& printer) const;
void printShortNameTo(Print& printer) const;
}
The isNull()
method returns true if the object is a wrapper around a
nullptr
. This is often used to indicate an error condition, or a "Not Found"
condition.
The stdOffsetMinutes()
method returns the standard (i.e. normal time, not DST
summer time) timezone offset of the zone for the last occurring ZoneEra
record
in the database. In almost all cases, this should correspond to the current
standard timezone, unless the timezone is scheduled to changes its standard
timezone offset in the near future. The value of this method is used by the
ZoneSorterByOffsetAndName
class when sorting timezones by its offset.
The kname()
method returns the KString
object representing the name of the
zone. The KString
object represents a string that has been compressed using a
fragment dictionary. This object knows how to decompress the encoded form. This
method is used by the ZoneSorterByName
and ZoneSorterByOffsetAndName
classes
when sorting timezones.
The printNameTo()
method prints the full zone name (e.g.
America/Los_Angeles
), and printShortNameTo()
prints only the last component
(e.g. Los_Angeles
).
The BasicZone
and ExtendedZone
objects are meant to be used transiently,
created on the stack then thrown away. For example:
const basic::ZoneInfo* zoneInfo = ...;
BasicZone(zoneInfo).printNameTo(Serial);
Serial.println();
Both BasicZone
and ExtendedZone
are light-weight wrapper objects around a
const ZoneInfo*
pointer. In fact, they are so light-weight that the C++
compiler should be able to optimize away both wrapper classes entirely, so that
they are equivalent to using the const ZoneInfo*
pointer directly.
If you need to copy the zone names into memory, use the PrintStr<N>
class from
the AceCommon library (https://github.com/bxparks/AceCommon) to print the
zone name into the memory buffer, then extract the string from the buffer:
#include <AceCommon.h>
using ace_common::PrintStr;
...
const basic::ZoneInfo* zoneInfo = ...;
PrintStr<32> printStr; // buffer of 32 bytes on the stack
BasicZone(zoneInfo).printNameTo(printStr);
const char* name = printStr.cstr();
// do stuff with 'name', but only while 'printStr' is alive
...
See also the Print To String section below.
The IANA TZ database contains 2 types of timezones:
- Zones, implemented by the
ZONE
keyword - Links, implemented by the
LINK
keyword
A Zone entry is the canonical name of a given time zone in the IANA database
(e.g. America/Los_Angeles
). A Link entry is an alias, an alternate name, for a
canonical entry (e.g. US/Pacific
which points to America/Los_Angeles
).
Prior to v2.1, AceTime treated Links slightly differently than Zones, providing
4 different implementations over several version. After v2.1, Links are
considered to be identical to Zones, because the TZDB considers both of them to
be first-class citizens. For most 32-bit processors with enough flash memory,
the kZoneAndLinkRegistry
should be used. The kZoneRegistry
containing
only the Zone entries is maintained mostly for backwards compatibility and
testing purposes.
Most methods on the TimeZone
class apply to both Zone and Link time zones.
There are 2 methods on the TimeZone
class which apply only to Links:
class TimeZone {
public:
...
bool isLink() const;
printTargetNameTo(Print& printer) const;
...
};
The `TimeZone::isLink()` method returns `true` if the current time zone is a
Link entry instead of a Zone entry. For example `US/Pacific` is a link to
`America/Los_Angeles`.
The `TimeZone::printTargetNameTo(Print&)` prints the name of the target zone if
the current time zone is a Link. Otherwise it prints nothing. For example, for
the time zone `US/Pacific` (which is a Link to `America/Los_Angeles`):
* `printTo(Print&)` prints "US/Pacific"
* `printTargetNameTo(Print&)` prints "America/Los_Angeles"
<a name="CustomZoneRegistry"></a>
#### Custom Zone Registry
On small microcontrollers, the default zone registries (`kZoneRegistry` and
`kZoneAndLinkRegistry`) may be too large. The `ZoneManager` can be configured
with a custom zone registry. It needs to be given an array of `ZoneInfo`
pointers when constructed. For example, here is a `BasicZoneManager` with only 4
zones from the `zonedb::` data set:
```C++
#include <AceTime.h>
using namespace ace_time;
...
static const basic::ZoneInfo* const kZoneRegistry[] ACE_TIME_PROGMEM = {
&zonedb::kZoneAmerica_Los_Angeles,
&zonedb::kZoneAmerica_Denver,
&zonedb::kZoneAmerica_Chicago,
&zonedb::kZoneAmerica_New_York,
};
static const uint16_t kZoneRegistrySize =
sizeof(kZoneRegistry) / sizeof(basic::ZoneInfo*);
static const uint16_t CACHE_SIZE = 2;
static BasicZoneProcessorCache<CACHE_SIZE> zoneProcessorCache;
static BasicZoneManager zoneManager(
kZoneRegistrySize, kZoneRegistry, zoneProcessorCache);
Here is the equivalent ExtendedZoneManager
with 4 zones from the zonedbx::
data set:
#include <AceTime.h>
using namespace ace_time;
...
static const extended::ZoneInfo* const kZoneRegistry[] ACE_TIME_PROGMEM = {
&zonedbx::kZoneAmerica_Los_Angeles,
&zonedbx::kZoneAmerica_Denver,
&zonedbx::kZoneAmerica_Chicago,
&zonedbx::kZoneAmerica_New_York,
};
static const uint16_t kZoneRegistrySize =
sizeof(kZoneRegistry) / sizeof(extended::ZoneInfo*);
static const uint16_t CACHE_SIZE = 2;
static ExtendedZoneProcessorCache<CACHE_SIZE> zoneProcessorCache;
static ExtendedZoneManager zoneManager(
kZoneRegistrySize, kZoneRegistry, zoneProcessorCache);
The ACE_TIME_PROGMEM
macro is defined in
compat.h and indicates whether the ZoneInfo
entries are stored in normal RAM or flash memory (i.e. PROGMEM
). It must
be used for custom zoneRegistries because the BasicZoneManager
and
ExtendedZoneManager
expect to find them in static RAM or flash memory
according to this macro.
See examples in various unit tests:
- tests/ZoneRegistrarTest
- tests/TimeZoneTest
- tests/ZonedDateTimeBasicTest
- tests/ZonedDateTimeExtendedTest
(TBD: I think it would be useful to create a script that can generate the C++ code representing these custom zone registries from a list of zones.)
(TBD: It might also be useful for app developers to create custom datasets
with different range of years. The tools are all here, but not explicitly
documented currently. Examples of how to this do exist inside the various
Makefile
files in the AceTimeValidation project.)
When a client application supports only a handful of zones in the ZoneManager
,
the order in which the zones are presented to the user may not be too important
since the user can scan the entire list quickly. But when the ZoneManager
contains the entire zoneInfo database (up to 594 zones and links), it becomes
useful to sort the zones in a predictable way before showing them to the user.
The ZoneSorterByName
and ZoneSorterByOffsetAndName
are 2 classes which can
sort the list of zones. They look like this:
namespace ace_time {
template <typename ZM>
class ZoneSorterByName {
public:
ZoneSorterByName(const ZM& zoneManager);
void fillIndexes(uint16_t indexes[], uint16_t size) const;
void sortIndexes(uint16_t indexes[], uint16_t size) const;
void sortIds(uint32_t ids[], uint16_t size) const;
void sortNames(const char* names[], uint16_t size) const;
};
template <typename ZM>
class ZoneSorterByOffsetAndName {
public:
ZoneSorterByOffsetAndName(const ZM& zoneManager);
void fillIndexes(uint16_t indexes[], uint16_t size) const;
void sortIndexes(uint16_t indexes[], uint16_t size) const;
void sortIds(uint32_t ids[], uint16_t size) const;
void sortNames(const char* names[], uint16_t size) const;
};
}
The ZoneSorterByName
class sorts the given zones in ascending order by the
zone's name. The ZoneSorterByOffsetAndName
class sorts the zones by its UTC
offset during standard time, then by the zone's name within the same UTC offset.
Both of these are templatized on the BasicZoneManager
or the
ExtendedZoneManager
classes because they require the methods implemented by
those classes. The ZoneSorter classes will not compile if the
ManualZoneManager
class is given because it does not make sense.
To use these classes, the calling client should follow these steps:
- Wrap an instance of a
ZoneSorterByName
class orZoneSorterByOffsetAndName
class around theZoneManager
class. - Create an array filled in the following manner:
- an
indexes[]
array filled with the index into thezoneRegistry
; or - an
ids[]
array filled with the 32-bit zone id; or - a
names[]
array filled with theconst char*
string of the zone name.
- an
- Call one of the
sortIndexes()
,sortIds()
, orsortNames()
methods of theZoneSorter
class to sort the array.
The code will look like this:
#include <AceTime.h>
using namespace ace_time;
using namespace ace_time::zonedbx;
ExtendedZoneProcessorCache<1> zoneProcessorCache;
ExtendedZoneManager zoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache
);
// Print each zone in the form of:
// "UTC-08:00 America/Los_Angeles"
// "UTC-07:00 America/Denver"
// [...]
void printZones(uint16_t indexes[], uint16_t size) {
for (uint16_t i = 0; i < size; i++) {
ExtendedZone zone = zoneManager.getZoneForIndex(indexes[i]);
TimeOffset stdOffset = TimeOffset::forMinutes(zone.stdOffsetMinutes());
// Print "UTC-08:00 America/Los_Angeles".
SERIAL_PORT_MONITOR.print(F("UTC"));
stdOffset.printTo(SERIAL_PORT_MONITOR);
SERIAL_PORT_MONITOR.print(' ');
zone.printNameTo(SERIAL_PORT_MONITOR);
SERIAL_PORT_MONITOR.println();
}
}
void sortAndPrintZones() {
// Create the indexes[kZoneAndLinkRegistrySize] on the stack. This has 594
// elements as of TZDB 2022a, so this requires a microcontroller which can
// support at least 1188 bytes on the stack.
uint16_t indexes[zonedbx::kZoneAndLinkRegistrySize];
// Create the sorter.
ZoneSorterByOffsetAndName<ExtendedZoneManager> zoneSorter(zoneManager);
// Fill the array with indexes from 0 to 593.
zoneSorter.fillIndexes(indexes, zonedbx::kZoneAndLinkRegistrySize);
// Sort the indexes.
zoneSorter.sortIndexes(indexes, zonedbx::kZoneAndLinkRegistrySize);
// Print in human readable form.
printZones(indexes, zonedbx::kZoneAndLinkRegistrySize);
}
The fillIndexes(uint16_t indexes[], uint16_t size)
method is a convenience
method that fills the given indexes[]
array from 0
to size-1
, so that it
can be sorted according to the specified sorting order. In other words, it is a
short hand for:
for (uint16_t i = 0; i < size; i++ ) {
indexes[i] = i;
}
See examples/ListZones for more examples.
The calling code can choose to sort only a subset of the zones registered into
the ZoneManager
. In the following example, 4 zone ids are placed into an array
of 4 slots, then sorted by offset and name:
#include <AceTime.h>
using namespace ace_time;
ExtendedZoneProcessorCache<1> zoneProcessorCache;
ExtendedZoneManager zoneManager(
zonedbx::kZoneAndLinkRegistrySize,
zonedbx::kZoneAndLinkRegistry,
zoneProcessorCache
);
uint32_t zoneIds[4] = {
zonedbx::kZoneIdAmerica_Los_Angeles,
zonedbx::kZoneIdAmerica_New_York,
zonedbx::kZoneIdAmerica_Denver,
zonedbx::kZoneIdAmerica_Chicago,
};
void sortIds() {
ZoneSorterByOffsetAndName<ExtendedZoneManager> zoneSorter(zoneManager);
zoneSorter.sortIds(zoneIds, 4);
...
}
Many classes provide a printTo(Print&)
method which prints a human-readable
string to the given Print
object. Any subclass of the Print
class can be
passed into these methods. The most familiar is the global the Serial
object
which prints to the serial port.
The AceCommon library (https://github.com:bxparks/AceCommon) provides a
subclass of Print
called PrintStr
which allows printing to an in-memory
buffer. The contents of the in-memory buffer can be retrieved as a normal
c-string using the PrintStr::cstr()
method.
Instances of the PrintStr
object is expected to be created on the stack. The
object will be destroyed automatically when the stack is unwound after returning
from the function where this is used. The size of the buffer on the stack is
provided as a compile-time constant. For example, PrintStr<32>
creates an
object with a 32-byte buffer on the stack.
An example usage looks like this:
#include <AceCommon.h>
#include <AceTime.h>
using ace_common::PrintStr;
using namespace ace_time;
...
{
TimeZone tz = TimeZone::forTimeOffset(TimeOffset::forHours(-8));
ZonedDateTime dt = ZonedDateTime::forComponents(
2018, 3, 11, 1, 59, 59, tz);
PrintStr<32> printStr; // 32-byte buffer
dt.printTo(printStr);
const char* cstr = printStr.cstr();
// do stuff with cstr...
printStr.flush(); // needed only if this will be used again
}
Mutating the date and time classes can be tricky. In fact, many other time libraries (such as Java 11 Time, Joda-Time, and Noda Time) avoid the problem altogether by making all objects immutable. In those libraries, mutations occur by creating a new copy of the target object with a new value for the mutated parameter. Making the objects immutable is definitely cleaner, but it causes the code size to increase significantly. For the case of the WorldClock program, the code size increased by 500-700 bytes, which I could not afford because the program takes up almost the entire flash memory of an Arduino Pro Micro with only 28672 bytes of flash memory.
Most date and time classes in the AceTime library are mutable. Except for
primitive mutations of setting specific fields (e.g.
ZonedDateTime::year(uint16_t)
), most higher-level mutation operations are not
implemented within the class itself to avoid bloating the class API surface. The
mutation functions live as functions in separate namespaces outside of the class
definitions:
time_period_mutation.h
time_offset_mutation.h
local_date_time_mutation.h
offset_date_time_mutation.h
zoned_date_time_mutation.h
Additional mutation operations can be written by the application developer and added into the same namespace, since C++ allows things to be added to a namespace multiple times.
Most of these mutation functions were created to solve a particular UI problem
in my various clock applications. In those clocks, the user is provided an OLED
display and 2 buttons. The user can change the time by long-pressing
the Select button. One of the components of the date or time will blink. The
user can press the other Change button to increment the component. Pressing the
Select button will move the blinking cursor to the next field. After all the
fields have been set, the user can long-press the Select button again to save
the new date and time into the SystemClock
.
The mutation functions directly manipulate the underlying date and time
components of ZonedDateTime
and other target classes. No validation rules are
applied. For example, the zoned_date_time_mutation::incrementDay()
method will
increment the ZonedDateTime::day()
field from Feb 29 to Feb 30, then to Feb
31, then wrap around to Feb 1. The object will become normalized when it is
converted into an Epoch seconds (using toEpochSeconds()
), then converted back
to a ZonedDateTime
object (using forEpochSeconds()
). By deferring this
normalization step until the user has finished setting all the clock fields, we
can reduce the size of the code in flash. (The limiting factor for many 8-bit
Arduino environments is the code size, not the CPU time.)
Mutating the ZonedDateTime
requires calling the ZonedDateTime::normalize()
method after making the changes. See the subsection on ZonedDateTime
Normalization below.
It is not clear that making the AceTime objects mutable was the best design decision. But it seems to produce far smaller code sizes (hundreds of bytes of flash memory saved for something like WorldClock), while providing the features that I need to implement the various Clock applications.
The TimeOffset
object can be mutated with:
namespace ace_time {
void setMinutes(int16_t minutes) {
namespace time_offset_mutation {
void increment15Minutes(TimeOffset& offset);
}
}
The LocalDate
object can be mutated with the following methods and functions:
namespace ace_time {
void LocalDate::year(int16_t year);
void LocalDate::month(uint8_t month);
void LocalDate::day(uint8_t month);
namespace local_date_mutation {
void incrementOneDay(LocalDate& ld);
void decrementOneDay(LocalDate& ld);
}
}
The OffsetDateTime
object can be mutated using the following methods and
functions:
namespace ace_time {
void OffsetDateTime::year(int16_t year);
void OffsetDateTime::month(uint8_t month);
void OffsetDateTime::day(uint8_t month);
void OffsetDateTime::hour(uint8_t hour);
void OffsetDateTime::minute(uint8_t minute);
void OffsetDateTime::second(uint8_t second);
void OffsetDateTime::timeZone(const TimeZone& timeZone);
namespace zoned_date_time_mutation {
void incrementYear(OffsetDateTime& dateTime);
void incrementMonth(OffsetDateTime& dateTime);
void incrementDay(OffsetDateTime& dateTime);
void incrementHour(OffsetDateTime& dateTime);
void incrementMinute(OffsetDateTime& dateTime);
}
}
The ZonedDateTime
object can be mutated using the following methods and
functions:
namespace ace_time {
void ZonedDateTime::year(int16_t year);
void ZonedDateTime::month(uint8_t month);
void ZonedDateTime::day(uint8_t month);
void ZonedDateTime::hour(uint8_t hour);
void ZonedDateTime::minute(uint8_t minute);
void ZonedDateTime::second(uint8_t second);
void ZonedDateTime::timeZone(const TimeZone& timeZone);
namespace zoned_date_time_mutation {
void incrementYear(ZonedDateTime& dateTime);
void incrementMonth(ZonedDateTime& dateTime);
void incrementDay(ZonedDateTime& dateTime);
void incrementHour(ZonedDateTime& dateTime);
void incrementMinute(ZonedDateTime& dateTime);
}
}
When the ZonedDateTime
object is mutated using the methods and functions
listed above, the client code must call ZonedDateTime::normalize()
before
calling a method that calculates derivative information, in particular, the
ZonedDateTime::toEpochSeconds()
method. Otherwise, the resulting epochSeconds
may be incorrect if the old ZonedDateTime
and the new ZonedDatetime
crosses
a DST boundary. Multiple mutations can be batched before calling
normalize()
.
For example:
TimeZone tz = ...;
ZonedDateTime zdt = ZonedDateTime::forComponent(2000, 1, 1, 0, 0, 0, tz);
zdt.year(2021);
zdt.month(4);
zdt.day(20);
zdt.normalize();
acetime_t newEpochSeconds = zdt.toEpochSeconds();
Adding this single call to normalize()
seems to increase flash consumption by
220 bytes on an 8-bit AVR processor. Unfortunately, it must be called to ensure
accuracy across DST boundaries.
The TimePeriod
can be mutated using the following methods:
namespace ace_time {
void TimePeriod::hour(uint8_t hour);
void TimePeriod::minute(uint8_t minute);
void TimePeriod::second(uint8_t second);
void TimePeriod::sign(int8_t sign);
namespace time_period_mutation {
void negate(TimePeriod& period);
void incrementHour(TimePeriod& period, uint8_t limit);
void incrementHour(TimePeriod& period);
void incrementMinute(TimePeriod& period);
}
}
Many features of the date and time classes have explicit or implicit range of validity in their inputs and outputs. The Arduino programming environment does not use C++ exceptions, so we handle invalid values by returning special version of various date/time objects to the caller.
Many methods return an return integer value. Error conditions are indicated by
special constants, many of whom are defined in the LocalDate
class:
int32_t LocalDate::kInvalidEpochDays
- Error value returned by
toEpochDays()
methods
- Error value returned by
int32_t LocalDate::kInvalidEpochSeconds
- Error value returned by
toEpochSeconds()
methods
- Error value returned by
int64_t LocalDate::kInvalidUnixSeconds64
- Error value returned by
toUnixSeconds64()
methods
- Error value returned by
Similarly, many factory methods accept an acetime_t
, int32_t
, or int64_t
arguments and return objects of various classes (e.g. LocalDateTime
,
OffsetDateTime
or ZonedDateTime
). When these methods are given the error
constants, they return an object whose isError()
method returns true
.
It is understandable that error checking is often neglected, since it adds to the maintenance burden. And sometimes, it is not always clear what should be done when an error occurs, especially in a microcontroller environment. However, I encourage the application to check for these errors conditions as much as practical, and try to degrade to some reasonable default behavior when an error is detected.
The isError()
method on these
classes will return true
upon a data range error:
bool LocalDate::isError() const;
bool LocalTime::isError() const;
bool LocalDateTime::isError() const;
bool OffsetDatetime::isError() const;
bool ZonedDateTime::isError() const;
bool TimeOffset::isError() const;
A well-crafted application should check for these error conditions before writing or displaying the objects to the user.
For example, the LocalDate
and LocalDateTime
classes support only 4-digit
year
component, from [1, 9999]
. The year 0 is used internally to indicate
-Infinity
and the year 10000
is used internally as +Infinity
.
The following are examples of invalid instances, where dt.isError()
will
return true:
auto dt = LocalDateTime::forComponents(-1, 1, 1, 0, 0, 0); // invalid year
auto dt = LocalDateTime::forComponents(2000, 0, 1, 0, 0, 0); // invalid month
auto dt = LocalDateTime::forComponents(2000, 1, 32, 0, 0, 0); // invalid day
auto dt = LocalDateTime::forComponents(2000, 1, 1, 24, 0, 0); // invalid hour
auto dt = LocalDateTime::forComponents(2000, 1, 1, 0, 61, 0); // invalid minute
auto dt = LocalDateTime::forComponents(2000, 1, 1, 0, 0, 61); // invalid second
Another example, the ZonedDateTime
class uses the generated ZoneInfo Database
in the zonedb::
and zonedbx::
namespaces. These data files are valid from
2000 until 10000. If you try to create a date outside of this range, an error
ZonedDateTime
object will returned. The following snippet will print "true":
BasicZoneProcessor zoneProcessor;
auto tz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles,
&zoneProcessor);
auto dt = ZonedDateTime::forComponents(1998, 3, 11, 1, 59, 59, tz);
Serial.println(dt.isError() ? "true" : "false");
- Leap seconds
- This library does not support leap seconds and will probably never do so.
- The library does not implement TAI (International Atomic Time).
- The
epochSeconds
is likeunixSeconds
in that it is unaware of leap seconds. When a leap seconds occurs, theepochSeconds
is held constant over 2 seconds, just likeunixSeconds
. - The
SystemClock
is unaware of leap seconds so it will continue to incrementepochSeconds
through the leap second. In other words, the SystemClock will be 1 second ahead of UTC after the leap second occurs.- If the referenceClock is the
NtpClock
, that clock happens to be leap second aware, and theepochSeconds
will bounce back one second upon the next synchronization, becoming synchronized to UTC. - If the referenceClock is the
DS3231Clock
, that clock is not leap second aware, so theepochSeconds
will continue to be ahead of UTC by one second even after synchronization.
- If the referenceClock is the
acetime_t
- AceTime uses an epoch of 2050-01-01T00:00:00 UTC by default. The epoch can
be changed using the
Epoch::currentEpochYear(year)
function. - The
acetime_t
type is a 32-bit signed integer whose smallest value is-2^31
and largest value is2^31-1
. However, the smallest value is used to indicate an internal "Error" condition, therefore the actual smallestacetime_t
is-2^31+1
. Therefore, the smallest and largest dates that can be represented byacetime_t
is theoreticall 1981-12-13T20:45:53 UTC to 2018-01-20T03:14:07 UTC (inclusive). - To be conversative, users of this library should limit the range of the
epoch seconds to +/- 50 years of the current epoch, in other words,
[2000,2100)
.
- AceTime uses an epoch of 2050-01-01T00:00:00 UTC by default. The epoch can
be changed using the
TimeOffset
- Implemented using
int16_t
in 1 minute increments.
- Implemented using
LocalDate
,LocalDateTime
- The
year
component is valid in the range of[1, 9999]
. - The
year = 0
is used internally to represent-Infinity
. This should not be visible to the end-user. - The
year = 10000
is used internally to represent+Infinity
. This should not be visible to the end-user. - The
year = INT32_MIN
is used to represent an error condition.
- The
forDateString()
- Various classes provide a
forDateString()
method to construct the object from a human-readable string. These methods are mostly meant to be used for debugging. The parsers are not robust and do not perform very much error checking, but they may be sufficient for your needs. ZonedDateTime::forDateString()
cannot support TZ Database zone identifiers (e.g. "America/Los_Angeles") because the AceTime library does not load the entire TZ Database due to memory constraints of most Arduino boards.
- Various classes provide a
TimeZone
- It might be possible to use both a basic
TimeZone
created using azonedb::ZoneInfo
entry, and an extendedTimeZone
using azonedbx::ZoneInfo
entry, together in a single program. However, this is not a configuration that is expected to be used often, so it has not been tested well, if at all. - One potential problem is that the equality of two
TimeZone
depends only on thezoneId
, so a BasicTimeZone
created with azonedb::kZoneAmerica_Los_Angeles
will be considered equal to an ExtendedTimeZone
created with azonedbx::kZoneAmerica_Los_Angeles
.
- It might be possible to use both a basic
ZonedDateTime::forComponents()
- The
ZonedDateTime::forComponents()
method takes the local wall time andTimeZone
instance as parameters which can be ambiguous or invalid for some values.- During the Standard time to DST transitions, a one-hour gap of illegal values may exist. For example, 2am (Standard) shifts to 3am (DST), therefore wall times between 02:00 and 03:00 (exclusive) are not valid.
- During DST to Standard time transitions, a one-hour interval occurs twice. For example, 2am (DST) shifts to 1am, so all times between 01:00 and 02:00 (exclusive) occurs twice in one day.
- The
ZonedDateTime::forCommponent()
methods makes an educated guess at what the user meant, but the algorithm may not be robust, is not tested as well as it could be, and the algorithm may change in the future. To keep the code size within reasonble limits of a small Arduino controller, the algorithm may be permanently sub-optimal.
- The
ZonedDateTime
Must Be Within +/- ~50 years of the AceTime Epoch- The internal time zone calculations use the same
int32_t
type as theacetime_t
epoch seconds. This has a range of about 136 years. - To be safe, the
ZoneDateTime
objects should be restricted to about +/- 50 years of the epoch defined byEpoch::currentEpochYear()
.
- The internal time zone calculations use the same
BasicZoneProcessor
- Supports 1-minute resolution for AT and UNTIL fields.
- Supports only a 15-minute resolution for the STDOFF and DST offset fields.
- Sufficient to support large number of timezones since the year 2000.
ExtendedZoneProcessor
- Has a 1-minute resolution for AT, UNTIL and STDOFF fields.
- Supports only a 15-minute resolution for DST field.
- All timezones after 1974 has DST offsets in 15-minutes increments.
zonedb/
andzonedbx/
ZoneInfo entries- These statically defined data structures are loaded into flash memory
using the
PROGMEM
keyword.- The vast majority of the data structure fields will stay in flash memory and not copied into RAM.
- The ZoneInfo entries have not been compressed using bit-fields.
- It may be possible to decrease the size of the full database using these compression techniques. However, compression will increase the size of the program file, so for applications that use only a small number of zones, it is not clear if the ZoneInfo entry compression will provide a reduction in the size of the overall program.
- The TZ database files
backzone
,systemv
andfactory
are not processed by thetzcompiler.py
tool.- They don't seem to contain anything worthwhile.
- TZ Database version 2019b contains the first use of the
{onDayOfWeek<=onDayOfMonth}
syntax that I have seen (specificallyRule Zion, FROM 2005, TO 2012, IN Apr, ON Fri<=1
).- The actual transition date can shift into the previous month (or to
the next month in the case of
>=
). However, shifting into the previous year or the next year is not supported. - The
tzcompiler.py
will exclude and flag the Rules which could potentially shift to a different year. As of version 2022f, no such Rule seems to exist.
- The actual transition date can shift into the previous month (or to
the next month in the case of
- These statically defined data structures are loaded into flash memory
using the
- Arduino Zero and SAMD21 Boards
- SAMD21 boards (which all identify themselves as
ARDUINO_SAMD_ZERO
) are no longer fully supported because:- I am no longer able to upload firmware to my SAMD21 boards
- The Arduino samd Core v1.8.10 migrated to the ArduinoCore-API. Unfortunately the ArduinoCore-API is not supported by AceTime.
- If you are using an original Arduino Zero and using the "Native USB Port",
you may encounter problems with nothing showing up on the Serial Monitor.
- The original Arduino Zero has 2 USB
ports. The Programming
port is connected to
Serial
object and the Native port is connected toSerialUSB
object. You can select either the "Arduino/Genuino Zero (Programming Port)" or the "Arduino/Genuino Zero (Native USB Port)" on the Board Manager selector in the Arduino IDEA. Unfortunately, if you select "(Native USB Port)", theSERIAL_MONITOR_PORT
macro should be defined to beSerialUSB
, but it continues to point toSerial
, which means that nothing will show up on the Serial Monitor. - You may be able to fix this by setting
ACE_TIME_CLOBBER_SERIAL_PORT_MONITOR
to1
insrc/ace_time/common/compat.h
. (I do not test this option often, so it may be broken.)
- The original Arduino Zero has 2 USB
ports. The Programming
port is connected to
- If you are using a SAMD21 development or breakout board, or one of the
many clones called something like "Ardunio SAMD21 M0 Mini" (this is what I
have), I have been unable to find a board configuration that is an exact
match. You have a few choices:
- If you are running the AceTime unit tests, you need to
have a working
SERIAL_PORT_MONITOR
, so the "Arduino MKR ZERO" board might work better, instead of the "Arduino Zero (Native USB Port)" board. - If you are running an app that requires proper pin configuration, it seems that the `Arduino MKR ZERO" configuration is not correct for this clone board. You need to go back to the "Arduino/Genuino Zero (Native USB Port)" board configuration.
- You may also try installing the SparkFun
Boards and select
the "SparkFun SAMD21 Mini Breakout" board. The advantage of using
this configuration is that the
SERIAL_PORT_MONITOR
is configured properly as well as the port pin numbers. However, I have found that the USB connection can be a bit flaky.
- If you are running the AceTime unit tests, you need to
have a working
- The
MKR Zero
board generates far faster code (30%?) than theSparkFun SAMD21 Mini Breakout
board. TheMKR Zero
could be using a different (more recent?) version of the GCC tool chain. I have not investigated this.
- SAMD21 boards (which all identify themselves as