diff --git a/.github/workflows/aunit_tests.yml b/.github/workflows/aunit_tests.yml index 7733a860f..bf8e2be68 100644 --- a/.github/workflows/aunit_tests.yml +++ b/.github/workflows/aunit_tests.yml @@ -9,7 +9,7 @@ on: [push] jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index ddb7cfa38..2b0113f9a 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -15,7 +15,7 @@ jobs: # https://github.com/actions/checkout#Checkout-multiple-repos-side-by-side, # but it looks like GITHUB_WORKSPACE is set to # /home/runner/work/AceTime/AceTime, so if path is set to 'main' as - # suggested in the article, then the repos is set to + # suggested in the article, then the repo location is set to # /home/runner/work/Acetime/AceTime/main/AceTime, which is really # confusing. Instead, use 'cd ..' to go up a level and call 'git clone' # manually. @@ -57,16 +57,13 @@ jobs: sudo apt update sudo apt install -y libcurl4-openssl-dev - - name: Build compare_cpp - run: | - make -C ../AceTimeTools/compare_cpp - # Run the validations from AceTimeValidations (BasicHinnantDateTest, # ExtendedHinnantDateTest, BasicAcetzTest, ExtendedAcetzTest). We don't run - # the others because when a new TZDB version comes out, the Python pytz, - # dateutil, and Java validation tests will fail because they depend - # on the obsolete TZ database on the host Operating System. - + # the others because when a new TZDB version comes out, the Python + # zoneinfo, Python pytz, Python dateutil, and Java validation tests will + # fail because they depend on the obsolete TZ database on the host + # Operating System. The 'make validations' will also compile the binary in + # AceTimeValidation/compare_cpp/ as necessary. - name: AceTimeValidation run: | cd ../AceTimeValidation diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf3e9509..911c0f035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,39 @@ # Changelog * Unreleased +* 1.10.0 (2022-01-18, TZDB 2021e) + * MemoryBenchmark: Add memory consumption for `ZoneSorterByName` and + `ZoneSorterByOffsetAndName`. + * AVR: 180-530 bytes of flash + * 32-bit: 120-600 bytes of flash + * Rename internal `TransitionStorage::findTransition()` to + `findTransitionForSeconds()` for better self-documentation. + * Move third party SAMD21 boards to new Tier 3 (May work, but not supported) + level. + * I can no longer upload binaries to these boards using Arduino IDE + 1.8.19 and SparkFun SAMD Core 1.8.6. + * Add support for `fold` parameter to control behavior around DST gaps + and overlaps. + * The semantics of the `fold` parameter is intended to be identical to + [Python PEP 495](https://www.python.org/dev/peps/pep-0495). + * Add `LocalTime::fold()`, `LocalDateTime::fold()`, + `OffsetDateTime::fold()`, `ZonedDateTime::fold()`. + * Update `ExtendedZoneProcessor::getOffsetDateTime(acetime_t)` to + calculate the `OffsetDateTime::fold()` as an output parameter. + * Update `ExtendedZoneProcessor::getOffsetDateTime(const + LocalDateTime&)` to handle `LocalDateTime::fold()` as an input + parameter. + * Increases flash usage of `ExtendedZoneProcessor` by around 600 bytes + on AVR, and 400-600 bytes on 32-bit processors. + * Add `toUnixSeconds64()` and `forUnixSeconds64()` methods to + `LocalDate`, `LocalDateTime`, `OffsetDateTime`, `ZonedDateTime`. + * These use 64-bit `int64_t` integers, which allows Unix seconds to be + used up to 2068-01-19T03:14:07Z (which is the limit of these + various classes due to the internal use of 32-bit `acetime_t`). + * These methods make it easier to interoperate with the `time_t` typedef + for `int64_t` on the ESP8266 and ESP32 platforms. + * Add [EspTime](examples/EspTime) app that shows how to integrate + the SNTP client on the ESP8266 and ESP32 platforms with AceTime. * 1.9.0 (2021-12-02, TZDB 2021e) * Add `ZoneSorterByName` and `ZoneSorterByOffsetAndName` classes to sort zone indexes, ids, or names according to 2 pre-defined sorting diff --git a/DEVELOPER.md b/DEVELOPER.md index 09dd6077f..b3e64e1ff 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -36,7 +36,7 @@ Here is the dependency diagram among these projects. AceTimeTools -------- ^ ^ ^ \ artransformer.py creating / | \ creating \ -> bufestimator.py - zonedb[x] / | \ zonedbpy \ -> zone_processor.py + zonedb[x] / | \ zonedb \ -> zone_processor.py / | \ v AceTime | AceTimePython ^ ^ | ^ @@ -54,7 +54,7 @@ the buffer sizes needed by the C++ `ExtendedZoneProcessor` class. The module to calculate those buffer sizes. On the other hand, AceTimePython needs AceTimeTools to generate the zoneinfo -files under `AceTimePython/zonedbpy`, which are consumed by the `acetz.py` +files under `AceTimePython/zonedb`, which are consumed by the `acetz.py` module. Fortunately, AceTimePython does *not* need AceTimeTools during runtime, so 3rd party consumers can incorporate AceTimePython without pulling in AceTimeTools. @@ -289,31 +289,60 @@ SAVE (15-min resolution) ## ExtendedZoneProcessor -To save memory, the `ExtendedZoneProcessor` class calculates the `Transition` -objects on the fly when the `initForEpochSeconds(acetime_t)` or -`initForYear(int16_t)` method is called. The alternative used by most Date-Time -libraries to precalculate the Transitions for all Zones, for a certain range of -years. Precalculation is definitely faster, and easier because the logic for -calculating the Transition objects resides in only a single place. However, the -expansion of the Transition objects consumes too much memory on an embedded -microcontrollers. - -When a request to create a `ZonedDateTime` object comes in, the -`ExtendedZoneProcessor.findTransition(epochSeconds)` method is called. It scans -the list of Transitions calculated above, looking for a match. The matching -Transition object contains the relevant standard offset and DST offset of that -Zone. - -The calculation of the Transitions is very complex, and I find that I can no -longer understand my own code after a few months away of this code base. So here -are some notes to help my future-self. The code is in -`Transition::initForYear(int16_t)` and is organized into 5 steps as described -below. +There are 2 fundamental ways that a `ZonedDateTime` can be constructed: + +1) Using its human-readable components and its timezone through the +`ZonedDateTime::forComponents()` factory method. The human-readable date can be: + * An undefined time in a DST gap, or + * A duplicate time during a DST overlap. +2) Using the epochSeconds and its timezone through the +`ZoneDateTime::forEpochSeconds()` factory method. + * The epochSeconds always specifies a well-defined unique time. + +The call stack of the first method looks like this: + +``` +ZoneDateTime::forComponents() + -> TimeZone::getOffsetDateTime(LocalDateTime&) + -> ExtendeZoneProcessor::getOffsetDateTime(LocalDateTime&) + -> TransitionStorage::findTransitionForDateTime(LocalDateTime&) +``` + +The call stack of the second method looks like this: + +``` +ZoneDateTime::forEpochSeconds(acetime_t) + -> TimeZone::getUtcOffset(acetime_t) + -> ExtendedZoneProcessor::getUtcOffset(acetime_t) + -> TransitionStorage::findTransitionForSeconds(acetime_t) +``` + +Both the `findTransitionForDateTime()` and `findTransitionForSeconds()` methods +search the list of Transitions of the specified TimeZone for a matching +Transition. Most Date-Time libraries precalculate these Transitions for all +Zones, for a certain range of years. Precalculation is definitely faster, and +easier because the logic for calculating the Transition objects resides in only +a single place. + +The precalculation of Transition objects for all zones and years consumes too +much memory on an embedded microcontrollers. To save memory, the +`ExtendedZoneProcessor` class in the AceTime library calculates the `Transition` +objects lazily, for only a single zone and for a single year (technically, for a +14-month interval covering the 12 months of the specified year) when the +`initForEpochSeconds(acetime_t)` or `initForYear(int16_t)` method is called. The +list of Transitions for a single year is cached, so that subsequent requests in +the same year avoid the work of recalculating the Transitions. + +The calculation of the Transitions for a given zone and year is very complex, +and I find that I can no longer understand my own code after a few months away +of this code base. So here are some notes to help my future-self. The code is in +`ExtendedZoneProcessor::initForYear(int16_t)` and is organized into 5 steps as +described below. ### Step 1: Find Matches -The `ExtendedZoneProcessor.findMatches()` finds all `ZoneEra` objects which +The `ExtendedZoneProcessor::findMatches()` finds all `ZoneEra` objects which overlap with the 14-month interval from Dec 1 of the prior year until Feb 1 of the following year. For example, `initForYear(2010)` means that the interval is from 2009-12-01T00:00 until 2011-02-01T00:00. @@ -574,12 +603,11 @@ available. * `$ make` * `$ ./ExtendedHinnantDateTest.out | grep failed` * There should be no failures. -* Update the zoneinfo files for AceTimePython (needed by BasicAcetzTest and +* Update the zonedb files for AceTimePython (needed by BasicAcetzTest and ExtendedAcetzTest): - * ``zonedbpy` - * `$ cd AceTimePython/src/acetime/zonedbpy` - * Edit the `Makefile` and update the `TZ_VERSION`. - * `$ make` + * `$ cd AceTimePython/src/acetime/zonedb` + * Edit the `Makefile` and update the `TZ_VERSION`. + * `$ make` * Verify that the `AceTime` library and the `AceTimePython` library agree with each other using the same TZDB version. This requires going into the [AceTimeValidation](https://github.com/bxparks/AceTimeValidation) project. diff --git a/README.md b/README.md index 2731d27ca..35ad95322 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ date and time between different time zones, properly accounting for all DST transitions from the year 2000 until 2050. The ZoneInfo Database is extracted from the [IANA TZ database](https://www.iana.org/time-zones). Different subsets of the ZoneInfo Database can be compiled into the application to reduce flash -memory size. +memory size. Standard C-library `time_t` types, 32-bit and 64-bit, are supported +through conversion methods. The companion library [AceTimeClock](https://github.com/bxparks/AceTimeClock) provides Clock classes to retrieve the time from more accurate sources, such as @@ -33,7 +34,10 @@ RTC](https://www.maximintegrated.com/en/products/analog/real-time-clocks/DS3231. chip. A special version of the `Clock` class called the `SystemClock` provides a fast and accurate "epoch seconds" across all Arduino compatible systems. This "epoch seconds" can be given to the classes in this library to convert it into -human readable components in different timezones. +human readable components in different timezones. On the ESP8266 and ESP32, the +AceTime library can be used with the SNTP client and the C-library `time()` +function through the 64-bit `time_t` value. See [ESP8266 and ESP32 +TimeZones](#Esp8266AndEspTimeZones) below. The primordial motivation for creating the AceTime library was to build a digital clock with an OLED or LED display, that would show the date and time of @@ -49,7 +53,7 @@ This library can be an alternative to the Arduino Time (https://github.com/PaulStoffregen/Time) and Arduino Timezone (https://github.com/JChristensen/Timezone) libraries. -**Version**: 1.9.0 (2021-12-02, TZDB version 2021e) +**Version**: 1.10.0 (2022-01-18, TZDB version 2021e) **Changelog**: [CHANGELOG.md](CHANGELOG.md) @@ -81,6 +85,7 @@ This library can be an alternative to the Arduino Time * [Comparison to Other Time Libraries](#Comparisons) * [Arduino Time Library](#ArduinoTimeLibrary) * [C Time Library](#CLibrary) + * [ESP8266 and ESP32 TimeZones](#Esp8266AndEspTimeZones) * [ezTime](#EzTime) * [Micro Time Zone](#MicroTimeZone) * [Java Time, Joda-Time, Noda Time](#JavaTime) @@ -95,16 +100,18 @@ This library can be an alternative to the Arduino Time The latest stable release is available in the Arduino Library Manager in the IDE. Search for "AceTime". Click install. The Library Manager should -automatically install AceTime and its the dependent library: +automatically install AceTime and its dependent libraries: * AceTime (https://github.com/bxparks/AceTime) * AceCommon (https://github.com/bxparks/AceCommon) +* AceSorting (https://github.com/bxparks/AceSorting) The development version can be installed by cloning the above repos manually. You can copy over the contents to the `./libraries` directory used by the -Arduino IDE. (The result is a set of directories named `./libraries/AceTime` and -`./libraries/AceCommon`). Or you can create symlinks from `./libraries` to these -directories. Or you can `git clone` directly into the `./libraries` directory. +Arduino IDE. (The result is a set of directories named `./libraries/AceTime`, +`./libraries/AceCommon`, `./libraries/AceSorting`). Or you can create symlinks +from `./libraries` to these directories. Or you can `git clone` directly into +the `./libraries` directory. The `develop` branch contains the latest development. The `master` branch contains the stable releases. @@ -131,7 +138,10 @@ The source files are organized as follows: * [examples/HelloDateTime](examples/HelloDateTime) * Simple demo of `ZonedDateTime` class * [examples/HelloZoneManager](examples/HelloZoneManager) - * Simple demo of `BasicZoneManager` class + * Simple demo of `ExtendedZoneManager` class + * Advanced + * [examples/EspTime](examples/EspTime) + * Use AceTime with the built-in SNTP client of ESP8266 and ESP32. * Benchmarks * [examples/MemoryBenchmark](examples/MemoryBenchmark) * determine flash and static memory consumption of various classes @@ -198,7 +208,7 @@ zones: using namespace ace_time; // ZoneProcessor instances should be created statically at initialization time. -static BasicZoneProcessor pacificProcessor; +static BasicZoneProcessor losAngelesProcessor; static BasicZoneProcessor londonProcessor; void setup() { @@ -206,52 +216,57 @@ void setup() { Serial.begin(115200); while (!Serial); // Wait until Serial is ready - Leonardo/Micro - auto pacificTz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles, - &pacificProcessor); - auto londonTz = TimeZone::forZoneInfo(&zonedb::kZoneEurope_London, - &londonProcessor); + // TimeZone objects are light-weight and can be created on the fly. + TimeZone losAngelesTz = TimeZone::forZoneInfo( + &zonedb::kZoneAmerica_Los_Angeles, + &losAngelesProcessor); + TimeZone londonTz = TimeZone::forZoneInfo( + &zonedb::kZoneEurope_London, + &londonProcessor); // Create from components. 2019-03-10T03:00:00 is just after DST change in // Los Angeles (2am goes to 3am). - auto startTime = ZonedDateTime::forComponents( - 2019, 3, 10, 3, 0, 0, pacificTz); + ZonedDateTime startTime = ZonedDateTime::forComponents( + 2019, 3, 10, 3, 0, 0, losAngelesTz); Serial.print(F("Epoch Seconds: ")); acetime_t epochSeconds = startTime.toEpochSeconds(); Serial.println(epochSeconds); Serial.print(F("Unix Seconds: ")); - acetime_t unixSeconds = startTime.toUnixSeconds(); + int32_t unixSeconds = startTime.toUnixSeconds(); Serial.println(unixSeconds); Serial.println(F("=== Los_Angeles")); - auto pacificTime = ZonedDateTime::forEpochSeconds(epochSeconds, pacificTz); + ZonedDateTime losAngelesTime = ZonedDateTime::forEpochSeconds( + epochSeconds, losAngelesTz); Serial.print(F("Time: ")); - pacificTime.printTo(Serial); + losAngelesTime.printTo(Serial); Serial.println(); Serial.print(F("Day of Week: ")); Serial.println( - DateStrings().dayOfWeekLongString(pacificTime.dayOfWeek())); + DateStrings().dayOfWeekLongString(losAngelesTime.dayOfWeek())); // Print info about UTC offset - TimeOffset offset = pacificTime.timeOffset(); + TimeOffset offset = losAngelesTime.timeOffset(); Serial.print(F("Total UTC Offset: ")); offset.printTo(Serial); Serial.println(); // Print info about the current time zone Serial.print(F("Zone: ")); - pacificTz.printTo(Serial); + losAngelesTz.printTo(Serial); Serial.println(); // Print the current time zone abbreviation, e.g. "PST" or "PDT" Serial.print(F("Abbreviation: ")); - Serial.print(pacificTz.getAbbrev(epochSeconds)); + Serial.print(losAngelesTz.getAbbrev(epochSeconds)); Serial.println(); // Create from epoch seconds. London is still on standard time. - auto londonTime = ZonedDateTime::forEpochSeconds(epochSeconds, londonTz); + ZonedDateTime londonTime = ZonedDateTime::forEpochSeconds( + epochSeconds, londonTz); Serial.println(F("=== London")); Serial.print(F("Time: ")); @@ -263,16 +278,16 @@ void setup() { londonTz.printTo(Serial); Serial.println(); - // Print the current time zone abbreviation, e.g. "PST" or "PDT" + // Print the current time zone abbreviation, e.g. "GMT" or "BST" Serial.print(F("Abbreviation: ")); Serial.print(londonTz.getAbbrev(epochSeconds)); Serial.println(); Serial.println(F("=== Compare ZonedDateTime")); - Serial.print(F("pacificTime.compareTo(londonTime): ")); - Serial.println(pacificTime.compareTo(londonTime)); - Serial.print(F("pacificTime == londonTime: ")); - Serial.println((pacificTime == londonTime) ? "true" : "false"); + Serial.print(F("losAngelesTime.compareTo(londonTime): ")); + Serial.println(losAngelesTime.compareTo(londonTime)); + Serial.print(F("losAngelesTime == londonTime: ")); + Serial.println((losAngelesTime == londonTime) ? "true" : "false"); } void loop() { @@ -294,17 +309,21 @@ Time: 2019-03-10T10:00:00+00:00[Europe/London] Zone: Europe/London Abbreviation: GMT === Compare ZonedDateTime -pacificTime.compareTo(londonTime): 0 -pacificTime == londonTime: false +losAngelesTime.compareTo(londonTime): 0 +losAngelesTime == londonTime: false ``` ### HelloZoneManager The [examples/HelloZoneManager](examples/HelloZoneManager) example shows how to -load the entire ZoneInfo Database into a `BasicZoneManager`, then create 3 time -zones using 3 different ways: `createForZoneInfo()`, `createForZoneName()`, and -`createForZoneId()`. +load the entire ZoneInfo Database into an `ExtendedZoneManager`, then create 3 +time zones using 3 different ways: `createForZoneInfo()`, `createForZoneName()`, +and `createForZoneId()`. This program requires a 32-bit microcontroller +environment because of its flash memory size. Using the `ExtendedZoneManager` +with the `zonedbx::kZoneRegistry` consumes ~34kB of flash which no longer fits +on an Arduino Nano. If the `ExtendedZoneManager` is replaced with the +`BasicZoneManager`, the flash size goes down to about ~22kB. ```C++ #include @@ -312,35 +331,37 @@ zones using 3 different ways: `createForZoneInfo()`, `createForZoneName()`, and using namespace ace_time; -// Create a BasicZoneManager with the entire TZ Database of ZONE entries. Use -// kZoneAndLinkRegistrySize and kZoneAndLinkRegistry to include LINK entries as -// well, at the cost of additional flash consumption. Cache size of 3 means that -// it can support 3 concurrent timezones without performance penalties. +// Create an ExtendedZoneManager with the entire TZ Database of Zone entries. +// Cache size of 3 means that it can support 3 concurrent timezones without +// performance penalties. static const int CACHE_SIZE = 3; -static BasicZoneProcessorCache zoneProcessorCache; -static BasicZoneManager manager( - zonedb::kZoneRegistrySize, zonedb::kZoneRegistry, zoneProcessorCache); +static ExtendedZoneProcessorCache zoneProcessorCache; +static ExtendedZoneManager manager( + zonedbx::kZoneRegistrySize, + zonedbx::kZoneRegistry, + zoneProcessorCache); void setup() { Serial.begin(115200); - while (!Serial); // Wait Serial is ready - Leonardo/Micro - - // Create Los Angeles by ZoneInfo - auto pacificTz = manager.createForZoneInfo(&zonedb::kZoneAmerica_Los_Angeles); - auto pacificTime = ZonedDateTime::forComponents( - 2019, 3, 10, 3, 0, 0, pacificTz); - pacificTime.printTo(Serial); + while (!Serial); // Wait until ready - Leonardo/Micro + + // Create America/Los_Angeles timezone by ZoneInfo. + TimeZone losAngelesTz = manager.createForZoneInfo( + &zonedb::kZoneAmerica_Los_Angeles); + ZonedDateTime losAngelesTime = ZonedDateTime::forComponents( + 2019, 3, 10, 3, 0, 0, losAngelesTz); + losAngelesTime.printTo(Serial); Serial.println(); - // Create London by ZoneName - auto londonTz = manager.createForZoneName("Europe/London"); - auto londonTime = pacificTime.convertToTimeZone(londonTz); + // Create Europe/London timezone by ZoneName. + TimeZone londonTz = manager.createForZoneName("Europe/London"); + ZonedDateTime londonTime = losAngelesTime.convertToTimeZone(londonTz); londonTime.printTo(Serial); Serial.println(); - // Create Sydney by ZoneId - auto sydneyTz = manager.createForZoneId(zonedb::kZoneIdAustralia_Sydney); - auto sydneyTime = pacificTime.convertToTimeZone(sydneyTz); + // Create Australia/Sydney timezone by ZoneId. + TimeZone sydneyTz = manager.createForZoneId(zonedb::kZoneIdAustralia_Sydney); + ZonedDateTime sydneyTime = losAngelesTime.convertToTimeZone(sydneyTz); sydneyTime.printTo(Serial); Serial.println(); } @@ -436,19 +457,23 @@ The details of how the Date, Time and TimeZone classes are validated are given in [AceTimeValidation](https://github.com/bxparks/AceTimeValidation). The ZoneInfo Database and the algorithms in this library have been validated to -match the UTC offsets calculated using 5 other date/time libraries written in +match the UTC offsets calculated using other date/time libraries written in different programming languages: * the Python pytz (https://pypi.org/project/pytz/) library from the year 2000 until 2037 (inclusive), -* the Python dateutil (https://pypi.org/project/python-dateutil/) library from +* the Python dateutil (https://pypi.org/project/python-dateutil/) library from the year 2000 until 2037 (inclusive), +* the Python 3.9 zoneinfo (https://docs.python.org/3/library/zoneinfo.html) + library from the year 1974 until 2049 (inclusive), * the Java JDK 11 [java.time](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/package-summary.html) - library from year 2000 to 2049 (inclusive), + library from year 2000 to 2049 (inclusive) * the C++11/14/17 Hinnant date (https://github.com/HowardHinnant/date) library - from year 1974 to 2049 (inclusive). + from year 1974 to 2049 (inclusive) * the C# Noda Time (https://nodatime.org) library from 1974 to 2049 (inclusive) +* the Go lang 1.17 `time` package (https://pkg.go.dev/time) in the standard + library from 1974 to 2049 (inclusive) ## Resource Consumption @@ -460,13 +485,13 @@ different programming languages: ``` sizeof(LocalDate): 3 -sizeof(LocalTime): 3 -sizeof(LocalDateTime): 6 +sizeof(LocalTime): 4 +sizeof(LocalDateTime): 7 sizeof(TimeOffset): 2 -sizeof(OffsetDateTime): 8 +sizeof(OffsetDateTime): 9 sizeof(TimeZone): 5 sizeof(TimeZoneData): 5 -sizeof(ZonedDateTime): 13 +sizeof(ZonedDateTime): 14 sizeof(TimePeriod): 4 sizeof(BasicZoneProcessor): 116 sizeof(ExtendedZoneProcessor): 432 @@ -492,13 +517,13 @@ sizeof(ExtendedZoneProcessor::MatchingEra): 20 **32-bit processors** ``` sizeof(LocalDate): 3 -sizeof(LocalTime): 3 -sizeof(LocalDateTime): 6 +sizeof(LocalTime): 4 +sizeof(LocalDateTime): 7 sizeof(TimeOffset): 2 -sizeof(OffsetDateTime): 8 +sizeof(OffsetDateTime): 10 sizeof(TimeZone): 12 sizeof(TimeZoneData): 8 -sizeof(ZonedDateTime): 20 +sizeof(ZonedDateTime): 24 sizeof(TimePeriod): 4 sizeof(BasicZoneProcessor): 164 sizeof(ExtendedZoneProcessor): 540 @@ -574,23 +599,29 @@ Arduino Nano: |----------------------------------------+--------------+-------------| | baseline | 474/ 11 | 0/ 0 | |----------------------------------------+--------------+-------------| -| LocalDateTime | 1128/ 19 | 654/ 8 | -| ZonedDateTime | 1378/ 26 | 904/ 15 | -| Manual ZoneManager | 1314/ 13 | 840/ 2 | +| LocalDateTime | 1132/ 20 | 658/ 9 | +| ZonedDateTime | 1306/ 27 | 832/ 16 | +| Manual ZoneManager | 1242/ 13 | 768/ 2 | |----------------------------------------+--------------+-------------| -| Basic TimeZone (1 zone) | 6094/ 315 | 5620/ 304 | -| Basic TimeZone (2 zones) | 6390/ 435 | 5916/ 424 | -| BasicZoneManager (1 zone) | 6300/ 326 | 5826/ 315 | -| BasicZoneManager (all zones) | 19042/ 702 | 18568/ 691 | -| BasicZoneManager (all zones+links) | 23374/ 702 | 22900/ 691 | +| Basic TimeZone (1 zone) | 6240/ 317 | 5766/ 306 | +| Basic TimeZone (2 zones) | 6398/ 437 | 5924/ 426 | +| BasicZoneManager (1 zone) | 6446/ 328 | 5972/ 317 | +| BasicZoneManager (all zones) | 19188/ 704 | 18714/ 693 | +| BasicZoneManager (all zones+links) | 23520/ 704 | 23046/ 693 | | BasicLinkManager (all links) | 2378/ 16 | 1904/ 5 | |----------------------------------------+--------------+-------------| -| Extended TimeZone (1 zone) | 8976/ 670 | 8502/ 659 | -| Extended TimeZone (2 zones) | 9368/ 1111 | 8894/ 1100 | -| ExtendedZoneManager (1 zone) | 9152/ 676 | 8678/ 665 | -| ExtendedZoneManager (all zones) | 30918/ 1160 | 30444/ 1149 | -| ExtendedZoneManager (all zones+links) | 35798/ 1160 | 35324/ 1149 | +| Basic ZoneSorterByName [1] | 6610/ 328 | 370/ 11 | +| Basic ZoneSorterByOffsetAndName [1] | 6742/ 328 | 502/ 11 | +|----------------------------------------+--------------+-------------| +| Extended TimeZone (1 zone) | 9606/ 672 | 9132/ 661 | +| Extended TimeZone (2 zones) | 9830/ 1113 | 9356/ 1102 | +| ExtendedZoneManager (1 zone) | 9782/ 678 | 9308/ 667 | +| ExtendedZoneManager (all zones) | 31548/ 1162 | 31074/ 1151 | +| ExtendedZoneManager (all zones+links) | 36428/ 1162 | 35954/ 1151 | | ExtendedLinkManager (all links) | 2570/ 16 | 2096/ 5 | +|----------------------------------------+--------------+-------------| +| Extended ZoneSorterByName [2] | 9962/ 678 | 356/ 6 | +| Extended ZoneSorterByOffsetAndName [2] | 10104/ 678 | 498/ 6 | +---------------------------------------------------------------------+ ``` @@ -603,22 +634,28 @@ ESP8266: | baseline | 260089/27892 | 0/ 0 | |----------------------------------------+--------------+-------------| | LocalDateTime | 260545/27908 | 456/ 16 | -| ZonedDateTime | 260641/27924 | 552/ 32 | -| Manual ZoneManager | 260557/27896 | 468/ 4 | +| ZonedDateTime | 260961/27924 | 872/ 32 | +| Manual ZoneManager | 260941/27896 | 852/ 4 | |----------------------------------------+--------------+-------------| -| Basic TimeZone (1 zone) | 266169/28644 | 6080/ 752 | -| Basic TimeZone (2 zones) | 266537/28804 | 6448/ 912 | -| BasicZoneManager (1 zone) | 266313/28660 | 6224/ 768 | -| BasicZoneManager (all zones) | 283657/28660 | 23568/ 768 | -| BasicZoneManager (all zones+links) | 290361/28660 | 30272/ 768 | +| Basic TimeZone (1 zone) | 266473/28644 | 6384/ 752 | +| Basic TimeZone (2 zones) | 266841/28804 | 6752/ 912 | +| BasicZoneManager (1 zone) | 266633/28660 | 6544/ 768 | +| BasicZoneManager (all zones) | 283961/28660 | 23872/ 768 | +| BasicZoneManager (all zones+links) | 290665/28660 | 30576/ 768 | | BasicLinkManager (all links) | 261933/27904 | 1844/ 12 | |----------------------------------------+--------------+-------------| -| Extended TimeZone (1 zone) | 268441/29172 | 8352/ 1280 | -| Extended TimeZone (2 zones) | 268745/29724 | 8656/ 1832 | -| ExtendedZoneManager (1 zone) | 268569/29180 | 8480/ 1288 | -| ExtendedZoneManager (all zones) | 298461/29176 | 38372/ 1284 | -| ExtendedZoneManager (all zones+links) | 306029/29176 | 45940/ 1284 | +| Basic ZoneSorterByName [1] | 266869/28664 | 396/ 20 | +| Basic ZoneSorterByOffsetAndName [1] | 266981/28664 | 508/ 20 | +|----------------------------------------+--------------+-------------| +| Extended TimeZone (1 zone) | 269113/29172 | 9024/ 1280 | +| Extended TimeZone (2 zones) | 269481/29724 | 9392/ 1832 | +| ExtendedZoneManager (1 zone) | 269241/29180 | 9152/ 1288 | +| ExtendedZoneManager (all zones) | 299117/29176 | 39028/ 1284 | +| ExtendedZoneManager (all zones+links) | 306685/29176 | 46596/ 1284 | | ExtendedLinkManager (all links) | 262125/27904 | 2036/ 12 | +|----------------------------------------+--------------+-------------| +| Extended ZoneSorterByName [2] | 269461/29184 | 348/ 12 | +| Extended ZoneSorterByOffsetAndName [2] | 269557/29184 | 444/ 12 | +---------------------------------------------------------------------+ ``` @@ -638,21 +675,24 @@ Arduino Nano: | EmptyLoop | 4.000 | |--------------------------------------------------+----------| | LocalDate::forEpochDays() | 219.000 | -| LocalDate::toEpochDays() | 57.000 | -| LocalDate::dayOfWeek() | 48.000 | +| LocalDate::toEpochDays() | 54.000 | +| LocalDate::dayOfWeek() | 49.000 | |--------------------------------------------------+----------| -| OffsetDateTime::forEpochSeconds() | 324.000 | -| OffsetDateTime::toEpochSeconds() | 86.000 | +| OffsetDateTime::forEpochSeconds() | 334.000 | +| OffsetDateTime::toEpochSeconds() | 85.000 | |--------------------------------------------------+----------| -| ZonedDateTime::toEpochSeconds() | 84.000 | -| ZonedDateTime::toEpochDays() | 73.000 | -| ZonedDateTime::forEpochSeconds(UTC) | 339.000 | -| ZonedDateTime::forEpochSeconds(Basic_nocache) | 1186.000 | -| ZonedDateTime::forEpochSeconds(Basic_cached) | 619.000 | -| ZonedDateTime::forEpochSeconds(Extended_nocache) | 2139.000 | -| ZonedDateTime::forEpochSeconds(Extended_cached) | 617.000 | +| ZonedDateTime::toEpochSeconds() | 85.000 | +| ZonedDateTime::toEpochDays() | 72.000 | +| ZonedDateTime::forEpochSeconds(UTC) | 362.000 | +| ZonedDateTime::forEpochSeconds(Basic_nocache) | 1198.000 | +| ZonedDateTime::forEpochSeconds(Basic_cached) | 643.000 | +| ZonedDateTime::forEpochSeconds(Extended_nocache) | 2231.000 | +| ZonedDateTime::forEpochSeconds(Extended_cached) | 647.000 | |--------------------------------------------------+----------| -| BasicZoneManager::createForZoneName(binary) | 119.000 | +| ZonedDateTime::forComponents(Extended_nocache) | 1598.000 | +| ZonedDateTime::forComponents(Extended_cached) | 82.000 | +|--------------------------------------------------+----------| +| BasicZoneManager::createForZoneName(binary) | 121.000 | | BasicZoneManager::createForZoneId(binary) | 48.000 | | BasicZoneManager::createForZoneId(linear) | 305.000 | +--------------------------------------------------+----------+ @@ -667,26 +707,29 @@ ESP8266: |--------------------------------------------------+----------| | EmptyLoop | 4.800 | |--------------------------------------------------+----------| -| LocalDate::forEpochDays() | 8.000 | -| LocalDate::toEpochDays() | 3.800 | -| LocalDate::dayOfWeek() | 3.800 | +| LocalDate::forEpochDays() | 7.800 | +| LocalDate::toEpochDays() | 3.200 | +| LocalDate::dayOfWeek() | 3.400 | +|--------------------------------------------------+----------| +| OffsetDateTime::forEpochSeconds() | 13.200 | +| OffsetDateTime::toEpochSeconds() | 7.000 | |--------------------------------------------------+----------| -| OffsetDateTime::forEpochSeconds() | 12.100 | -| OffsetDateTime::toEpochSeconds() | 6.800 | +| ZonedDateTime::toEpochSeconds() | 7.000 | +| ZonedDateTime::toEpochDays() | 5.600 | +| ZonedDateTime::forEpochSeconds(UTC) | 16.600 | +| ZonedDateTime::forEpochSeconds(Basic_nocache) | 99.000 | +| ZonedDateTime::forEpochSeconds(Basic_cached) | 29.800 | +| ZonedDateTime::forEpochSeconds(Extended_nocache) | 196.800 | +| ZonedDateTime::forEpochSeconds(Extended_cached) | 30.200 | |--------------------------------------------------+----------| -| ZonedDateTime::toEpochSeconds() | 6.900 | -| ZonedDateTime::toEpochDays() | 5.800 | -| ZonedDateTime::forEpochSeconds(UTC) | 13.000 | -| ZonedDateTime::forEpochSeconds(Basic_nocache) | 95.700 | -| ZonedDateTime::forEpochSeconds(Basic_cached) | 26.100 | -| ZonedDateTime::forEpochSeconds(Extended_nocache) | 189.600 | -| ZonedDateTime::forEpochSeconds(Extended_cached) | 25.800 | +| ZonedDateTime::forComponents(Extended_nocache) | 167.400 | +| ZonedDateTime::forComponents(Extended_cached) | 6.400 | |--------------------------------------------------+----------| -| BasicZoneManager::createForZoneName(binary) | 14.700 | -| BasicZoneManager::createForZoneId(binary) | 6.600 | -| BasicZoneManager::createForZoneId(linear) | 43.900 | +| BasicZoneManager::createForZoneName(binary) | 14.600 | +| BasicZoneManager::createForZoneId(binary) | 6.400 | +| BasicZoneManager::createForZoneId(linear) | 44.800 | +--------------------------------------------------+----------+ -Iterations_per_run: 10000 +Iterations_per_run: 5000 ``` @@ -695,26 +738,44 @@ Iterations_per_run: 10000 ### Hardware -This library has Tier 1 support on the following boards: +**Tier 1: Fully supported** + +These boards are tested on each release: * Arduino Nano (16 MHz ATmega328P) * SparkFun Pro Micro (16 MHz ATmega32U4) -* SAMD21 M0 Mini (48 MHz ARM Cortex-M0+) * STM32 Blue Pill (STM32F103C8, 72 MHz ARM Cortex-M3) * NodeMCU 1.0 (ESP-12E module, 80 MHz ESP8266) * WeMos D1 Mini (ESP-12E module, 80 MHz ESP8266) * ESP32 dev board (ESP-WROOM-32 module, 240 MHz dual core Tensilica LX6) * Teensy 3.2 (96 MHz ARM Cortex-M4) -Tier 2 support can be expected on the following boards, mostly because I don't -test these as often: +**Tier 2: Should work** + +These boards should work but I don't test them as often: * ATtiny85 (8 MHz ATtiny85) * Arduino Pro Mini (16 MHz ATmega328P) * Mini Mega 2560 (Arduino Mega 2560 compatible, 16 MHz ATmega2560) * Teensy LC (48 MHz ARM Cortex-M0+) -The following boards are *not* supported: +**Tier 3: May work, but not supported** + +* SAMD21 M0 Mini (48 MHz ARM Cortex-M0+) + * Arduino-branded SAMD21 boards use the ArduinoCore-API, so are explicitly + blacklisted. See below. + * Other 3rd party SAMD21 boards *may* work using the SparkFun SAMD core. + * However, as of SparkFun SAMD Core v1.8.6 and Arduino IDE 1.8.19, I can no + longer upload binaries to these 3rd party boards due to errors. + * Therefore, third party SAMD21 boards are now in this new Tier 3 category. + * The AceTime library may work on these boards, but I can no longer support + them. + +**Tier Blacklisted** + +The following boards are *not* supported and are explicitly blacklisted to allow +the compiler to print useful error messages instead of hundreds of lines of +compiler errors: * Any platform using the ArduinoCore-API (https://github.com/arduino/ArduinoCore-api), such as: @@ -728,17 +789,17 @@ The following boards are *not* supported: This library was developed and tested using: -* [Arduino IDE 1.8.16](https://www.arduino.cc/en/Main/Software) +* [Arduino IDE 1.8.19](https://www.arduino.cc/en/Main/Software) * [Arduino CLI 0.19.2](https://arduino.github.io/arduino-cli) * [SpenceKonde ATTinyCore 1.5.2](https://github.com/SpenceKonde/ATTinyCore) -* [Arduino AVR Boards 1.8.3](https://github.com/arduino/ArduinoCore-avr) +* [Arduino AVR Boards 1.8.4](https://github.com/arduino/ArduinoCore-avr) * [Arduino SAMD Boards 1.8.9](https://github.com/arduino/ArduinoCore-samd) * [SparkFun AVR Boards 1.1.13](https://github.com/sparkfun/Arduino_Boards) -* [SparkFun SAMD Boards 1.8.4](https://github.com/sparkfun/Arduino_Boards) -* [STM32duino 2.0.0](https://github.com/stm32duino/Arduino_Core_STM32) +* [SparkFun SAMD Boards 1.8.6](https://github.com/sparkfun/Arduino_Boards) +* [STM32duino 2.2.0](https://github.com/stm32duino/Arduino_Core_STM32) * [ESP8266 Arduino 3.0.2](https://github.com/esp8266/Arduino) -* [ESP32 Arduino 1.0.6](https://github.com/espressif/arduino-esp32) -* [Teensyduino 1.55](https://www.pjrc.com/teensy/td_download.html) +* [ESP32 Arduino 2.0.2](https://github.com/espressif/arduino-esp32) +* [Teensyduino 1.56](https://www.pjrc.com/teensy/td_download.html) This library is *not* compatible with: @@ -787,26 +848,31 @@ The problem with the POSIX format is that it is somewhat difficult for a human to understand, and the programmer must manually update this string when a timezone changes its DST transition rules. Also, there is no historical information in the POSIX string, so date and time written in the past cannot be -accurately expressed. The problem with the TZ Database is that most -implementations are too large to fit inside most Arduino environments. The -Arduino libraries that I am aware of use the POSIX format (e.g. -[ropg/ezTime](https://github.com/ropg/ezTime) or +accurately expressed. As far as I know, POSIX timezone strings can support only +2 DST transitions per year. However, there are a handful of timezones which have +(or used to have) 4 timezone transitions in a single year, which cannot be +represented by a POSIX string. Most Arduino timezone libraries use the POSIX +format (e.g. [ropg/ezTime](https://github.com/ropg/ezTime) or [JChristensen/Timezone](https://github.com/JChristensen/Timezone)) for simplicity and smaller memory footprint. -The AceTime library uses the TZ Database. When new versions of the database are -released (several times a year), I can regenerate the zone files, recompile the -application, and it will instantly use the new transition rules, without the -developer needing to create a new POSIX string. To address the memory constraint -problem, the AceTime library is designed to load only of the smallest subset of -the TZ Database that is required to support the selected timezones (1 to 4 have -been extensively tested). Dynamic lookup of the time zone is possible using the +The libraries that incorporate the full IANA TZ Database are often far too large +to fit inside the resource constrained Arduino environments. The AceTime library +has been optimized to reduce the flash memory size of the library as much as +possible. The application can choose to load only of the smallest subset of the +TZ Database that is required to support the selected timezones (1 to 4 have been +extensively tested). Dynamic lookup of the time zone is possible using the `ZoneManager`, and the app develop can customize it with the list of zones that are compiled into the app. On microcontrollers with more than about 32kB of flash memory (e.g. ESP8266, ESP32, Teensy 3.2) and depending on the size of the rest of the application, it may be possible to load the entire IANA TZ database. -This will allow the end-user to select the timezone dynamically, just like on -the big-iron machines. +This will allow the end-user to select the timezone dynamically, just like +desktop-class machines. + +When new versions of the database are released (several times a year), I can +regenerate the zone files, recompile the application, and it will instantly use +the new transition rules, without the developer needing to create a new POSIX +string. The AceTime library is inspired by and borrows from: * [Java 11 Time](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/package-summary.html) @@ -902,17 +968,47 @@ Arduino platforms, but not others: must be manually called, probably in an ISR (interrupt service routine). * The SAMD21 and Teensy platforms do not seem to have a `` library. * The ESP8266 and ESP32 have a `` library. - * contains some rudimentary support for POSIX formatted timezones. - * does not have the equivalent of the (non-standard) `mk_gmtime()` AVR - function. - * unknown, not researched: - * does the `time()` value auto-increment? - * is the source of `time()` the same as `millis()` or a different RTC? + * The `time()` function automatically increments through the + `system_get_time()` system call. + * Provides an SNTP client that can synchronize with an NTP service + and resynchronize the `time()` function. + * Adds `configTime()` functions to configure the behavior of the + SNTP service, including POSIX timezones. + * ESP8266 `TZ.h` containing pre-calculated POSIX timezone strings. These libraries are all based upon the [traditional C/Unix library methods](http://www.catb.org/esr/time-programming/) which can be difficult to understand. + +### ESP8266 and ESP32 TimeZones + +The ESP8266 platform provides a +[cores/esp8266/TZ.h](https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h) +file which contains a list of pre-generated POSIX timezone strings. These can be +passed into the `configTime()` function that initializes the SNTP service. + +The ESP32 platform does not provide a `TZ.h` file as far as I can tell. I +believe the same POSIX strings can be passed into its `configTime()` function. +But POSIX timezone strings have limitations that I described in +[Motivation](#Motivation), and the C-style library functions are often +confusing and hard to use. + +Application developers can choose to use the built-in SNTP service and the +`time()` function on the ESP8266 and ESP32 as the source of accurate clock, but +take advantage of the versatility and power of the AceTime library for timezone +conversions. AceTime v1.10 adds the `forUnixSeconds64()` and +`toUnixSeconds64()` methods in various classes to make it far easier to interact +with the 64-bit `time_t` integers returned by the `time()` function on these +platforms. + +See [EspTime](examples/EspTime) for an example of how to integrate AceTime with +the built-in SNTP service and `time()` function on the ESP8266 and ESP32. See +also the +[EspSntpClock](https://github.com/bxparks/AceTimeClock/blob/develop/src/ace_time/clock/EspSntpClock.h) +class in the AceTimeClock project which provides a thin-wrapper around this +service on the ESP platforms. + ### ezTime diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 87b4fe0c0..f9ca1e061 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1,10 +1,10 @@ # AceTime User Guide The primary purpose of AceTime classes is to convert between an integer -representing the number of seconds since Epoch (2000-01-01T00:00:00 UTC) to -human-readable components in different timezones. +representing the number of seconds since the AceTime Epoch (2000-01-01T00:00:00 +UTC) and the equivalent human-readable components in different timezones. -**Version**: 1.9.0 (2021-12-02, TZDB 2021e) +**Version**: 1.10.0 (2021-01-18, TZDB 2021e) **Related Documents**: @@ -32,16 +32,27 @@ human-readable components in different timezones. * [Basic TimeZone](#BasicTimeZone) * [Extended TimeZone](#ExtendedTimeZone) * [ZonedDateTime](#ZonedDateTime) + * [Class Declaration](#ZonedDateTimeDeclaration) + * [Creation](#ZonedDateTimeCreation) * [Conversion to Other Time Zones](#TimeZoneConversion) - * [Caching](#Caching) + * [DST Transition Caching](#DstTransitionCaching) * [ZoneManager](#ZoneManager) * [Class Hierarchy](#ClassHierarchy) * [Default Registries](#DefaultRegistries) + * [ZoneProcessorCache](#ZoneProcessorCache) + * [ZoneManager Creation](#ZoneManagerCreation) * [createForZoneName()](#CreateForZoneName) * [createForZoneId()](#CreateForZoneId) * [createForZoneIndex()](#CreateForZoneIndex) * [createForTimeZoneData()](#CreateForTimeZoneData) * [ManualZoneManager](#ManualZoneManager) + * [Handling Gaps and Overlaps](#HandlingGapsAndOverlaps) + * [Problems with Gaps and Overlaps](#ProblemsWithGapsAndOverlaps) + * [Classes with Fold](#ClassesWithFold) + * [Factory Methods with Fold](#FactoryMethodsWithFold) + * [Resource Consumption with Fold](#ResourceConsumptionWithFold) + * [Semantic Changes with Fold](#SemanticChangesWithFold) + * [Examples with Fold](#ExamplesWithFold) * [ZoneInfo Database](#ZoneInfoDatabase) * [ZoneInfo Entries](#ZoneInfoEntries) * [Basic zonedb](#BasicZonedb) @@ -63,8 +74,8 @@ human-readable components in different timezones. * [ZonedDateTime Normalization](#ZonedDateTimeNormalization) * [TimePeriod Mutations](#TimePeriodMutations) * [Error Handling](#ErrorHandling) + * [Invalid Sentinels](#InvalidSentinels) * [isError()](#IsError) - * [LocalDate::kInvalidEpochSeconds](#KInvalidEpochSeconds) * [Bugs and Limitations](#Bugs) @@ -281,16 +292,24 @@ integer, the largest value is 2,147,483,647. Therefore, the largest date that can be represented in this library is 2068-01-19T03:14:07 UTC. The `acetime_t` is analogous to the `time_t` type in the standard C library, -with 3 major differences: +with several major differences: * The `time_t` does not exist on all Arduino platforms. -* Modern implementations of the `time_t` type uses a 64-bit `int64_t` to prevent - the "Year 2038" overflow problem. Unfortunately, it is too resource intensive - to use a 64-bit integer on 8-bit processors. +* Some Arduino platforms and older Unix platforms use a 32-bit `int32_t` to + represent `time_t`. +* Modern implementations (e.g. ESP8266 and ESP32) use a 64-bit `int64_t` to + represent `time_t` to prevent the "Year 2038" overflow problem. Unfortunately, + AceTime cannot use 64-bit integers internally because they are too resource + intensive on 8-bit processors. * Most `time_t` implementations uses the Unix Epoch of 1970-01-01 00:00:00 UTC. - It is possible to convert between a `time_t` and an `acetime_t` by adding or - subtracting the appropriate number of seconds between the 2 Epoch dates. See - `LocalDate::kSecondsSinceUnixEpoch` + AceTime uses an epoch of 2000-01-01 00:00:00 UTC. + +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 constant is +946684800 and defined by `LocalDate::kSecondsSinceUnixEpoch`. Helper methods +are available on various classes to avoid manual conversion between these 2 +epochs: `forUnixSeconds()` and `toUnixSeconds()`, and the corresponding +`forUnixSeconds64()` and `toUnixSeconds64()` 64-bit versions added in v1.10. ### LocalDate and LocalTime @@ -333,10 +352,15 @@ class LocalTime { class LocalDate { public: static const int16_t kEpochYear = 2000; - static const acetime_t kInvalidEpochDays = INT32_MIN; - static const acetime_t kInvalidEpochSeconds = LocalTime::kInvalidSeconds; static const acetime_t kSecondsSinceUnixEpoch = 946684800; + static const int32_t kInvalidEpochDays = INT32_MIN; + static const acetime_t kInvalidEpochSeconds = INT32_MIN; + + static const int32_t kInvalidUnixDays = INT32_MIN; + static const int32_t kInvalidUnixSeconds = INT32_MIN; + static const int64_t kInvalidUnixSeconds64 = INT64_MIN; + static const uint8_t kMonday = 1; static const uint8_t kTuesday = 2; static const uint8_t kWednesday = 3; @@ -346,10 +370,12 @@ class LocalDate { static const uint8_t kSunday = 7; static LocalDate forComponents(int16_t year, uint8_t month, uint8_t day); - static LocalDate forEpochDays(acetime_t epochDays); - static LocalDate forUnixDays(acetime_t unixDays); + static LocalDate forEpochDays(int32_t epochDays); static LocalDate forEpochSeconds(acetime_t epochSeconds); - static LocalDate forUnixSeconds(acetime_t unixSeconds); + + static LocalDate forUnixDays(int32_t unixDays); + static LocalDate forUnixSeconds(int32_t unixSeconds); + static LocalDate forUnixSeconds64(int64_t unixSeconds); int16_t year() const; void year(int16_t year); @@ -363,10 +389,12 @@ class LocalDate { uint8_t dayOfWeek() const; bool isError() const; - acetime_t toEpochDays() const { - acetime_t toUnixDays() const { + int32_t toEpochDays() const { acetime_t toEpochSeconds() const { - acetime_t toUnixSeconds() const { + + int32_t toUnixDays() const { + int32_t toUnixSeconds() const { + int64_t toUnixSeconds64() const { int8_t compareTo(const LocalDate& that) const { void printTo(Print& printer) const; @@ -482,7 +510,8 @@ class LocalDateTime { 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 forUnixSeconds(acetime_t unixSeconds); + static LocalDateTime forUnixSeconds(int32_t unixSeconds); + static LocalDateTime forUnixSeconds64(int64_t unixSeconds); static LocalDateTime forDateString(const char* dateString); bool isError() const; @@ -510,10 +539,12 @@ class LocalDateTime { const LocalDate& localDate() const; const LocalTime& localTime() const; - acetime_t toEpochDays() const; - acetime_t toUnixDays() const; + int32_t toEpochDays() const; acetime_t toEpochSeconds() const; - acetime_t toUnixSeconds() const; + + int32_t toUnixDays() const; + int32_t toUnixSeconds() const; + int64_t toUnixSeconds64() const; int8_t compareTo(const LocalDateTime& that) const; void printTo(Print& printer) const; @@ -699,7 +730,9 @@ class OffsetDateTime { TimeOffset timeOffset); static OffsetDateTime forEpochSeconds(acetime_t epochSeconds, TimeOffset timeOffset); - static OffsetDateTime forUnixSeconds(acetime_t unixSeconds, + static OffsetDateTime forUnixSeconds(int32_t unixSeconds, + TimeOffset timeOffset); + static OffsetDateTime forUnixSeconds64(int64_t unixSeconds, TimeOffset timeOffset); static OffsetDateTime forDateString(const char* dateString); @@ -732,10 +765,12 @@ class OffsetDateTime { void timeOffset(TimeOffset timeOffset); OffsetDateTime convertToTimeOffset(TimeOffset timeOffset) const; - acetime_t toEpochDays() const; - acetime_t toUnixDays() const; + int32_t toEpochDays() const; acetime_t toEpochSeconds() const; - acetime_t toUnixSeconds() const; + + int32_t toUnixDays() const; + int32_t toUnixSeconds() const; + int64_t toUnixSeconds64() const; int8_t compareTo(const OffsetDateTime& that) const; void printTo(Print& printer) const; @@ -750,7 +785,7 @@ 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)); -acetime_t epochDays = offsetDateTime.toEpochDays(); +int32_t epochDays = offsetDateTime.toEpochDays(); acetime_t epochSeconds = offsetDateTime.toEpochSeconds(); offsetDateTime.printTo(Serial); // prints "2018-01-01 00:00:00+00:15" @@ -1138,6 +1173,9 @@ 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. + +#### ZonedDateTime Declaration + ```C++ namespace ace_time { @@ -1147,12 +1185,13 @@ class ZonedDateTime { 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 forUnixSeconds(acetime_t unixSeconds, + static ZonedDateTime forUnixSeconds(int32_t unixSeconds, const TimeZone& timeZone); + static ZonedDateTime forUnixSeconds64(int64_t unixSeconds, + const TimeZone& timeZone); + static ZonedDateTime forDateString(const char* dateString); explicit ZonedDateTime(); @@ -1183,10 +1222,12 @@ class ZonedDateTime { ZonedDateTime convertToTimeZone(const TimeZone& timeZone) const; - acetime_t toEpochDays() const; - acetime_t toUnixDays() const; + int32_t toEpochDays() const; acetime_t toEpochSeconds() const; - acetime_t toUnixSeconds() const; + + int32_t toUnixDays() const; + int32_t toUnixSeconds() const; + int64_t toUnixSeconds64() const; int8_t compareTo(const ZonedDateTime& that) const; void printTo(Print& printer) const; @@ -1197,37 +1238,58 @@ class ZonedDateTime { } ``` -Here is an example of how to create one and extract the epoch seconds: + +#### ZonedDateTime Creation + +There are 2 main factory methods for constructing this object: + +* `ZonedDateTime::forComponents()` +* `ZonedDateTime::forEpochSeconds()` + +Here is an example of how these can be used: ```C++ -BasicZoneProcessor zoneProcessor; +ExtendedZoneProcessor zoneProcessor; void someFunction() { ... - auto tz = TimeZone::forZoneInfo(&zonedb::kZoneAmerica_Los_Angeles, + auto tz = TimeZone::forZoneInfo( + &zonedbx::kZoneAmerica_Los_Angeles, &zoneProcessor); - // 2018-01-01 00:00:00+00:15 - auto zonedDateTime = ZonedDateTime::forComponents( - 2018, 1, 1, 0, 0, 0, tz); - acetime_t epochDays = zonedDateTime.toEpochDays(); - acetime_t epochSeconds = zonedDateTime.toEpochSeconds(); + // 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(); - zonedDateTime.printTo(Serial); // prints "2018-01-01 00:00:00-08:00" - Serial.println(epochDays); // prints 6574 [TODO: Check] - Serial.println(epochSeconds); // prints 568079100 [TODO: Check] + // Should print "2018-01-02T00:00:00-08:00[America/Los_Angeles]" + zdtPlus1d.printTo(Serial); + Serial.println(); ... } ``` -Both `printTo()` and `forDateString()` are expected to be used only for -debugging. The `printTo()` prints a human-readable representation of the date in +The `printTo()` prints a human-readable representation of the date in [ISO 8601](https://en.wikipedia.org/wiki/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 @@ -1269,14 +1331,15 @@ 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. - -#### Caching + +#### DST Transition Caching The conversion from an epochSeconds to date-time components using -`ZonedDateTime::forEpochSeconds()` is an expensive operation (see -[AutoBenchmark](examples/AutoBenchmark/)). To improve performance, the -`BasicZoneProcessor` and `ExtendedZoneProcessor` implement internal caching -based on the `year` component. This optimizes the most commonly expected +`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](examples/AutoBenchmark/), the cache @@ -1290,13 +1353,15 @@ 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 -may become cumbersome. Also, it is difficult to reconstruct a `TimeZone` +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. It keeps an internal cache of -`ZoneProcessors`, reusing them as needed. And it holds a registry of `ZoneInfo` -objects, so that a `TimeZone` can be created using its `zoneName`, `zoneInfo`, -or `zoneId`. +The `ZoneManager` solves these problems by implementing 2 features: + +1) It supports a registry of `ZoneInfo` objects, so that a `TimeZone` can be + created using its `zoneName` string, `zoneInfo` pointer, or `zoneId` integer. +2) It supports the use of cache of `ZoneProcessors` that can be mapped to + a particular zone as needed. #### Class Hierarchy @@ -1304,7 +1369,7 @@ or `zoneId`. 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 virtual functions were removed. This saves about 1100-1200 +was removed and all functions became nonvirtual. This saves about 1100-1200 bytes of flash on AVR processors. ```C++ @@ -1366,17 +1431,6 @@ class ManualZoneManager { } ``` -The `SIZE` template parameter is the size of the internal cache of -`ZoneProcessor` objects. 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 `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. - #### Default Registries @@ -1394,6 +1448,39 @@ header: * Zones and Links supported by `ExtendedZoneManager` * `ace_time::zonedbx` namespace + + +#### ZoneProcessorCache + +The `BasicZoneManager` and the `ExtendedZoneManager` classes need to be given an +instance of a `BasicZoneProcessorCache` or +`ExtendedZoneProcessorCache` object. + +```C++ +BasicZoneProcessorCache basicZoneProcessorCache; +ExtendedZoneProcessorCache 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. + + +#### ZoneManager Creation + 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 @@ -1402,6 +1489,11 @@ memory consumption of each configuration, as determined by v1.6 with TZDB version 2021a: ```C++ +static const uint8_t CACHE_SIZE = 2; // tuned for application + +BasicZoneProcessorCache basicZoneProcessorCache; +ExtendedZoneProcessorCache extendedZoneProcessorCache; + // BasicZoneManager, Zones only // 266 zones // 21.6 kB (8-bits) @@ -1409,7 +1501,7 @@ v1.6 with TZDB version 2021a: BasicZoneManager zoneManager( zonedb::kZoneRegistrySize, zonedb::kZoneRegistry, - zoneProcessorCache); + basicZoneProcessorCache); // BasicZoneManager, Zones and Fat Links // 266 zones, 183 fat links @@ -1418,7 +1510,7 @@ BasicZoneManager zoneManager( BasicZoneManager zoneManager( zonedb::kZoneAndLinkRegistrySize, zonedb::kZoneAndLinkRegistry, - zoneProcessorCache); + basicZoneProcessorCache); // ExtendedZoneManager, Zones only // 386 Zones @@ -1427,7 +1519,7 @@ BasicZoneManager zoneManager( ExtendedZoneManager zoneManager( zonedbx::kZoneRegistrySize, zonedbx::kZoneRegistry, - zoneProcessorCache); + extendedZoneProcessorCache); // ExtendedZoneManager, Zones and Fat Links // 386 Zones, 207 fat Links @@ -1436,37 +1528,18 @@ ExtendedZoneManager zoneManager( ExtendedZoneManager zoneManager( zonedbx::kZoneAndLinkRegistrySize, zonedbx::kZoneAndLinkRegistry, - zoneProcessorCache); + extendedZoneProcessorCache); ``` A more complicated option of using *thin link* through the `LinkManager` is explained the [Zones and Links](#ZonesAndLinks) section below. Once the `ZoneManager` is configured with the appropriate registries, you can -use one of the `createForXxx()` methods to create a `TimeZone`, like this: - -```C++ -#include -using namespace ace_time; -... -static const uint16_t CACHE_SIZE = 2; -static BasicZoneProcessorCache zoneProcessorCache; -static BasicZoneManager zoneManager( - zonedb::kZoneRegistrySize, zonedb::kZoneRegistry, zoneProcessorCache); - -void someFunction(const char* zoneName) { - TimeZone tz = zoneManager.createForZoneName("America/Los_Angeles"); - if (tz.isError()) { - tz = TimeZone::forUtc(); - ... - } -} -``` - -Other `createForXxx()` methods are described in subsections below. +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](#CustomZoneRegistry) subsection below +[Custom Zone Registry](#CustomZoneRegistry) subsection below. #### createForZoneName() @@ -1475,21 +1548,30 @@ The `ZoneManager` allows creation of a `TimeZone` using the fully qualified zone name: ```C++ -BasicZoneProcessorCache zoneProcessorCache; -BasicZoneManager zoneManager(..., zoneProcessorCache); +ExtendedZoneProcessorCache zoneProcessorCache; +ExtendedZoneManager zoneManager( + zonedbx::kZoneRegistrySize, + zonedbx::kZoneRegistry, + 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 (less memory, less CPU time) using: +could be done more efficiently using the `createForZoneInfo()` like this: ```C++ -BasicZoneProcessorCache zoneProcessorCache; -BasicZoneManager zoneManager(..., zoneProcessorCache); +ExtendedZoneProcessorCache zoneProcessorCache; +ExtendedZoneManager zoneManager( + zonedbx::kZoneRegistrySize, + zonedbx::kZoneRegistry, + zoneProcessorCache); void someFunction() { TimeZone tz = zoneManager.createForZoneInfo(zonedb::kZoneAmerica_Los_Angeles); @@ -1507,14 +1589,14 @@ the user was allowed to type in the zone name, and you wanted to create a 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: -* from the `kZoneId{zone name}` constants in `src/ace_time/zonedb/zone_infos.h` +* the `kZoneId{zone name}` constants in `src/ace_time/zonedb/zone_infos.h` and `src/ace_time/zonedbx/zone_infos.h`: * `const uint32_t kZoneIdAmerica_New_York = 0x1e2a7654; // America/New_York` * `const uint32_t kZoneIdAmerica_Los_Angeles = 0xb7f7e8f2; // America/Los_Angeles` * ... -* from the `TimeZone::getZoneId()` method: +* the `TimeZone::getZoneId()` method: * `uint32_t zoneId = tz.getZoneId();` -* from the `ZoneInfo` pointer using the `BasicZone()` helper object: +* the `ZoneInfo` pointer using the `BasicZone()` helper object: * `uint32_t zoneId = BasicZone(&zonedb::kZoneAmerica_Los_Angeles).zoneId();` * `uint32_t zoneId = ExtendedZone(&zonedbx::kZoneAmerica_Los_Angeles).zoneId();` @@ -1522,19 +1604,17 @@ The `ZoneManager::createForZoneId()` method returns the `TimeZone` object corresponding to the given `zoneId`: ```C++ -BasicZoneProcessorCache zoneProcessorCache; -BasicZoneManager zoneManager(..., zoneProcessorCache); - -void someFunction() { - TimeZone tz = zoneManager.createForZoneId(kZoneIdAmerica_New_York); - ... -} - ExtendedZoneProcessorCache zoneProcessorCache; -ExtendedZoneManager zoneManager(..., zoneProcessorCache); +ExtendedZoneManager zoneManager( + zonedbx::kZoneRegistrySize, + zonedbx::kZoneRegistry, + zoneProcessorCache); void someFunction() { TimeZone tz = zoneManager.createForZoneId(kZoneIdAmerica_New_York); + if (tz.isError() { + // handle error + } ... } ``` @@ -1615,6 +1695,297 @@ 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. + +### Handling Gaps and Overlaps + +(Added in v1.10) + +Better control over DST gaps and overlaps was added in v1.10 using the +techniques described by the [PEP 495](https://www.python.org/dev/peps/pep-0495/) +document in Python 3.6. + +1) An additional parameter called `fold` was added to the `LocalTime`, + `LocalDateTime`, `OffsetDateTime`, and `ZonedDateTime` classes. +2) Support for the `fold` parameter was added to `ExtendedZoneProcessor`. The + `BasicZoneProcessor` does *not* support the `fold` parameter and will ignore + it. + + +#### Problems with Gaps and Overlaps + +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](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/chrono/ChronoLocalDate.html) + * 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()` +* [C++ Hinnant date library](https://howardhinnant.github.io/date/tz.html) + * throws an exception in a gap or overlap if a specifier `choose::earliest` + or `choose::latest` is not specified +* [Noda Time](https://nodatime.org/3.0.x/api/NodaTime.ZonedDateTime.html) + * throws `AmbiguousTimeException` or `SkippedTimeException` + * can specify an `Offset` to `ZonedDateTime` class to resolve ambiguity +* [Python datetime](https://docs.python.org/3/library/datetime.html) + * uses a `fold` parameter to resolve ambiguous or non-existent time + * see [PEP 495](https://www.python.org/dev/peps/pep-0495/) + +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. + + +#### Classes with Fold + +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. + +```C++ +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); + + [...] +}; + +} +``` + + +#### Factory Methods with 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: + +**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 0. +* `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. + +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. + + +#### Semantic Changes with Fold + +The `fold` parameter has no effect on most existing methods. It is ignored in +all comparison operators: + +* `operator==()`, `operator!=()` ignore the `fold` +* `operator<()`, `operator>()`, etc. ignore the `fold` +* `compareTo()` ignores the `fold` + +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. With the `fold` parameter, this round-trip was not guaranteed +during an overlap. + + +#### Resource Consumption with Fold + +According to [MemoryBenchmark](examples/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](examples/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. + + +#### Examples with Fold + +Here are some examples taken from +[ZonedDateTimeExtendedTest](tests/ZonedDateTimeExtendedTest): + +```C++ +ExtendedZoneProcessorCache<1> zoneProcessorCache; +ExtendedZoneManager extendedZoneManager( + zonedbx::kZoneRegistrySize, + zonedbx::kZoneRegistry, + 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(); +``` + ## ZoneInfo Database @@ -1657,8 +2028,8 @@ in the `transformer.py` script and summarized in * the UNTIL time suffix can only be 'w' (not 's' or 'u') * there can be only one DST transition in a single month -In the current version (v1.2), this database contains 268 zones from the year -2000 to 2049 (inclusive). +As of version v1.9 (with TZDB 2021e), this database contains 258 Zone entries +and 193 Link entries, supported from the year 2000 to 2049 (inclusive). #### Extended zonedbx @@ -1668,14 +2039,14 @@ 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 multipole of 15-minutes ranging from -1:00 to 2:45 +* 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 -In the current version (v1.2), this database contains all 387 timezones from -the year 2000 to 2049 (inclusive). +As of version v1.9 (with TZDB 2021e), this database contains all 377 Zone +entries and 217 Link entries, supported from the year 2000 to 2049 (inclusive). #### TZ Database Version @@ -1765,10 +2136,12 @@ 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; }; @@ -1777,14 +2150,33 @@ 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`). @@ -1804,7 +2196,7 @@ 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` class from -the the AceComon library (https://github.com/bxparks/AceCommon) to print the +the AceCommon library (https://github.com/bxparks/AceCommon) to print the zone name into the memory buffer, then extract the string from the buffer: ```C++ @@ -1867,7 +2259,7 @@ There were several negative consequences however: not `US/Pacific` * similarly, the `zoneId` component of a `kZoneUS_Pacific` struct was set to the same `zoneId` of `kZoneAmerica_Los_Angeles` -* the `kZoneIdUS_Pacific` constant did not exist, in constrast to the target +* the `kZoneIdUS_Pacific` constant did not exist, in contrast to the target `KZoneIdAmerica_Los_Angeles` constant * the `zonedb::kZoneRegistry` and `zonedbx::kZoneRegistry` contained only the Zone entries, not the Link entries @@ -2317,7 +2709,7 @@ immutable is definitely cleaner, but it causes the code size to increase significantly. For the case of the [WorldClock](https://github.com/bxparks/clocks/tree/master/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 Ardunio Pro Micro with +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 @@ -2496,6 +2888,35 @@ 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. + +### Invalid Sentinels + +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 +* `acetime_t LocalDate::kInvalidEpochSeconds` + * Error value returned by `toEpochSeconds()` methods +* `int32_t LocalDate::kInvalidUnixDays` + * Error value returned by `toUnixDays()` methods +* `int32_t LocalDate::kInvalidUnixSeconds` + * Error value returned by `toUnixSeconds()` methods +* `int64_t LocalDate::kInvalidUnixSeconds64` + * Error value returned by `toUnixSeconds64()` methods + +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 whoses `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. + ### isError() @@ -2542,19 +2963,6 @@ auto dt = ZonedDateTime::forComponents(1998, 3, 11, 1, 59, 59, tz); Serial.println(dt.isError() ? "true" : "false"); ``` - -### LocalDate::kInvalidEpochSeconds - -Many methods return an `acetime_t` type. For example, `toEpochSeconds()` or -`toUnixSeconds()` on `LocalDateTime`, `OffsetDateTime` or `ZonedDateTime`. -These methods will return `LocalDate::kInvalidEpochSeconds` if an invalid -value is calculated. - -Similarly, there are many methods which accept an `acetime_t` as an argument and -returns a object of time `LocalDateTime`, `OffsetDateTime` or `ZonedDateTime`. -When these methods are passed a value of `LocalDate::kInvalidEpochSeconds`, the -resulting object will return a true value for `isError()`. - ## Bugs and Limitations @@ -2588,11 +2996,17 @@ resulting object will return a true value for `isError()`. * To be safe, users of this library should stay at least 1 year away from the lower and upper limits of `acetime_t` (i.e. stay within the year 1932 to 2067, inclusive). -* `toUnixSeconds()` +* `toUnixSeconds()` and `forUnixSeconds()` * [Unix time](https://en.wikipedia.org/wiki/Unix_time) uses an epoch of - 1970-01-01T00:00:00Z. This method returns an `acetime_t` which is - a signed integer, just like the old 32-bit Unix systems. The range of - dates is 1901-12-13T20:45:52Z to 2038-01-19T03:14:07Z. + 1970-01-01T00:00:00Z. This method returns an `int32_t` like most 32-bit + Unix systems. The range of dates is 1901-12-13T20:45:52Z to + 2038-01-19T03:14:07Z. +* `toUnixSeconds64()` and `forUnixSeconds64()` + * 64-bit versions of `toUnixSeconds()` and `forUnixSeconds()` were added in + v1.10. However, the largest value returned by this method is 3094168447, + far smaller than `INT64_MAX`. This value corresponds to + 2068-01-19T03:14:07Z which is the largest date that can be stored + internally using the 32-bit `acetime_t` type. * `TimeOffset` * Implemented using `int16_t` in 1 minute increments. * `LocalDate`, `LocalDateTime` @@ -2647,32 +3061,12 @@ resulting object will return a true value for `isError()`. range. This is explained in [ZoneInfo Year Range](#ZoneInfoYearRange). * `BasicZoneProcessor` * Supports 1-minute resolution for AT and UNTIL fields. - * Supports only a 15-minute resolution for STDOFF and DST offset fields, - but this is sufficient to support the vast majority of timezones since - 2000. - * The `zonedb/` files have been filtered to satisfy these constraints. - * Tested against Python [pytz](https://pypi.org/project/pytz/) from - 2000 until 2038 (limited by pytz). - * Tested against Python - [dateutil](https://pypi.org/project/python-dateutil/) from - 2000 until 2038 (limited by dateutil). - * Tested against Java `java.time` from 2000 to until 2050. - * Tested against C++11/14/17 - [Hinnant date](https://github.com/HowardHinnant/date) from 2000 until - 2050. + * Supports only a 15-minute resolution for the STDOFF and DST offset fields. + * Sufficient to support the vast majority of timezones since 2000. * `ExtendedZoneProcessor` - * Has a 1-minute resolution for all time fields. - * The `zonedbx/` files currently (version 2021a) do not have any timezones - with 1-minute resolution. - * Tested against Python [pytz](https://pypi.org/project/pytz/) from - 2000 until 2038 (limited by pytz). - * Tested against Python - [dateutil](https://pypi.org/project/python-dateutil/) from - 2000 until 2038 (limited by dateutil). - * Tested against Java `java.time` from 2000 to until 2050. - * Tested against [Hinnant date](https://github.com/HowardHinnant/date) - using 1-minute resolution from 1975 to 2050. See - [ExtendedHinnantDateTest](tests/validation/ExtendedHinnantDateTest). + * 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/` and `zonedbx/` ZoneInfo entries * These statically defined data structures are loaded into flash memory using the `PROGMEM` keyword. The vast majority of the data structure diff --git a/docs/doxygen.cfg b/docs/doxygen.cfg index 80bd87171..b4736ec14 100644 --- a/docs/doxygen.cfg +++ b/docs/doxygen.cfg @@ -38,7 +38,7 @@ PROJECT_NAME = "AceTime" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 1.9.0 +PROJECT_NUMBER = 1.10.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/docs/html/AceTime_8h_source.html b/docs/html/AceTime_8h_source.html index aa5524e6c..359c6f394 100644 --- a/docs/html/AceTime_8h_source.html +++ b/docs/html/AceTime_8h_source.html @@ -22,7 +22,7 @@
AceTime -  1.9.0 +  1.10.0
Date and time classes for Arduino that support timezones from the TZ Database.
@@ -82,59 +82,53 @@
20 // Blacklist boards using new Arduino API due to incompatibilities. This
21 // currently includes all megaAVR boards and SAMD21 boards using arduino::samd
22 // >= 1.8.10. Boards using arduino:samd <= 1.8.9 or SparkFun:samd are fine.
-
23 #if defined(ARDUINO_ARCH_MEGAAVR)
-
24 #error MegaAVR not supported, https://github.com/bxparks/AceTime/issues/44
-
25 
-
26 #elif defined(ARDUINO_ARCH_SAMD) && defined(ARDUINO_API_VERSION)
-
27 #error SAMD21 with arduino:samd >= 1.8.10 not supported, https://github.com/bxparks/AceTime/issues/45
-
28 
-
29 #elif defined(ARDUINO_API_VERSION)
-
30 #error Platforms using ArduinoCore-API not supported
-
31 #endif
-
32 
-
33 #include "ace_time/common/compat.h"
-
34 #include "ace_time/common/common.h"
-
35 #include "ace_time/common/DateStrings.h"
-
36 #include "ace_time/internal/ZoneContext.h"
-
37 #include "ace_time/internal/ZoneInfo.h"
-
38 #include "ace_time/internal/ZonePolicy.h"
-
39 #include "ace_time/internal/ZoneRegistrar.h"
-
40 #include "ace_time/internal/LinkRegistrar.h"
-
41 #include "ace_time/zonedb/zone_policies.h"
-
42 #include "ace_time/zonedb/zone_infos.h"
-
43 #include "ace_time/zonedb/zone_registry.h"
-
44 #include "ace_time/zonedbx/zone_policies.h"
-
45 #include "ace_time/zonedbx/zone_infos.h"
-
46 #include "ace_time/zonedbx/zone_registry.h"
-
47 #include "ace_time/LocalDate.h"
-
48 #include "ace_time/local_date_mutation.h"
-
49 #include "ace_time/LocalTime.h"
-
50 #include "ace_time/LocalDateTime.h"
-
51 #include "ace_time/TimeOffset.h"
- -
53 #include "ace_time/OffsetDateTime.h"
-
54 #include "ace_time/ZoneProcessor.h"
-
55 #include "ace_time/BasicZoneProcessor.h"
-
56 #include "ace_time/ExtendedZoneProcessor.h"
-
57 #include "ace_time/ZoneProcessorCache.h"
-
58 #include "ace_time/ZoneManager.h"
-
59 #include "ace_time/LinkManager.h"
-
60 #include "ace_time/ZoneSorterByName.h"
-
61 #include "ace_time/ZoneSorterByOffsetAndName.h"
-
62 #include "ace_time/TimeZoneData.h"
-
63 #include "ace_time/TimeZone.h"
-
64 #include "ace_time/BasicZone.h"
-
65 #include "ace_time/ExtendedZone.h"
-
66 #include "ace_time/ZonedDateTime.h"
- -
68 #include "ace_time/TimePeriod.h"
- -
70 
-
71 // Version format: xxyyzz == "xx.yy.zz"
-
72 #define ACE_TIME_VERSION 10900
-
73 #define ACE_TIME_VERSION_STRING "1.9.0"
-
74 
-
75 #endif
+
23 #if defined(ARDUINO_API_VERSION)
+
24 #error Platforms using ArduinoCore-API not supported
+
25 #endif
+
26 
+
27 #include "ace_time/common/compat.h"
+
28 #include "ace_time/common/common.h"
+
29 #include "ace_time/common/DateStrings.h"
+
30 #include "ace_time/internal/ZoneContext.h"
+
31 #include "ace_time/internal/ZoneInfo.h"
+
32 #include "ace_time/internal/ZonePolicy.h"
+
33 #include "ace_time/internal/ZoneRegistrar.h"
+
34 #include "ace_time/internal/LinkRegistrar.h"
+
35 #include "ace_time/zonedb/zone_policies.h"
+
36 #include "ace_time/zonedb/zone_infos.h"
+
37 #include "ace_time/zonedb/zone_registry.h"
+
38 #include "ace_time/zonedbx/zone_policies.h"
+
39 #include "ace_time/zonedbx/zone_infos.h"
+
40 #include "ace_time/zonedbx/zone_registry.h"
+
41 #include "ace_time/LocalDate.h"
+
42 #include "ace_time/local_date_mutation.h"
+
43 #include "ace_time/LocalTime.h"
+
44 #include "ace_time/LocalDateTime.h"
+
45 #include "ace_time/TimeOffset.h"
+ +
47 #include "ace_time/OffsetDateTime.h"
+
48 #include "ace_time/ZoneProcessor.h"
+
49 #include "ace_time/BasicZoneProcessor.h"
+
50 #include "ace_time/ExtendedZoneProcessor.h"
+
51 #include "ace_time/ZoneProcessorCache.h"
+
52 #include "ace_time/ZoneManager.h"
+
53 #include "ace_time/LinkManager.h"
+
54 #include "ace_time/ZoneSorterByName.h"
+
55 #include "ace_time/ZoneSorterByOffsetAndName.h"
+
56 #include "ace_time/TimeZoneData.h"
+
57 #include "ace_time/TimeZone.h"
+
58 #include "ace_time/BasicZone.h"
+
59 #include "ace_time/ExtendedZone.h"
+
60 #include "ace_time/ZonedDateTime.h"
+ +
62 #include "ace_time/TimePeriod.h"
+ +
64 
+
65 // Version format: xxyyzz == "xx.yy.zz"
+
66 #define ACE_TIME_VERSION 11000
+
67 #define ACE_TIME_VERSION_STRING "1.10.0"
+
68 
+
69 #endif
diff --git a/docs/html/BasicBrokers_8cpp_source.html b/docs/html/BasicBrokers_8cpp_source.html index cdcb92dba..c039baafa 100644 --- a/docs/html/BasicBrokers_8cpp_source.html +++ b/docs/html/BasicBrokers_8cpp_source.html @@ -22,7 +22,7 @@
AceTime -  1.9.0 +  1.10.0
Date and time classes for Arduino that support timezones from the TZ Database.
diff --git a/docs/html/BasicBrokers_8h.html b/docs/html/BasicBrokers_8h.html index cfe503e96..c1a23ac63 100644 --- a/docs/html/BasicBrokers_8h.html +++ b/docs/html/BasicBrokers_8h.html @@ -22,7 +22,7 @@
AceTime -  1.9.0 +  1.10.0
Date and time classes for Arduino that support timezones from the TZ Database.
diff --git a/docs/html/BasicBrokers_8h_source.html b/docs/html/BasicBrokers_8h_source.html index f21a405ca..938609c6e 100644 --- a/docs/html/BasicBrokers_8h_source.html +++ b/docs/html/BasicBrokers_8h_source.html @@ -22,7 +22,7 @@
AceTime -  1.9.0 +  1.10.0
Date and time classes for Arduino that support timezones from the TZ Database.
@@ -388,101 +388,101 @@
348 
350  void printNameTo(Print& printer) const;
351 
-
353  void printShortNameTo(Print& printer) const;
-
354 
-
355  private:
-
356  const ZoneInfo* mZoneInfo;
-
357 };
-
358 
- -
364  public:
-
365  ZoneRegistryBroker(const ZoneInfo* const* zoneRegistry):
-
366  mZoneRegistry(zoneRegistry) {}
-
367 
-
368  // use default copy constructor
-
369  ZoneRegistryBroker(const ZoneRegistryBroker&) = default;
+
356  void printShortNameTo(Print& printer) const;
+
357 
+
358  private:
+
359  const ZoneInfo* mZoneInfo;
+
360 };
+
361 
+ +
367  public:
+
368  ZoneRegistryBroker(const ZoneInfo* const* zoneRegistry):
+
369  mZoneRegistry(zoneRegistry) {}
370 
-
371  // use default assignment operator
-
372  ZoneRegistryBroker& operator=(const ZoneRegistryBroker&) = default;
+
371  // use default copy constructor
+
372  ZoneRegistryBroker(const ZoneRegistryBroker&) = default;
373 
-
374  #if ACE_TIME_USE_PROGMEM
-
375 
-
376  const ZoneInfo* zoneInfo(uint16_t i) const {
-
377  return (const ZoneInfo*) pgm_read_ptr(&mZoneRegistry[i]);
-
378  }
-
379 
-
380  #else
-
381 
-
382  const ZoneInfo* zoneInfo(uint16_t i) const {
-
383  return mZoneRegistry[i];
-
384  }
-
385 
-
386  #endif
-
387 
-
388  private:
-
389  const ZoneInfo* const* mZoneRegistry;
-
390 };
-
391 
-
392 //-----------------------------------------------------------------------------
-
393 
- -
396  public:
-
397  explicit LinkEntryBroker(const LinkEntry* linkEntry = nullptr):
-
398  mLinkEntry(linkEntry) {}
-
399 
-
400  // use default copy constructor
-
401  LinkEntryBroker(const LinkEntryBroker&) = default;
+
374  // use default assignment operator
+
375  ZoneRegistryBroker& operator=(const ZoneRegistryBroker&) = default;
+
376 
+
377  #if ACE_TIME_USE_PROGMEM
+
378 
+
379  const ZoneInfo* zoneInfo(uint16_t i) const {
+
380  return (const ZoneInfo*) pgm_read_ptr(&mZoneRegistry[i]);
+
381  }
+
382 
+
383  #else
+
384 
+
385  const ZoneInfo* zoneInfo(uint16_t i) const {
+
386  return mZoneRegistry[i];
+
387  }
+
388 
+
389  #endif
+
390 
+
391  private:
+
392  const ZoneInfo* const* mZoneRegistry;
+
393 };
+
394 
+
395 //-----------------------------------------------------------------------------
+
396 
+ +
399  public:
+
400  explicit LinkEntryBroker(const LinkEntry* linkEntry = nullptr):
+
401  mLinkEntry(linkEntry) {}
402 
-
403  // use default assignment operator
-
404  LinkEntryBroker& operator=(const LinkEntryBroker&) = default;
+
403  // use default copy constructor
+
404  LinkEntryBroker(const LinkEntryBroker&) = default;
405 
-
406  #if ACE_TIME_USE_PROGMEM
-
407  uint32_t zoneId() const { return pgm_read_dword(&mLinkEntry->zoneId); }
-
408  uint32_t linkId() const { return pgm_read_dword(&mLinkEntry->linkId); }
-
409 
-
410  #else
-
411  uint32_t zoneId() const { return mLinkEntry->zoneId; }
-
412  uint32_t linkId() const { return mLinkEntry->linkId; }
-
413 
-
414  #endif
-
415 
-
416  private:
-
417  const LinkEntry* mLinkEntry;
-
418 };
-
419 
- -
424  public:
-
425  LinkRegistryBroker(const LinkEntry zoneRegistry[]):
-
426  mLinkRegistry(zoneRegistry) {}
-
427 
-
428  // use default copy constructor
-
429  LinkRegistryBroker(const LinkRegistryBroker&) = default;
+
406  // use default assignment operator
+
407  LinkEntryBroker& operator=(const LinkEntryBroker&) = default;
+
408 
+
409  #if ACE_TIME_USE_PROGMEM
+
410  uint32_t zoneId() const { return pgm_read_dword(&mLinkEntry->zoneId); }
+
411  uint32_t linkId() const { return pgm_read_dword(&mLinkEntry->linkId); }
+
412 
+
413  #else
+
414  uint32_t zoneId() const { return mLinkEntry->zoneId; }
+
415  uint32_t linkId() const { return mLinkEntry->linkId; }
+
416 
+
417  #endif
+
418 
+
419  private:
+
420  const LinkEntry* mLinkEntry;
+
421 };
+
422 
+ +
427  public:
+
428  LinkRegistryBroker(const LinkEntry zoneRegistry[]):
+
429  mLinkRegistry(zoneRegistry) {}
430 
-
431  // use default assignment operator
-
432  LinkRegistryBroker& operator=(const LinkRegistryBroker&) = default;
+
431  // use default copy constructor
+
432  LinkRegistryBroker(const LinkRegistryBroker&) = default;
433 
-
434  // Same code whether or not ACE_TIME_USE_PROGMEM is active.
-
435  const LinkEntry* linkEntry(uint16_t i) const {
-
436  return &mLinkRegistry[i];
-
437  }
-
438 
-
439  private:
-
440  const LinkEntry* mLinkRegistry;
-
441 };
-
442 
-
443 //-----------------------------------------------------------------------------
-
444 
- -
447  public:
-
448  ZoneInfoBroker createZoneInfoBroker(uintptr_t zoneKey) const {
-
449  return ZoneInfoBroker((const ZoneInfo*) zoneKey);
-
450  }
-
451 };
-
452 
-
453 } // basic
-
454 } // ace_time
+
434  // use default assignment operator
+
435  LinkRegistryBroker& operator=(const LinkRegistryBroker&) = default;
+
436 
+
437  // Same code whether or not ACE_TIME_USE_PROGMEM is active.
+
438  const LinkEntry* linkEntry(uint16_t i) const {
+
439  return &mLinkRegistry[i];
+
440  }
+
441 
+
442  private:
+
443  const LinkEntry* mLinkRegistry;
+
444 };
+
445 
+
446 //-----------------------------------------------------------------------------
+
447 
+ +
450  public:
+
451  ZoneInfoBroker createZoneInfoBroker(uintptr_t zoneKey) const {
+
452  return ZoneInfoBroker((const ZoneInfo*) zoneKey);
+
453  }
+
454 };
455 
-
456 #endif
+
456 } // basic
+
457 } // ace_time
+
458 
+
459 #endif
Data broker for accessing ZoneRule.
Definition: BasicBrokers.h:51
@@ -490,11 +490,11 @@
Data broker for accessing ZoneEra.
Definition: BasicBrokers.h:194
Metadata about the zone database.
Definition: ZoneContext.h:16
void printShortNameTo(Print &printer) const
Print a short human-readable identifier (e.g.
-
A factory that creates a basic::ZoneInfoBroker.
Definition: BasicBrokers.h:446
+
A factory that creates a basic::ZoneInfoBroker.
Definition: BasicBrokers.h:449
Data broker for accessing ZoneInfo.
Definition: BasicBrokers.h:286
-
Data broker for accessing the ZoneRegistry.
Definition: BasicBrokers.h:363
-
Data broker for a LinkRegistry composed of LinkEntry records.
Definition: BasicBrokers.h:423
-
Data broker for accessing a LinkEntry.
Definition: BasicBrokers.h:395
+
Data broker for accessing the ZoneRegistry.
Definition: BasicBrokers.h:366
+
Data broker for a LinkRegistry composed of LinkEntry records.
Definition: BasicBrokers.h:426
+
Data broker for accessing a LinkEntry.
Definition: BasicBrokers.h:398
Data broker for accessing ZonePolicy.
Definition: BasicBrokers.h:136