diff --git a/CHANGELOG.md b/CHANGELOG.md index 73b0c4693..c49a2fcb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Changelog * Unreleased +* 0.7 + * Change TimeZoneData to store mStdOffset and mDstOffset in units of + one minute (instead of 15-minute increments, "code") in the off chance + that the library supports timezones with one-minute shifts in the future. + * Implement TimeOffset using 2 bytes (`int16_t`) instead of one byte + (`int8_t`) to give it a resolution of one minute instead of 15 minutes. + * Generate zoneinfo files containing AT and UNTIL timestamps with + one-minute resolution (instead of 15-minute resolution). ZoneInfo files + (`zonedb/` and `zonedbx/`) remain identical in size. Flash memory + consumption usually increases by 130 to 500 bytes, but sometimes decreases + by 50-100 bytes. Timezones whose DST transitions occur at 00:01 + (America/Goose_Bay, America/Moncton, America/St_Johns, Asia/Gaza, + Asia/Hebron) no longer truncate to 00:00. + * Rename `TimeOffset::forHour()` to `forHours()` for consistency with + `forMinutes()`. + * Make `ExtendedZoneProcessor` more memory efficient for 32-bit processors + by packing internal fields to 4-byte boundaries. + * Integrate C++11/14/17 + [Hinnant Date](https://github.com/HowardHinnant/date) library by + creating additional `tests/validation` tests. + * Upgrade `zonedb` and `zonedbx` zoneinfo files to version 2019b, + after validating against the Hinnant date library. + * Upgrade to `pytz` version 2019.2 to pickup TZ Database 2019b. * 0.6.1 * Create a second Jenkins continuous build pipeline file `tests/JenskinfileUnitHost` to use UnitHostDuino to run the unit tests diff --git a/README.md b/README.md index 1229c2791..0c6dc5de4 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ The AceTime classes are organized into roughly 4 bundles, placed in different C++ namespaces: * date and time classes and types - * `ace_time::DateStrings` * `ace_time::acetime_t` + * `ace_time::DateStrings` * `ace_time::LocalTime` * `ace_time::LocalDate` * `ace_time::LocalDateTime` @@ -116,16 +116,22 @@ The library provides 2 sets of zoneinfo files created from the IANA TZ Database: the TZ Database (essentially the entire database) intended to be used with the `ExtendedZoneProcessor` class. -These zoneinfo files (and the `ZoneProcessor` classes which calculate the UTC -offsets and DST transitions) have been validated to match the UTC offsets -calculated using the Python [pytz](https://pypi.org/project/pytz/) library from -the year 2000 until 2037 (inclusive), and using the [Java 11 -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). Custom datasets with smaller or -larger range of years may be generated by developers using scripts provided in -this library (although this is not documented currently). The target application -may be compiled against the custom dataset instead of using `zonedb::` and -`zonedbx::` zone files provided in this library. +These zoneinfo files and the algorithms in this library have been validated to +match the UTC offsets calculated using 3 other date/time libraries: + +* the Python [pytz](https://pypi.org/project/pytz/) library from + the year 2000 until 2037 (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), +* the C++11/14/17 [Hinnant date](https://github.com/HowardHinnant/date) libary + from year 2000 to 2049 (inclusive). + +Custom datasets with smaller or larger range of years may be generated by +developers using scripts provided in this library (although this is not +documented currently). The target application may be compiled against the custom +dataset instead of using `zonedb::` and `zonedbx::` zone files provided in this +library. It is expected that most applications using AceTime will use only a small number of timezones at the same time (1 to 3 zones have been extensively tested) and @@ -198,9 +204,11 @@ Conversion from an epochSeconds to date-time components including timezone * 2.8 microseconds on an ESP32, * 6 microseconds on a Teensy 3.2. -**Version**: 0.6.1 (2019-08-07, TZ DB version 2019a, beta) +**Version**: 0.7 (2019-08-13, TZ DB version 2019b, beta) -**Status**: Stable, no major refactoring planned. Expected to go to 1.0 soon. +**Status**: Upgraded to latest TZ DB version 2019b. Validated against 3 +other timezone libraries (Python, Java, C++). See [CHANGELOG.md](CHANGELOG.md) +for more details. API quite stable now. ## Examples diff --git a/USER_GUIDE.md b/USER_GUIDE.md index b93431210..116eebd17 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2,7 +2,7 @@ See the [README.md](README.md) for introductory background. -Version: 0.6.1 (2019-08-07, TZ DB version 2019a, beta) +Version: 0.7 (2019-08-13, TZ DB version 2019b, beta) ## Installation @@ -67,13 +67,15 @@ usually have more precise dependency information: Various scripts in the `tools/` directory depend on: -* [TZ Database on GitHub](https://github.com/eggert/tz) +* [IANA TZ Database on GitHub](https://github.com/eggert/tz) * [pytz library](https://pypi.org/project/pytz/) +* [Hinnant date library](https://github.com/HowardHinnant/date) * Python 3.5 or greater * Java OpenJDK 11 If you want to run the unit tests or some of the command line examples using a Linux or MacOS machine, you need: + * [UnixHostDuino](https://github.com/bxparks/UnixHostDuino) ### Doxygen Docs @@ -565,22 +567,22 @@ timePeriod.printTo(Serial) ### TimeOffset A `TimeOffset` class represents an amount of time shift from a reference point. -Often the reference is the UTC time and this class represents the amount of time -shift from UTC. Currently (year 2019) every time zone in the world is shifted -from UTC by a multiple of 15 minutes (e.g. -03:30 or +01:00). `TimeOffset` is a -thin wrapper around a single `int8_t` type which can encode integers from -[-128, 127]. Internally -128 is used to indicate an error condition, so we can -represent a UTC shift of from -31:45 to +31:45 hours, which is more than enough -to encode all UTC offsets currently in use in the world. +This is usually used to represent a timezone's standard UTC offset or its DST +offset in the summer. The time resolution of this class changed from 15 minutes +(using a single byte `int8_t` implementation prior to v0.7) to 1 minute (using a +2-byte `int16_t` implementation since v0.7). The range of an `int16_t` is +[-32768, +32767], but -32768 is used to indicate an error condition, so the +actual range is [-32767, +32767] minutes. In practice, the range of values +actually used is probably within [-48, +48] hours, or [-2880, +2800] minutes ```C++ namespace ace_time { class TimeOffset { public: - static TimeOffset forHour(int8_t hour); - static TimeOffset forHourMinute(int8_t hour, int8_t minute); + static TimeOffset forHours(int8_t hour); static TimeOffset forMinutes(int16_t minutes); + static TimeOffset forHourMinute(int8_t hour, int8_t minute); int16_t toMinutes() const; int32_t toSeconds() const; @@ -597,9 +599,9 @@ class TimeOffset { A `TimeOffset` can be created using the factory methods: ```C++ -auto offset = TimeOffset::forHour(-8); // -08:00 -auto offset = TimeOffset::forHourMinute(-2, -30); // -02:30 +auto offset = TimeOffset::forHours(-8); // -08:00 auto offset = TimeOffset::forMinutes(135); // +02:15 +auto offset = TimeOffset::forHourMinute(-2, -30); // -02:30 ``` If the time offset is negative, then both the hour and minute components of @@ -621,7 +623,7 @@ When a method in some class (e.g. `OffsetDateTime` or `ZonedDateTime` below) returns a `TimeOffset`, it is useful to indicate an error condition by returning the special value created by the factory method `TimeOffset::forError()`. This special error marker has the property that `TimeOffset::isError()` returns -`true`. Internally, this is an instance whose internal integer code is -128. +`true`. Internally, this is an instance whose internal integer is -32768. The convenience method `TimeOffset::isZero()` returns `true` if the offset has a zero offset. This is often used to determine if a timezone is currently @@ -862,11 +864,11 @@ To create `TimeZone` instances with other offsets, use the `forTimeOffset()` factory method: ```C++ -auto tz = TimeZone::forTimeOffset(TimeOffset::forHour(-8)); // UTC-08:00 +auto tz = TimeZone::forTimeOffset(TimeOffset::forHours(-8)); // UTC-08:00 auto tz = TimeZone::forTimeOffset(TimeOffset::forHourMinute(-4, -30)); // UTC-04:30 auto tz = TimeZone::forTimeOffset( - TimeOffset::forHour(-8), - TimeOffset::forHour(1)); // UTC-08:00+01:00 (effectively -07:00) + TimeOffset::forHours(-8), + TimeOffset::forHours(1)); // UTC-08:00+01:00 (effectively -07:00) ``` The `TimeZone::isUtc()`, `TimeZone::isDst()` and `TimeZone::setDst(bool)` @@ -938,7 +940,7 @@ void someFunction() { // 2018-03-11T01:59:59-08:00 was still in STD time { auto dt = OffsetDateTime::forComponents(2018, 3, 11, 1, 59, 59, - TimeOffset::forHour(-8)); + TimeOffset::forHours(-8)); acetime_t epochSeconds = dt.toEpochSeconds(); auto offset = tz.getUtcOffset(epochSeconds); // returns -08:00 } @@ -946,7 +948,7 @@ void someFunction() { // one second later, 2018-03-11T02:00:00-08:00 was in DST time { auto dt = OffsetDateTime::forComponents(2018, 3, 11, 2, 0, 0, - TimeOffset::forHour(-8)); + TimeOffset::forHours(-8)); acetime_t epochSeconds = dt.toEpochSeconds(); auto offset = tz.getUtcOffset(epochSeconds); // returns -07:00 } @@ -1000,7 +1002,7 @@ void someFunction() { // 2018-03-11T01:59:59-08:00 was still in STD time { auto dt = OffsetDateTime::forComponents(2018, 3, 11, 1, 59, 59, - TimeOffset::forHour(-8)); + TimeOffset::forHours(-8)); acetime_t epochSeconds = dt.toEpochSeconds(); auto offset = tz.getUtcOffset(epochSeconds); // returns -08:00 } @@ -1008,7 +1010,7 @@ void someFunction() { // one second later, 2018-03-11T02:00:00-08:00 was in DST time { auto dt = OffsetDateTime::forComponents(2018, 3, 11, 2, 0, 0, - TimeOffset::forHour(-8)); + TimeOffset::forHours(-8)); acetime_t epochSeconds = dt.toEpochSeconds(); auto offset = tz.getUtcOffset(epochSeconds); // returns -07:00 } @@ -1245,15 +1247,9 @@ The `zonedb/` files do not support all the timezones in the TZ Database. The list of these zones and The reasons for excluding them are given at the bottom of the [zonedb/zone_infos.h](src/ace_time/zonedb/zone_infos.h) file. -Although the `zonedbx/` files support all zones from its TZ input files, there -are number of timezones whose DST transitions in the past happened at 00:01 -(instead of exactly at midnight 00:00). To save memory, the internal -representation used by AceTime supports transitions only at -15-minute boundaries. For these timezones, the DST transition time is shifted to -00:00 instead, and the transition happens one-minute earlier than it should. As -of TZ DB version 2019a, there are 5 zones affected by this rounding, as listed -at the bottom of [zonedbx/zone_infos.h](src/ace_time/zonedbx/zone_infos.h), and -these all occur before the year 2012. +The goal of the `zonedbx/` files is to support all zones listed in the TZ +Database. Currently, as of TZ Database version 2019b, this goal is met +from the year 2000 to 2049 inclusive. #### BasicZone and ExtendedZone @@ -1303,7 +1299,8 @@ class ExtendedZone { } ``` -They are meant to be used transiently, for example: +The `BasicZone` and `ExtendedZone` objects are meant to be used transiently, +for example: ```C++ ... const basic::ZoneInfo* zoneInfo = ...; @@ -1312,22 +1309,27 @@ Serial.println(BasicZone(zoneInfo).shortName()); ``` The return type of `name()` and `shortName()` change whether or not the zone -name is stored in flash memory or in static memory. The `name()` method returns -the full zone name from the TZ Database (e.g. `"America/Los_Angeles"`). The -`shortName()` method returns only the last component (e.g. `"Los_Angeles"`). +name is stored in flash memory or in static memory. As of v0.4, +`ACE_TIME_USE_PROGMEM=1` for all platforms. On platforms which do not directly +support `PROGMEM`, they provide macros which retain compatibilty with `PROGMEM` +so everything should work transparently. + +The `name()` method returns the full zone name from the TZ Database (e.g. +`"America/Los_Angeles"`). The `shortName()` method returns only the last +component (e.g. `"Los_Angeles"`). ### ZoneManager The `TimeZone::forZoneInfo()` methods are simple to use but have the -disadvantage that the `BasicZoneProcessor` or `ExtendedZoneProcessor` -need 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` dynamically, say, -from its fullly qualified name (e.g. `"America/Los_Angeles"`). The `ZoneManager` -solves these problems. It keeps an internal cache or `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`. +disadvantage that the `BasicZoneProcessor` or `ExtendedZoneProcessor` need 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` +dynamically, say, from its fullly qualified name (e.g. `"America/Los_Angeles"`). +The `ZoneManager` solves these problems. It keeps an internal cache or +`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`. ```C++ namespace ace_time{ @@ -1903,7 +1905,7 @@ void loop() { acetime_t nowSeconds = ntpClock.getNow(); // convert epochSeconds to UTC-08:00 OffsetDateTime odt = OffsetDateTime::forEpochSeconds( - nowSeconds, TimeOffset::forHour(-8)); + nowSeconds, TimeOffset::forHours(-8)); odt.printTo(Serial); delay(10000); // wait 10 seconds } @@ -1971,7 +1973,7 @@ void loop() { acetime_t nowSeconds = dsClock.getNow(); // convert epochSeconds to UTC-08:00 OffsetDateTime odt = OffsetDateTime::forEpochSeconds( - nowSeconds, TimeOffset::forHour(-8)); + nowSeconds, TimeOffset::forHours(-8)); odt.printTo(Serial); delay(10000); // wait 10 seconds } @@ -2238,7 +2240,7 @@ void loop() { acetime_t now = systemClock.getNow(); if (now - prevNow >= 10) { auto odt = OffsetDateTime::forEpochSeconds( - now, TimeOffset::forHour(-8)); // convert epochSeconds to UTC-08:00 + now, TimeOffset::forHours(-8)); // convert epochSeconds to UTC-08:00 odt.printTo(Serial); } } @@ -2291,23 +2293,49 @@ the `ExtendedZoneProcessor` was much larger than the ones supported by timezones. My next idea was to validate AceTime against a known, independently created, -timezone library that also supports the TZ Database. The Python pytz library was -a natural choice since the `tzcompiler.py` was already written in Python. The -`BasicValidationUsingPythonTest` and `ExtendedValidationUsingPythonTest` tests -are the results, where I use `pytz` to determine the list of DST transitions for -all timezones, then determine the expected (year, month, day, hour, minute, -second) components that `ZonedDateTime` should produce. The `tzcompiler.py` -generates a `validation_data.cpp` file which contains the test data points for -all supported timezones. The resulting program no longer fits in any Arduino -microcontroller that I am aware of, but through the use of the -[UnixHostDuino](https://github.com/bxparks/UnixHostDuino) emulation -framework, I can run these large validation test suites on a Linux or Mac -desktop. This worked great until I discovered that `pytz` supports [dates only -until 2038](https://answers.launchpad.net/pytz/+question/262216). That meant -that I could not validate the `ZonedDateTime` classes after 2038. - -I then turned to Java 11 `java.time` library, which supports years through the -[year 1000000000 +timezone library that also supports the TZ Database. Currently, I validate +the AceTime library against 3 other timezone libraries: + +* Python [pytz](https://pypi.org/project/pytz/) +* Java 11 [java.time](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/package-summary.html) +* C++11/14/17 [Hinnant date](https://github.com/HowardHinnant/date) + +When these tests pass, I become confident that AceTime is producing the correct +results, but it is entirely expected that some obscure edge-case bugs will be +found in the future. + +### Python pytz + +The Python pytz library was a natural choice since the `tzcompiler.py` was +already written in Python. I created: + +* [BasicValidationUsingPythonTest](tests/validation/BasicValidationUsingPythonTest/) +* [ExtendedValidationUsingPythonTest](tests/validation/ExtendedValidationUsingPythonTest/) + +The `pytz` library is used to generate various C++ source code +(`validation_data.cpp`, `validation_data.h`, `validation_tests.cpp`) which +contain a list of epochSeconds, the UTC offset, the DST offset, at DST +transition points, for all timezones. The integration test then compiles in the +`ZonedDateTime` and verifies that the expected DST transitions and date +components are identical. + +The resulting data test set contains between 150k to 220k data points, and can +no longer fit in any Arduino microcontroller that I am aware of. They can be +executed only on desktop-class Linux or MacOS machines through the use of the +[UnixHostDuino](https://github.com/bxparks/UnixHostDuino) emulation framework. + +The `pytz` library supports [dates only until +2038](https://answers.launchpad.net/pytz/+question/262216). It is also tricky to +match the `pytz` version to the TZ Database version used by AceTime. The +following combinations have been tested: + +* TZ Datbase: 2019a; pytz: 2019.1 +* TZ Datbase: 2019b; pytz: 2019.2 + +### Java java.time + +The Java 11 `java.time` library is not limited to 2038 but supports years +through the [year 1000000000 (billion)](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/class-use/Instant.html). I wrote the [TestDataGenerator.java](tools/java/TestDataGenerator) program to generate a `validation_data.cpp` file in exactly the same format as the @@ -2315,19 +2343,42 @@ generate a `validation_data.cpp` file in exactly the same format as the which is the exact range of years supported by the `zonedb::` and `zonedbx::` zoneinfo files. -The end result is the 4 validation programs under `tests/validation`: +The result is 2 validation programs under `tests/validation`: + +* [BasicValidationUsingJavaTest](tests/validation/BasicValidationUsingJavaTest/) +* [ExtendedValidationUsingJavaTest](tests/validation/ExtendedValidationUsingJavaTest/) + +The most difficult part of using Java is figuring out how to install it +and figuring out which of the many variants of the JDK to use. On Ubuntu 18.04, +I used `openjdk 11.0.4 2019-07-16` which seems to use TZ Database 2018g. I have +no recollection how I installed, I think it was something like `$ sudo apt +install openjdk-11-jdk:amd64`. + +The underlying timezone database used by the `java.time` package seems to be +locked to the release version of the JDK. I have not been able to figure out a +way to upgrade the timezone database independently (it's something to do with +the +[TZUpdater](https://www.oracle.com/technetwork/java/javase/documentation/tzupdater-readme-136440.html) +but I haven't figured it out.) -* `BasicValidationUsingJavaTest` -* `BasicValidationUsingPythonTest` -* `ExtendedValidationUsingJavaTest` -* `ExtendedValidationUsingPythonTest` +### C++ Hinnant Date -When these tests pass, they show that the timezone algorithms in AceTime produce -the same results as the Python `pytz` library and the Java 11 `java.time` -library, showing that 3 independently written libraries and algorithms agree -with each other. These validation tests give me good confidence that AceTime -produces correct results for the most part, but it is entirely expected that -some obscure edge-case bugs will be found in the future. +I looked for a timezone library that allowed me to control the specific +version of the TZ Database. This led me to the C++11/14/17 [Hinnant +date](https://github.com/HowardHinnant/date) library, which has apparently been +accepted into the C++20 standard. This date and timezone library is incredible +powerful, complex and difficult to use. I managed to incorporate it into 2 more +validation tests, and verified that the AceTime library matches the Hinnant date +library for all timezones from 2000 to 2049 (inclusive): + +* [BasicValidationUsingHinnantDateTest](tests/validation/BasicValidationUsingHinnantDateTest/) +* [ExtendedValidationUsingHinnantDateTest](tests/validation/ExtendedValidationUsingHinnantDateTest/) + +I have validated the AceTime library against the following versions against +the Hinnant date library: + +* TZ Database: 2019a +* TZ Database: 2019b ## Benchmarks @@ -2359,17 +2410,18 @@ classes (more details at [examples/AutoBenchmark](examples/AutoBenchmark): sizeof(LocalDate): 3 sizeof(LocalTime): 3 sizeof(LocalDateTime): 6 -sizeof(TimeOffset): 1 -sizeof(OffsetDateTime): 7 +sizeof(TimeOffset): 2 +sizeof(OffsetDateTime): 8 sizeof(BasicZoneProcessor): 99 -sizeof(ExtendedZoneProcessor): 397 -sizeof(TimeZone): 3 -sizeof(ZonedDateTime): 10 +sizeof(ExtendedZoneProcessor): 437 +sizeof(BasicZoneManager<1>): 107 +sizeof(ExtendedZoneManager<1>): 445 +sizeof(TimeZone): 5 +sizeof(ZonedDateTime): 13 sizeof(TimePeriod): 4 -sizeof(SystemClock): 17 sizeof(DS3231Clock): 3 -sizeof(SystemClockLoop): 14 -sizeof(SystemClockCoroutine): 31 +sizeof(SystemClockLoop): 34 +sizeof(SystemClockCoroutine): 44 ``` **32-bit processors** @@ -2377,17 +2429,18 @@ sizeof(SystemClockCoroutine): 31 sizeof(LocalDate): 3 sizeof(LocalTime): 3 sizeof(LocalDateTime): 6 -sizeof(TimeOffset): 1 -sizeof(OffsetDateTime): 7 +sizeof(TimeOffset): 2 +sizeof(OffsetDateTime): 8 sizeof(BasicZoneProcessor): 136 -sizeof(ExtendedZoneProcessor): 468 -sizeof(TimeZone): 8 -sizeof(ZonedDateTime): 16 +sizeof(ExtendedZoneProcessor): 500 +sizeof(BasicZoneManager<1>): 156 +sizeof(ExtendedZoneManager<1>): 520 +sizeof(TimeZone): 12 +sizeof(ZonedDateTime): 20 sizeof(TimePeriod): 4 -sizeof(SystemClock): 24 sizeof(NtpClock): 88 (ESP8266), 116 (ESP32) -sizeof(SystemClockLoop): 16 -sizeof(SystemClockCoroutine): 48 +sizeof(SystemClockLoop): 44 +sizeof(SystemClockCoroutine): 68 ``` The [MemoryBenchmark](examples/MemoryBenchmark) program gives a more @@ -2502,25 +2555,46 @@ provides other fine-grained classes such as `OffsetTime`, `OffsetDate`, `Year`, providing too many classes. The API of the library is already too large, I did not want to make them larger than necessary. -### HowardHinnant Libraries - -A number of C++ libraries from Howard Hinnant are based the `` standard -library: - -* [date](http://howardhinnant.github.io/date/date.html) -* [tz](http://howardhinnant.github.io/date/tz.html) -* [iso_week](http://howardhinnant.github.io/date/iso_week.html) -* [julian](http://howardhinnant.github.io/date/julian.html) -* [islamic](http://howardhinnant.github.io/date/islamic.html) - -To be honest, I have not looked very closely at these libraries, mostly because -of my suspicion that they are too large to fit into an Arduino microcontroller. +### HowardHinnant Date Library + +The [date](https://github.com/HowardHinnant/date) package by Howard Hinnant is +based upon the `` standard library and consists of several libraries of +which `date.h` and `tz.h` are comparable to AceTime. Modified versions of these +libraries were voted into the C++20 standard. + +Unfortunately these libaries are not suitable for an Arduino microcontroller +environment because: + +* The libraries depend extensively on 64-bit integers which are + impractical on 8-bit microcontrollers with only 32kB of flash memory. +* The `tz.h` library has the option of downloading the TZ Database files over + the network using `libcurl` to the OS filesystem then parsing the files, or + using the native zoneinfo files on the host OS. Neither options are practical + on small microcontrollers. The raw TZ Database files consume about 1MB in + gzip'ed format, which are not suitable for a 32kB Arduino microcontroller. +* The libraries has dependencies on other libraries such as `` and + `` which don't exist on most Arduino platforms. +* The libraries are heavily templatized to provide maximum flexibility + and type-safety. But this makes the libraries incredibly hard to understand + and cumbersome to use for the simple use cases targeted by the AceTime + library. + +The Hinnant date libraries were invaluable for writing the +[BasicValidationUsingHinnantDateTest](tests/validation/BasicValidationUsingHinnantDateTest/) +and +[ExtendedValidationUsingHinnantDateTest](tests/validation/ExtendedValidationUsingHinnantDateTest/) +validation tests (in v0.7) which are the AceTime algorithms to the Hinnant Date +algorithms. For all times zones between the years 2000 until 2050, the AceTime +UTC offsets (`TimeZone::getUtcOffset()`) and epochSecond conversion to +date components (`ZonedDateTime::fromEpochSeconds()`) match the results from the +Hinannt Date libraries perfectly. ### Google cctz The [cctz](https://github.com/google/cctz) library from Google is also based on -the `` library. Again, I did not look at this library closely because I -did not think it would fit inside an Arduino controller. +the `` library. I have not looked at this library closely because I +assumed that it would fit inside an Arduino controller. Hopefully I will get +some time to take a closer look in the future. ## Bugs and Limitations @@ -2545,15 +2619,7 @@ did not think it would fit inside an Arduino controller. 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. * `TimeOffset` - * Implemented using `int8_t` to save memory. - * Represents time offsets in increments of 15 minutes. All timezones after - 2012 are in multiples of 15 minutes. - * Five zones before 2012 have transitions at 00:01 which cannot be - represented by this class. Those transitions have been truncated to 00:00. - See the bottom of the generated - [zonedb/zone_infos.h](src/ace_time/zonedb/zone_infos.h) and - [zonedbx/zone_infos.h](src/ace_time/zonedbx/zone_infos.h) files for the - up-to-date list. + * Implemented using `int16_t` in 1 minute increments. * `LocalDate`, `LocalDateTime` * These classes (and all other Date classes which are based on these) use a single 8-bit signed byte to represent the 'year' internally. This saves @@ -2600,17 +2666,6 @@ did not think it would fit inside an Arduino controller. from 2000 until 2038. * Java `java.time` library has an upper limit far beyond the year 2068 limit of `ZonedDateTime`. Testing was performed from 2000 to until 2050. -* `ExtendedZoneProcessor` - * There are 5 time zones (as of version 2019a of the TZ Database, see - the bottom of `zonedbx/zone_infos.h`) which have DST transitions that - occur at 00:01 (one minute after midnight). This transition cannot be - represented as a multiple of 15-minutes. The transition times of these - zones have been shifted to the nearest 15-minute boundary, in other words, - the transitions occur at 00:00 instead of 00:01. Clocks based on - `ExtendedZoneProcessor` will be off by one hour during the 1-minute - interval from 00:00 and 00:01. - * Fortunately all of these transitions happen before 2012. If you are - interested in only dates after 2019, then this will not affect you. * `NtpClock` * The `NtpClock` on an ESP8266 calls `WiFi.hostByName()` to resolve the IP address of the NTP server. Unfortunately, when I tested this @@ -2627,18 +2682,11 @@ did not think it would fit inside an Arduino controller. [pytz](https://pypi.org/project/pytz/) library. Unfortunately, pytz does not support dates after Unix signed 32-bit `time_t` rollover at (2038-01-19T03:14:07Z). - * These are too big to run on any Arduino controller. They are designed to - run on a Linux or MacOS machine through the Makefiles using the - [UnixHostDuino](https://github.com/bxparks/UnixHostDuino) emulator. * `BasicValidationUsingJavaTest` and `ExtendedValidationUsingJavaTest` * These tests compare the transition times calculated by AceTime to Java 11 `java.time` package which should support the entire range of dates that AceTime can represent. We have artificially limited the range of testing from 2000 to 2050. - * These are too big to run on any Arduino controller. They are designed to - run on a Linux or MacOS machine through the Makefiles using the - [UnixHostDuino](https://github.com/bxparks/UnixHostDuino) - emulator. * `zonedb/` and `zonedbx/` zoneinfo files * 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 9fb747548..14248311b 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 = 0.6.1 +PROJECT_NUMBER = 0.7 # 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 e5cc66e67..d45d16ebf 100644 --- a/docs/html/AceTime_8h_source.html +++ b/docs/html/AceTime_8h_source.html @@ -22,7 +22,7 @@
AceTime -  0.6.1 +  0.7
Date and time classes for Arduino that support timezones from the TZ Database, and a system clock that can synchronize from an NTP server or an RTC chip.
@@ -68,7 +68,7 @@
AceTime.h
-
1 /*
2  * MIT License
3  * Copyright (c) 2018 Brian T. Park
4  */
5 
13 #ifndef ACE_TIME_ACE_TIME_H
14 #define ACE_TIME_ACE_TIME_H
15 
16 #include "ace_time/common/compat.h"
17 #include "ace_time/common/common.h"
18 #include "ace_time/common/DateStrings.h"
19 #include "ace_time/internal/ZoneContext.h"
20 #include "ace_time/internal/ZoneInfo.h"
21 #include "ace_time/internal/ZonePolicy.h"
22 #include "ace_time/zonedb/zone_policies.h"
23 #include "ace_time/zonedb/zone_infos.h"
24 #include "ace_time/zonedb/zone_registry.h"
25 #include "ace_time/zonedbx/zone_policies.h"
26 #include "ace_time/zonedbx/zone_infos.h"
27 #include "ace_time/zonedbx/zone_registry.h"
28 #include "ace_time/ZoneRegistrar.h"
29 #include "ace_time/LocalDate.h"
30 #include "ace_time/local_date_mutation.h"
31 #include "ace_time/LocalTime.h"
32 #include "ace_time/LocalDateTime.h"
33 #include "ace_time/TimeOffset.h"
34 #include "ace_time/time_offset_mutation.h"
35 #include "ace_time/OffsetDateTime.h"
36 #include "ace_time/ZoneProcessor.h"
37 #include "ace_time/BasicZoneProcessor.h"
38 #include "ace_time/ExtendedZoneProcessor.h"
39 #include "ace_time/ZoneProcessorCache.h"
40 #include "ace_time/ZoneManager.h"
41 #include "ace_time/TimeZoneData.h"
42 #include "ace_time/TimeZone.h"
43 #include "ace_time/BasicZone.h"
44 #include "ace_time/ExtendedZone.h"
45 #include "ace_time/ZonedDateTime.h"
46 #include "ace_time/zoned_date_time_mutation.h"
47 #include "ace_time/TimePeriod.h"
48 #include "ace_time/time_period_mutation.h"
49 #include "ace_time/clock/Clock.h"
50 #include "ace_time/clock/NtpClock.h"
51 #include "ace_time/clock/DS3231Clock.h"
52 #include "ace_time/clock/UnixClock.h"
53 #include "ace_time/clock/SystemClock.h"
54 #include "ace_time/clock/SystemClockLoop.h"
55 #include "ace_time/clock/SystemClockCoroutine.h"
56 
57 // Version format: xxyyzz == "xx.yy.zz"
58 #define ACE_TIME_VERSION 601
59 #define ACE_TIME_VERSION_STRING "0.6.1"
60 
61 #endif
Macros and definitions that provide a consistency layer among the various Arduino boards for compatib...
+
1 /*
2  * MIT License
3  * Copyright (c) 2018 Brian T. Park
4  */
5 
13 #ifndef ACE_TIME_ACE_TIME_H
14 #define ACE_TIME_ACE_TIME_H
15 
16 #include "ace_time/common/compat.h"
17 #include "ace_time/common/common.h"
18 #include "ace_time/common/DateStrings.h"
19 #include "ace_time/internal/ZoneContext.h"
20 #include "ace_time/internal/ZoneInfo.h"
21 #include "ace_time/internal/ZonePolicy.h"
22 #include "ace_time/zonedb/zone_policies.h"
23 #include "ace_time/zonedb/zone_infos.h"
24 #include "ace_time/zonedb/zone_registry.h"
25 #include "ace_time/zonedbx/zone_policies.h"
26 #include "ace_time/zonedbx/zone_infos.h"
27 #include "ace_time/zonedbx/zone_registry.h"
28 #include "ace_time/ZoneRegistrar.h"
29 #include "ace_time/LocalDate.h"
30 #include "ace_time/local_date_mutation.h"
31 #include "ace_time/LocalTime.h"
32 #include "ace_time/LocalDateTime.h"
33 #include "ace_time/TimeOffset.h"
34 #include "ace_time/time_offset_mutation.h"
35 #include "ace_time/OffsetDateTime.h"
36 #include "ace_time/ZoneProcessor.h"
37 #include "ace_time/BasicZoneProcessor.h"
38 #include "ace_time/ExtendedZoneProcessor.h"
39 #include "ace_time/ZoneProcessorCache.h"
40 #include "ace_time/ZoneManager.h"
41 #include "ace_time/TimeZoneData.h"
42 #include "ace_time/TimeZone.h"
43 #include "ace_time/BasicZone.h"
44 #include "ace_time/ExtendedZone.h"
45 #include "ace_time/ZonedDateTime.h"
46 #include "ace_time/zoned_date_time_mutation.h"
47 #include "ace_time/TimePeriod.h"
48 #include "ace_time/time_period_mutation.h"
49 #include "ace_time/clock/Clock.h"
50 #include "ace_time/clock/NtpClock.h"
51 #include "ace_time/clock/DS3231Clock.h"
52 #include "ace_time/clock/UnixClock.h"
53 #include "ace_time/clock/SystemClock.h"
54 #include "ace_time/clock/SystemClockLoop.h"
55 #include "ace_time/clock/SystemClockCoroutine.h"
56 
57 // Version format: xxyyzz == "xx.yy.zz"
58 #define ACE_TIME_VERSION 700
59 #define ACE_TIME_VERSION_STRING "0.7"
60 
61 #endif
Macros and definitions that provide a consistency layer among the various Arduino boards for compatib...
-
1 /*
2  * MIT License
3  * Copyright (c) 2019 Brian T. Park
4  */
5 
6 #ifndef ACE_TIME_BASIC_ZONE_PROCESSOR_H
7 #define ACE_TIME_BASIC_ZONE_PROCESSOR_H
8 
9 #include <string.h> // strchr()
10 #include <stdint.h>
11 #include "internal/ZonePolicy.h"
12 #include "internal/ZoneInfo.h"
13 #include "internal/Brokers.h"
14 #include "common/logging.h"
15 #include "TimeOffset.h"
16 #include "LocalDate.h"
17 #include "OffsetDateTime.h"
18 #include "ZoneProcessor.h"
19 
20 #define ACE_TIME_BASIC_ZONE_PROCESSOR_DEBUG 0
21 
22 class BasicZoneProcessorTest_init_primitives;
23 class BasicZoneProcessorTest_init;
24 class BasicZoneProcessorTest_setZoneInfo;
25 class BasicZoneProcessorTest_createAbbreviation;
26 class BasicZoneProcessorTest_calcStartDayOfMonth;
27 class BasicZoneProcessorTest_calcRuleOffsetCode;
28 
29 namespace ace_time {
30 
31 template<uint8_t SIZE, uint8_t TYPE, typename ZS, typename ZI, typename ZIB>
33 
34 namespace basic {
35 
51 struct Transition {
59  static const uint8_t kAbbrevSize = 6 + 1;
60 
67 
79 
81  acetime_t startEpochSeconds;
82 
84  int8_t yearTiny;
85 
91  int8_t offsetCode;
92 
94  int8_t deltaCode;
95 
104 
106  void log() const {
107  if (ACE_TIME_BASIC_ZONE_PROCESSOR_DEBUG) {
108  if (sizeof(acetime_t) == sizeof(int)) {
109  logging::printf("startEpochSeconds: %d\n", startEpochSeconds);
110  } else {
111  logging::printf("startEpochSeconds: %ld\n", startEpochSeconds);
112  }
113  logging::printf("offsetCode: %d\n", offsetCode);
114  logging::printf("abbrev: %s\n", abbrev);
115  if (rule.isNotNull()) {
116  logging::printf("Rule.fromYear: %d\n", rule.fromYearTiny());
117  logging::printf("Rule.toYear: %d\n", rule.toYearTiny());
118  logging::printf("Rule.inMonth: %d\n", rule.inMonth());
119  logging::printf("Rule.onDayOfMonth: %d\n", rule.onDayOfMonth());
120  }
121  }
122  }
123 };
124 
126 struct MonthDay {
127  uint8_t month;
128  uint8_t day;
129 };
130 
131 } // namespace basic
132 
185  public:
190  explicit BasicZoneProcessor(const basic::ZoneInfo* zoneInfo = nullptr):
191  ZoneProcessor(kTypeBasic),
192  mZoneInfo(zoneInfo) {}
193 
195  const void* getZoneInfo() const override {
196  return mZoneInfo.zoneInfo();
197  }
198 
199  uint32_t getZoneId() const override { return mZoneInfo.zoneId(); }
200 
201  TimeOffset getUtcOffset(acetime_t epochSeconds) const override {
202  const basic::Transition* transition = getTransition(epochSeconds);
203  int8_t code = (transition)
204  ? transition->offsetCode : TimeOffset::kErrorCode;
205  return TimeOffset::forOffsetCode(code);
206  }
207 
208  TimeOffset getDeltaOffset(acetime_t epochSeconds) const override {
209  const basic::Transition* transition = getTransition(epochSeconds);
210  int8_t code = (transition)
211  ? transition->deltaCode : TimeOffset::kErrorCode;
212  return TimeOffset::forOffsetCode(code);
213  }
214 
215  const char* getAbbrev(acetime_t epochSeconds) const override {
216  const basic::Transition* transition = getTransition(epochSeconds);
217  return (transition) ? transition->abbrev : "";
218  }
219 
249  OffsetDateTime getOffsetDateTime(const LocalDateTime& ldt) const override {
250  // Only a single local variable of OffsetDateTime used, to allow Return
251  // Value Optimization (and save 20 bytes of flash for WorldClock).
252  OffsetDateTime odt;
253  bool success = init(ldt.localDate());
254  if (success) {
255  // 0) Use the UTC epochSeconds to get intial guess of offset.
256  acetime_t epochSeconds0 = ldt.toEpochSeconds();
257  auto offset0 = getUtcOffset(epochSeconds0);
258 
259  // 1) Use offset0 to get the next epochSeconds and offset.
260  odt = OffsetDateTime::forLocalDateTimeAndOffset(ldt, offset0);
261  acetime_t epochSeconds1 = odt.toEpochSeconds();
262  auto offset1 = getUtcOffset(epochSeconds1);
263 
264  // 2) Use offset1 to get the next epochSeconds and offset.
265  odt = OffsetDateTime::forLocalDateTimeAndOffset(ldt, offset1);
266  acetime_t epochSeconds2 = odt.toEpochSeconds();
267  auto offset2 = getUtcOffset(epochSeconds2);
268 
269  // If offset1 and offset2 are equal, then we have an equilibrium
270  // and odt(1) must equal odt(2), so we can just return the last odt.
271  if (offset1.toOffsetCode() == offset2.toOffsetCode()) {
272  // pass
273  } else {
274  // Pick the later epochSeconds and offset
275  acetime_t epochSeconds;
276  TimeOffset offset;
277  if (epochSeconds1 > epochSeconds2) {
278  epochSeconds = epochSeconds1;
279  offset = offset1;
280  } else {
281  epochSeconds = epochSeconds2;
282  offset = offset2;
283  }
284  odt = OffsetDateTime::forEpochSeconds(epochSeconds, offset);
285  }
286  } else {
287  odt = OffsetDateTime::forError();
288  }
289 
290  return odt;
291  }
292 
293  void printTo(Print& printer) const override;
294 
295  void printShortTo(Print& printer) const override;
296 
298  void log() const {
299  if (ACE_TIME_BASIC_ZONE_PROCESSOR_DEBUG) {
300  if (!mIsFilled) {
301  logging::printf("*not initialized*\n");
302  return;
303  }
304  logging::printf("mYear: %d\n", mYear);
305  logging::printf("mNumTransitions: %d\n", mNumTransitions);
306  logging::printf("---- PrevTransition\n");
307  mPrevTransition.log();
308  for (int i = 0; i < mNumTransitions; i++) {
309  logging::printf("---- Transition: %d\n", i);
310  mTransitions[i].log();
311  }
312  }
313  }
314 
333  static basic::MonthDay calcStartDayOfMonth(int16_t year, uint8_t month,
334  uint8_t onDayOfWeek, int8_t onDayOfMonth) {
335  if (onDayOfWeek == 0) return {month, (uint8_t) onDayOfMonth};
336 
337  if (onDayOfMonth >= 0) {
338  // Convert "last{Xxx}" to "last{Xxx}>={daysInMonth-6}".
339  uint8_t daysInMonth = LocalDate::daysInMonth(year, month);
340  if (onDayOfMonth == 0) {
341  onDayOfMonth = daysInMonth - 6;
342  }
343 
344  auto limitDate = LocalDate::forComponents(year, month, onDayOfMonth);
345  uint8_t dayOfWeekShift = (onDayOfWeek - limitDate.dayOfWeek() + 7) % 7;
346  uint8_t day = (uint8_t) (onDayOfMonth + dayOfWeekShift);
347  if (day > daysInMonth) {
348  // TODO: Support shifting from Dec to Jan of following year.
349  day -= daysInMonth;
350  month++;
351  }
352  return {month, day};
353  } else {
354  onDayOfMonth = -onDayOfMonth;
355  auto limitDate = LocalDate::forComponents(year, month, onDayOfMonth);
356  int8_t dayOfWeekShift = (limitDate.dayOfWeek() - onDayOfWeek + 7) % 7;
357  int8_t day = onDayOfMonth - dayOfWeekShift;
358  if (day < 1) {
359  // TODO: Support shifting from Jan to Dec of the previous year.
360  month--;
361  uint8_t daysInPrevMonth = LocalDate::daysInMonth(year, month);
362  day += daysInPrevMonth;
363  }
364  return {month, (uint8_t) day};
365  }
366  }
367 
368  private:
369  friend class ::BasicZoneProcessorTest_init_primitives;
370  friend class ::BasicZoneProcessorTest_init;
371  friend class ::BasicZoneProcessorTest_setZoneInfo;
372  friend class ::BasicZoneProcessorTest_createAbbreviation;
373  friend class ::BasicZoneProcessorTest_calcStartDayOfMonth;
374  friend class ::BasicZoneProcessorTest_calcRuleOffsetCode;
375 
376  template<uint8_t SIZE, uint8_t TYPE, typename ZS, typename ZI, typename ZIB>
377  friend class ZoneProcessorCacheImpl; // setZoneInfo()
378 
380  static const uint8_t kMaxCacheEntries = 4;
381 
387  static const acetime_t kMinEpochSeconds = INT32_MIN + 1;
388 
389  // Disable copy constructor and assignment operator.
390  BasicZoneProcessor(const BasicZoneProcessor&) = delete;
391  BasicZoneProcessor& operator=(const BasicZoneProcessor&) = delete;
392 
393  bool equals(const ZoneProcessor& other) const override {
394  const auto& that = (const BasicZoneProcessor&) other;
395  return getZoneInfo() == that.getZoneInfo();
396  }
397 
399  void setZoneInfo(const void* zoneInfo) override {
400  if (mZoneInfo.zoneInfo() == zoneInfo) return;
401 
402  mZoneInfo = basic::ZoneInfoBroker((const basic::ZoneInfo*) zoneInfo);
403  mYear = 0;
404  mIsFilled = false;
405  mNumTransitions = 0;
406  }
407 
409  const basic::Transition* getTransition(acetime_t epochSeconds) const {
410  LocalDate ld = LocalDate::forEpochSeconds(epochSeconds);
411  bool success = init(ld);
412  return (success) ? findMatch(epochSeconds) : nullptr;
413  }
414 
443  bool init(const LocalDate& ld) const {
444  int16_t year = ld.year();
445  if (ld.month() == 1 && ld.day() == 1) {
446  year--;
447  }
448  if (isFilled(year)) return true;
449 
450  mYear = year;
451  mNumTransitions = 0; // clear cache
452 
453  if (year < mZoneInfo.startYear() - 1 || mZoneInfo.untilYear() < year) {
454  return false;
455  }
456 
457  addRulePriorToYear(year);
458  addRulesForYear(year);
459  calcTransitions();
460  calcAbbreviations();
461 
462  mIsFilled = true;
463  return true;
464  }
465 
467  bool isFilled(int16_t year) const {
468  return mIsFilled && (year == mYear);
469  }
470 
475  void addRulePriorToYear(int16_t year) const {
476  int8_t yearTiny = year - LocalDate::kEpochYear;
477  int8_t priorYearTiny = yearTiny - 1;
478 
479  // Find the prior Era.
480  const basic::ZoneEraBroker era = findZoneEraPriorTo(year);
481 
482  // If the prior ZoneEra is a simple Era (no zone policy), then create a
483  // Transition using a rule==nullptr. Otherwise, find the latest rule
484  // within the ZoneEra.
485  const basic::ZonePolicyBroker zonePolicy = era.zonePolicy();
486  basic::ZoneRuleBroker latest;
487  if (zonePolicy.isNotNull()) {
488  // Find the latest rule for the matching ZoneEra whose
489  // ZoneRule::toYearTiny < yearTiny. Assume that there are no more than
490  // 1 rule per month.
491  uint8_t numRules = zonePolicy.numRules();
492  for (uint8_t i = 0; i < numRules; i++) {
493  const basic::ZoneRuleBroker rule = zonePolicy.rule(i);
494  // Check if rule is effective prior to the given year
495  if (rule.fromYearTiny() < yearTiny) {
496  if ((latest.isNull()) || compareZoneRule(year, rule, latest) > 0) {
497  latest = rule;
498  }
499  }
500  }
501  }
502 
503  mPrevTransition = createTransition(era, latest, priorYearTiny);
504  }
505 
511  static basic::Transition createTransition(basic::ZoneEraBroker era,
513  int8_t offsetCode;
514  int8_t deltaCode;
515  char letter;
516  if (rule.isNull()) {
517  deltaCode = 0;
518  offsetCode = era.offsetCode();
519  letter = 0;
520  } else {
521  deltaCode = rule.deltaCode();
522  offsetCode = era.offsetCode() + deltaCode;
523  letter = rule.letter();
524  }
525 
526  return {
527  era,
528  rule,
529  0 /*epochSeconds*/,
530  yearTiny,
531  offsetCode,
532  deltaCode,
533  {letter} /*abbrev*/
534  };
535  }
536 
538  static int8_t compareZoneRule(int16_t year,
539  const basic::ZoneRuleBroker a, const basic::ZoneRuleBroker b) {
540  int16_t aYear = effectiveRuleYear(year, a);
541  int16_t bYear = effectiveRuleYear(year, b);
542  if (aYear < bYear) return -1;
543  if (aYear > bYear) return 1;
544  if (a.inMonth() < b.inMonth()) return -1;
545  if (a.inMonth() > b.inMonth()) return 1;
546  return 0;
547  }
548 
553  static int16_t effectiveRuleYear(int16_t year,
554  const basic::ZoneRuleBroker rule) {
555  int8_t yearTiny = year - LocalDate::kEpochYear;
556  if (rule.toYearTiny() < yearTiny) {
557  return rule.toYearTiny() + LocalDate::kEpochYear;
558  }
559  if (rule.fromYearTiny() < yearTiny) {
560  return year - 1;
561  }
562  return 0;
563  }
564 
566  void addRulesForYear(int16_t year) const {
567  const basic::ZoneEraBroker era = findZoneEra(year);
568 
569  // If the ZonePolicy has no rules, then add a Transition which takes
570  // effect at the start time of the current year.
571  const basic::ZonePolicyBroker zonePolicy = era.zonePolicy();
572  if (zonePolicy.isNull()) {
573  addRule(year, era, basic::ZoneRuleBroker());
574  return;
575  }
576 
577  // If the ZonePolicy has rules, find all matching transitions, and add
578  // them to mTransitions, in sorted order according to the
579  // ZoneRule::inMonth field.
580  int8_t yearTiny = year - LocalDate::kEpochYear;
581  uint8_t numRules = zonePolicy.numRules();
582  for (uint8_t i = 0; i < numRules; i++) {
583  const basic::ZoneRuleBroker rule = zonePolicy.rule(i);
584  if ((rule.fromYearTiny() <= yearTiny) &&
585  (yearTiny <= rule.toYearTiny())) {
586  addRule(year, era, rule);
587  }
588  }
589  }
590 
605  void addRule(int16_t year, basic::ZoneEraBroker era,
606  basic::ZoneRuleBroker rule) const {
607 
608  // If a zone needs more transitions than kMaxCacheEntries, the check below
609  // will cause the DST transition information to be inaccurate, and it is
610  // highly likely that this situation would be caught in the
611  // BasicValidationUsingPython or BasicValidationUsingJava unit tests.
612  // Since these unit tests pass, I feel confident that those zones which
613  // need more than kMaxCacheEntries are already filtered out by
614  // tzcompiler.py.
615  //
616  // Ideally, the tzcompiler.py script would explicitly remove those zones
617  // which need more than kMaxCacheEntries Transitions. But this would
618  // require a Python version of the BasicZoneProcessor, and unfortunately,
619  // zone_specifier.py implements only the ExtendedZoneProcessor algorithm
620  // An early version of zone_specifier.py may have implemented something
621  // close to BasicZoneProcessor, and it may be available in the git
622  // history. But it seems like too much work right now to try to dig that
623  // out, just to implement the explicit check for kMaxCacheEntries. It
624  // would mean maintaining another version of zone_specifier.py.
625  if (mNumTransitions >= kMaxCacheEntries) return;
626 
627  // insert new element at the end of the list
628  int8_t yearTiny = year - LocalDate::kEpochYear;
629  mTransitions[mNumTransitions] = createTransition(era, rule, yearTiny);
630  mNumTransitions++;
631 
632  // perform an insertion sort
633  for (uint8_t i = mNumTransitions - 1; i > 0; i--) {
634  basic::Transition& left = mTransitions[i - 1];
635  basic::Transition& right = mTransitions[i];
636  // assume only 1 rule per month
637  if ((left.rule.isNotNull() && right.rule.isNotNull() &&
638  left.rule.inMonth() > right.rule.inMonth())
639  || (left.rule.isNotNull() && right.rule.isNull())) {
640  basic::Transition tmp = left;
641  left = right;
642  right = tmp;
643  }
644  }
645  }
646 
652  const basic::ZoneEraBroker findZoneEra(int16_t year) const {
653  for (uint8_t i = 0; i < mZoneInfo.numEras(); i++) {
654  const basic::ZoneEraBroker era = mZoneInfo.era(i);
655  if (year < era.untilYearTiny() + LocalDate::kEpochYear) return era;
656  }
657  // Return the last ZoneEra if we run off the end.
658  return mZoneInfo.era(mZoneInfo.numEras() - 1);
659  }
660 
672  const basic::ZoneEraBroker findZoneEraPriorTo(int16_t year) const {
673  for (uint8_t i = 0; i < mZoneInfo.numEras(); i++) {
674  const basic::ZoneEraBroker era = mZoneInfo.era(i);
675  if (year <= era.untilYearTiny() + LocalDate::kEpochYear) return era;
676  }
677  // Return the last ZoneEra if we run off the end.
678  return mZoneInfo.era(mZoneInfo.numEras() - 1);
679  }
680 
688  void calcTransitions() const {
689  // Set the initial startEpochSeconds to be -Infinity
690  mPrevTransition.startEpochSeconds = kMinEpochSeconds;
691  const basic::Transition* prevTransition = &mPrevTransition;
692 
693  for (uint8_t i = 0; i < mNumTransitions; i++) {
694  basic::Transition& transition = mTransitions[i];
695  const int16_t year = transition.yearTiny + LocalDate::kEpochYear;
696 
697  if (transition.rule.isNull()) {
698  // If the transition is simple (has no named rule), then the
699  // ZoneEra applies for the entire year (since BasicZoneProcessor
700  // supports only whole year in the UNTIL field). The whole year UNTIL
701  // field has an implied 'w' modifier on 00:00, we don't need to call
702  // calcRuleOffsetCode() with a 'w', we can just use the previous
703  // transition's offset to calculate the startDateTime of this
704  // transition.
705  //
706  // Also, when transition.rule == nullptr, the mNumTransitions should
707  // be 1, since only a single transition is added by
708  // addRulesForYear().
709  const int8_t prevOffsetCode = prevTransition->offsetCode;
711  year, 1, 1, 0, 0, 0,
712  TimeOffset::forOffsetCode(prevOffsetCode));
713  transition.startEpochSeconds = startDateTime.toEpochSeconds();
714  } else {
715  // In this case, the transition points to a named ZonePolicy, which
716  // means that there could be multiple ZoneRules associated with the
717  // given year. For each transition, determine the startEpochSeconds,
718  // and the effective offset code.
719 
720  // Determine the start date of the rule.
721  const basic::MonthDay monthDay = calcStartDayOfMonth(
722  year, transition.rule.inMonth(), transition.rule.onDayOfWeek(),
723  transition.rule.onDayOfMonth());
724 
725  // Determine the offset of the 'atTimeModifier'. The 'w' modifier
726  // requires the offset of the previous transition.
727  const int8_t prevOffsetCode = calcRuleOffsetCode(
728  prevTransition->offsetCode,
729  transition.era.offsetCode(),
730  transition.rule.atTimeModifier());
731 
732  // startDateTime
733  const uint8_t timeCode = transition.rule.atTimeCode();
734  const uint8_t atHour = timeCode / 4;
735  const uint8_t atMinute = (timeCode % 4) * 15;
737  year, monthDay.month, monthDay.day,
738  atHour, atMinute, 0 /*second*/,
739  TimeOffset::forOffsetCode(prevOffsetCode));
740  transition.startEpochSeconds = startDateTime.toEpochSeconds();
741  }
742 
743  prevTransition = &transition;
744  }
745  }
746 
753  static int8_t calcRuleOffsetCode(int8_t prevEffectiveOffsetCode,
754  int8_t currentBaseOffsetCode, uint8_t atModifier) {
755  if (atModifier == 'w') {
756  return prevEffectiveOffsetCode;
757  } else if (atModifier == 's') {
758  return currentBaseOffsetCode;
759  } else { // 'u', 'g' or 'z'
760  return 0;
761  }
762  }
763 
765  void calcAbbreviations() const {
766  calcAbbreviation(&mPrevTransition);
767  for (uint8_t i = 0; i < mNumTransitions; i++) {
768  calcAbbreviation(&mTransitions[i]);
769  }
770  }
771 
773  static void calcAbbreviation(basic::Transition* transition) {
774  createAbbreviation(
775  transition->abbrev,
777  transition->era.format(),
778  transition->deltaCode,
779  transition->abbrev[0]);
780  }
781 
817  static void createAbbreviation(char* dest, uint8_t destSize,
818  const char* format, uint8_t deltaCode, char letter) {
819  // Check if RULES column empty.
820  if (deltaCode == 0 && letter == '\0') {
821  strncpy(dest, format, destSize);
822  dest[destSize - 1] = '\0';
823  return;
824  }
825 
826  // Check if FORMAT contains a '%'.
827  if (strchr(format, '%') != nullptr) {
828  copyAndReplace(dest, destSize, format, '%', letter);
829  } else {
830  // Check if FORMAT contains a '/'.
831  const char* slashPos = strchr(format, '/');
832  if (slashPos != nullptr) {
833  if (deltaCode == 0) {
834  uint8_t headLength = (slashPos - format);
835  if (headLength >= destSize) headLength = destSize - 1;
836  memcpy(dest, format, headLength);
837  dest[headLength] = '\0';
838  } else {
839  uint8_t tailLength = strlen(slashPos+1);
840  if (tailLength >= destSize) tailLength = destSize - 1;
841  memcpy(dest, slashPos+1, tailLength);
842  dest[tailLength] = '\0';
843  }
844  } else {
845  // Just copy the FORMAT disregarding the deltaCode and letter.
846  strncpy(dest, format, destSize);
847  dest[destSize - 1] = '\0';
848  }
849  }
850  }
851 
857  static void copyAndReplace(char* dst, uint8_t dstSize, const char* src,
858  char oldChar, char newChar) {
859  while (*src != '\0' && dstSize > 0) {
860  if (*src == oldChar) {
861  if (newChar == '-') {
862  src++;
863  } else {
864  *dst = newChar;
865  dst++;
866  src++;
867  dstSize--;
868  }
869  } else {
870  *dst++ = *src++;
871  dstSize--;
872  }
873  }
874 
875  if (dstSize == 0) {
876  --dst;
877  }
878  *dst = '\0';
879  }
880 
882  const basic::Transition* findMatch(acetime_t epochSeconds) const {
883  const basic::Transition* closestMatch = &mPrevTransition;
884  for (uint8_t i = 0; i < mNumTransitions; i++) {
885  const basic::Transition* m = &mTransitions[i];
886  if (m->startEpochSeconds <= epochSeconds) {
887  closestMatch = m;
888  }
889  }
890  return closestMatch;
891  }
892 
893  basic::ZoneInfoBroker mZoneInfo;
894 
895  mutable int16_t mYear = 0; // maybe create LocalDate::kInvalidYear?
896  mutable bool mIsFilled = false;
897  mutable uint8_t mNumTransitions = 0;
898  mutable basic::Transition mTransitions[kMaxCacheEntries];
899  mutable basic::Transition mPrevTransition; // previous year's transition
900 };
901 
902 }
903 
904 #endif
Base interface for ZoneProcessor classes.
Definition: ZoneProcessor.h:45
+
1 /*
2  * MIT License
3  * Copyright (c) 2019 Brian T. Park
4  */
5 
6 #ifndef ACE_TIME_BASIC_ZONE_PROCESSOR_H
7 #define ACE_TIME_BASIC_ZONE_PROCESSOR_H
8 
9 #include <string.h> // strchr()
10 #include <stdint.h>
11 #include "internal/ZonePolicy.h"
12 #include "internal/ZoneInfo.h"
13 #include "internal/Brokers.h"
14 #include "common/logging.h"
15 #include "TimeOffset.h"
16 #include "LocalDate.h"
17 #include "OffsetDateTime.h"
18 #include "ZoneProcessor.h"
19 
20 #define ACE_TIME_BASIC_ZONE_PROCESSOR_DEBUG 0
21 
22 class BasicZoneProcessorTest_init_primitives;
23 class BasicZoneProcessorTest_init;
24 class BasicZoneProcessorTest_setZoneInfo;
25 class BasicZoneProcessorTest_createAbbreviation;
26 class BasicZoneProcessorTest_calcStartDayOfMonth;
27 class BasicZoneProcessorTest_calcRuleOffsetCode;
28 
29 namespace ace_time {
30 
31 template<uint8_t SIZE, uint8_t TYPE, typename ZS, typename ZI, typename ZIB>
33 
34 namespace basic {
35 
51 struct Transition {
59  static const uint8_t kAbbrevSize = 6 + 1;
60 
67 
79 
81  acetime_t startEpochSeconds;
82 
84  int8_t yearTiny;
85 
91  int8_t offsetCode;
92 
94  int8_t deltaCode;
95 
104 
106  void log() const {
107  if (ACE_TIME_BASIC_ZONE_PROCESSOR_DEBUG) {
108  if (sizeof(acetime_t) == sizeof(int)) {
109  logging::printf("startEpochSeconds: %d\n", startEpochSeconds);
110  } else {
111  logging::printf("startEpochSeconds: %ld\n", startEpochSeconds);
112  }
113  logging::printf("offsetCode: %d\n", offsetCode);
114  logging::printf("abbrev: %s\n", abbrev);
115  if (rule.isNotNull()) {
116  logging::printf("Rule.fromYear: %d\n", rule.fromYearTiny());
117  logging::printf("Rule.toYear: %d\n", rule.toYearTiny());
118  logging::printf("Rule.inMonth: %d\n", rule.inMonth());
119  logging::printf("Rule.onDayOfMonth: %d\n", rule.onDayOfMonth());
120  }
121  }
122  }
123 };
124 
126 struct MonthDay {
127  uint8_t month;
128  uint8_t day;
129 };
130 
131 } // namespace basic
132 
185  public:
190  explicit BasicZoneProcessor(const basic::ZoneInfo* zoneInfo = nullptr):
191  ZoneProcessor(kTypeBasic),
192  mZoneInfo(zoneInfo) {}
193 
195  const void* getZoneInfo() const override {
196  return mZoneInfo.zoneInfo();
197  }
198 
199  uint32_t getZoneId() const override { return mZoneInfo.zoneId(); }
200 
201  TimeOffset getUtcOffset(acetime_t epochSeconds) const override {
202  const basic::Transition* transition = getTransition(epochSeconds);
203  int8_t code = (transition)
204  ? transition->offsetCode : TimeOffset::kErrorCode;
205  return TimeOffset::forOffsetCode(code);
206  }
207 
208  TimeOffset getDeltaOffset(acetime_t epochSeconds) const override {
209  const basic::Transition* transition = getTransition(epochSeconds);
210  int8_t code = (transition)
211  ? transition->deltaCode : TimeOffset::kErrorCode;
212  return TimeOffset::forOffsetCode(code);
213  }
214 
215  const char* getAbbrev(acetime_t epochSeconds) const override {
216  const basic::Transition* transition = getTransition(epochSeconds);
217  return (transition) ? transition->abbrev : "";
218  }
219 
249  OffsetDateTime getOffsetDateTime(const LocalDateTime& ldt) const override {
250  // Only a single local variable of OffsetDateTime used, to allow Return
251  // Value Optimization (and save 20 bytes of flash for WorldClock).
252  OffsetDateTime odt;
253  bool success = init(ldt.localDate());
254  if (success) {
255  // 0) Use the UTC epochSeconds to get intial guess of offset.
256  acetime_t epochSeconds0 = ldt.toEpochSeconds();
257  auto offset0 = getUtcOffset(epochSeconds0);
258 
259  // 1) Use offset0 to get the next epochSeconds and offset.
260  odt = OffsetDateTime::forLocalDateTimeAndOffset(ldt, offset0);
261  acetime_t epochSeconds1 = odt.toEpochSeconds();
262  auto offset1 = getUtcOffset(epochSeconds1);
263 
264  // 2) Use offset1 to get the next epochSeconds and offset.
265  odt = OffsetDateTime::forLocalDateTimeAndOffset(ldt, offset1);
266  acetime_t epochSeconds2 = odt.toEpochSeconds();
267  auto offset2 = getUtcOffset(epochSeconds2);
268 
269  // If offset1 and offset2 are equal, then we have an equilibrium
270  // and odt(1) must equal odt(2), so we can just return the last odt.
271  if (offset1.toOffsetCode() == offset2.toOffsetCode()) {
272  // pass
273  } else {
274  // Pick the later epochSeconds and offset
275  acetime_t epochSeconds;
276  TimeOffset offset;
277  if (epochSeconds1 > epochSeconds2) {
278  epochSeconds = epochSeconds1;
279  offset = offset1;
280  } else {
281  epochSeconds = epochSeconds2;
282  offset = offset2;
283  }
284  odt = OffsetDateTime::forEpochSeconds(epochSeconds, offset);
285  }
286  } else {
287  odt = OffsetDateTime::forError();
288  }
289 
290  return odt;
291  }
292 
293  void printTo(Print& printer) const override;
294 
295  void printShortTo(Print& printer) const override;
296 
298  void log() const {
299  if (ACE_TIME_BASIC_ZONE_PROCESSOR_DEBUG) {
300  if (!mIsFilled) {
301  logging::printf("*not initialized*\n");
302  return;
303  }
304  logging::printf("mYear: %d\n", mYear);
305  logging::printf("mNumTransitions: %d\n", mNumTransitions);
306  logging::printf("---- PrevTransition\n");
307  mPrevTransition.log();
308  for (int i = 0; i < mNumTransitions; i++) {
309  logging::printf("---- Transition: %d\n", i);
310  mTransitions[i].log();
311  }
312  }
313  }
314 
333  static basic::MonthDay calcStartDayOfMonth(int16_t year, uint8_t month,
334  uint8_t onDayOfWeek, int8_t onDayOfMonth) {
335  if (onDayOfWeek == 0) return {month, (uint8_t) onDayOfMonth};
336 
337  if (onDayOfMonth >= 0) {
338  // Convert "last{Xxx}" to "last{Xxx}>={daysInMonth-6}".
339  uint8_t daysInMonth = LocalDate::daysInMonth(year, month);
340  if (onDayOfMonth == 0) {
341  onDayOfMonth = daysInMonth - 6;
342  }
343 
344  auto limitDate = LocalDate::forComponents(year, month, onDayOfMonth);
345  uint8_t dayOfWeekShift = (onDayOfWeek - limitDate.dayOfWeek() + 7) % 7;
346  uint8_t day = (uint8_t) (onDayOfMonth + dayOfWeekShift);
347  if (day > daysInMonth) {
348  // TODO: Support shifting from Dec to Jan of following year.
349  day -= daysInMonth;
350  month++;
351  }
352  return {month, day};
353  } else {
354  onDayOfMonth = -onDayOfMonth;
355  auto limitDate = LocalDate::forComponents(year, month, onDayOfMonth);
356  int8_t dayOfWeekShift = (limitDate.dayOfWeek() - onDayOfWeek + 7) % 7;
357  int8_t day = onDayOfMonth - dayOfWeekShift;
358  if (day < 1) {
359  // TODO: Support shifting from Jan to Dec of the previous year.
360  month--;
361  uint8_t daysInPrevMonth = LocalDate::daysInMonth(year, month);
362  day += daysInPrevMonth;
363  }
364  return {month, (uint8_t) day};
365  }
366  }
367 
368  private:
369  friend class ::BasicZoneProcessorTest_init_primitives;
370  friend class ::BasicZoneProcessorTest_init;
371  friend class ::BasicZoneProcessorTest_setZoneInfo;
372  friend class ::BasicZoneProcessorTest_createAbbreviation;
373  friend class ::BasicZoneProcessorTest_calcStartDayOfMonth;
374  friend class ::BasicZoneProcessorTest_calcRuleOffsetCode;
375 
376  template<uint8_t SIZE, uint8_t TYPE, typename ZS, typename ZI, typename ZIB>
377  friend class ZoneProcessorCacheImpl; // setZoneInfo()
378 
380  static const uint8_t kMaxCacheEntries = 4;
381 
387  static const acetime_t kMinEpochSeconds = INT32_MIN + 1;
388 
389  // Disable copy constructor and assignment operator.
390  BasicZoneProcessor(const BasicZoneProcessor&) = delete;
391  BasicZoneProcessor& operator=(const BasicZoneProcessor&) = delete;
392 
393  bool equals(const ZoneProcessor& other) const override {
394  const auto& that = (const BasicZoneProcessor&) other;
395  return getZoneInfo() == that.getZoneInfo();
396  }
397 
399  void setZoneInfo(const void* zoneInfo) override {
400  if (mZoneInfo.zoneInfo() == zoneInfo) return;
401 
402  mZoneInfo = basic::ZoneInfoBroker((const basic::ZoneInfo*) zoneInfo);
403  mYear = 0;
404  mIsFilled = false;
405  mNumTransitions = 0;
406  }
407 
409  const basic::Transition* getTransition(acetime_t epochSeconds) const {
410  LocalDate ld = LocalDate::forEpochSeconds(epochSeconds);
411  bool success = init(ld);
412  return (success) ? findMatch(epochSeconds) : nullptr;
413  }
414 
443  bool init(const LocalDate& ld) const {
444  int16_t year = ld.year();
445  if (ld.month() == 1 && ld.day() == 1) {
446  year--;
447  }
448  if (isFilled(year)) return true;
449 
450  mYear = year;
451  mNumTransitions = 0; // clear cache
452 
453  if (year < mZoneInfo.startYear() - 1 || mZoneInfo.untilYear() < year) {
454  return false;
455  }
456 
457  addRulePriorToYear(year);
458  addRulesForYear(year);
459  calcTransitions();
460  calcAbbreviations();
461 
462  mIsFilled = true;
463  return true;
464  }
465 
467  bool isFilled(int16_t year) const {
468  return mIsFilled && (year == mYear);
469  }
470 
475  void addRulePriorToYear(int16_t year) const {
476  int8_t yearTiny = year - LocalDate::kEpochYear;
477  int8_t priorYearTiny = yearTiny - 1;
478 
479  // Find the prior Era.
480  const basic::ZoneEraBroker era = findZoneEraPriorTo(year);
481 
482  // If the prior ZoneEra is a simple Era (no zone policy), then create a
483  // Transition using a rule==nullptr. Otherwise, find the latest rule
484  // within the ZoneEra.
485  const basic::ZonePolicyBroker zonePolicy = era.zonePolicy();
486  basic::ZoneRuleBroker latest;
487  if (zonePolicy.isNotNull()) {
488  // Find the latest rule for the matching ZoneEra whose
489  // ZoneRule::toYearTiny < yearTiny. Assume that there are no more than
490  // 1 rule per month.
491  uint8_t numRules = zonePolicy.numRules();
492  for (uint8_t i = 0; i < numRules; i++) {
493  const basic::ZoneRuleBroker rule = zonePolicy.rule(i);
494  // Check if rule is effective prior to the given year
495  if (rule.fromYearTiny() < yearTiny) {
496  if ((latest.isNull()) || compareZoneRule(year, rule, latest) > 0) {
497  latest = rule;
498  }
499  }
500  }
501  }
502 
503  mPrevTransition = createTransition(era, latest, priorYearTiny);
504  }
505 
511  static basic::Transition createTransition(basic::ZoneEraBroker era,
513  int8_t offsetCode;
514  int8_t deltaCode;
515  char letter;
516  if (rule.isNull()) {
517  deltaCode = 0;
518  offsetCode = era.offsetCode();
519  letter = 0;
520  } else {
521  deltaCode = rule.deltaCode();
522  offsetCode = era.offsetCode() + deltaCode;
523  letter = rule.letter();
524  }
525 
526  return {
527  era,
528  rule,
529  0 /*epochSeconds*/,
530  yearTiny,
531  offsetCode,
532  deltaCode,
533  {letter} /*abbrev*/
534  };
535  }
536 
538  static int8_t compareZoneRule(int16_t year,
539  const basic::ZoneRuleBroker a, const basic::ZoneRuleBroker b) {
540  int16_t aYear = effectiveRuleYear(year, a);
541  int16_t bYear = effectiveRuleYear(year, b);
542  if (aYear < bYear) return -1;
543  if (aYear > bYear) return 1;
544  if (a.inMonth() < b.inMonth()) return -1;
545  if (a.inMonth() > b.inMonth()) return 1;
546  return 0;
547  }
548 
553  static int16_t effectiveRuleYear(int16_t year,
554  const basic::ZoneRuleBroker rule) {
555  int8_t yearTiny = year - LocalDate::kEpochYear;
556  if (rule.toYearTiny() < yearTiny) {
557  return rule.toYearTiny() + LocalDate::kEpochYear;
558  }
559  if (rule.fromYearTiny() < yearTiny) {
560  return year - 1;
561  }
562  return 0;
563  }
564 
566  void addRulesForYear(int16_t year) const {
567  const basic::ZoneEraBroker era = findZoneEra(year);
568 
569  // If the ZonePolicy has no rules, then add a Transition which takes
570  // effect at the start time of the current year.
571  const basic::ZonePolicyBroker zonePolicy = era.zonePolicy();
572  if (zonePolicy.isNull()) {
573  addRule(year, era, basic::ZoneRuleBroker());
574  return;
575  }
576 
577  // If the ZonePolicy has rules, find all matching transitions, and add
578  // them to mTransitions, in sorted order according to the
579  // ZoneRule::inMonth field.
580  int8_t yearTiny = year - LocalDate::kEpochYear;
581  uint8_t numRules = zonePolicy.numRules();
582  for (uint8_t i = 0; i < numRules; i++) {
583  const basic::ZoneRuleBroker rule = zonePolicy.rule(i);
584  if ((rule.fromYearTiny() <= yearTiny) &&
585  (yearTiny <= rule.toYearTiny())) {
586  addRule(year, era, rule);
587  }
588  }
589  }
590 
605  void addRule(int16_t year, basic::ZoneEraBroker era,
606  basic::ZoneRuleBroker rule) const {
607 
608  // If a zone needs more transitions than kMaxCacheEntries, the check below
609  // will cause the DST transition information to be inaccurate, and it is
610  // highly likely that this situation would be caught in the
611  // BasicValidationUsingPython or BasicValidationUsingJava unit tests.
612  // Since these unit tests pass, I feel confident that those zones which
613  // need more than kMaxCacheEntries are already filtered out by
614  // tzcompiler.py.
615  //
616  // Ideally, the tzcompiler.py script would explicitly remove those zones
617  // which need more than kMaxCacheEntries Transitions. But this would
618  // require a Python version of the BasicZoneProcessor, and unfortunately,
619  // zone_specifier.py implements only the ExtendedZoneProcessor algorithm
620  // An early version of zone_specifier.py may have implemented something
621  // close to BasicZoneProcessor, and it may be available in the git
622  // history. But it seems like too much work right now to try to dig that
623  // out, just to implement the explicit check for kMaxCacheEntries. It
624  // would mean maintaining another version of zone_specifier.py.
625  if (mNumTransitions >= kMaxCacheEntries) return;
626 
627  // insert new element at the end of the list
628  int8_t yearTiny = year - LocalDate::kEpochYear;
629  mTransitions[mNumTransitions] = createTransition(era, rule, yearTiny);
630  mNumTransitions++;
631 
632  // perform an insertion sort
633  for (uint8_t i = mNumTransitions - 1; i > 0; i--) {
634  basic::Transition& left = mTransitions[i - 1];
635  basic::Transition& right = mTransitions[i];
636  // assume only 1 rule per month
637  if ((left.rule.isNotNull() && right.rule.isNotNull() &&
638  left.rule.inMonth() > right.rule.inMonth())
639  || (left.rule.isNotNull() && right.rule.isNull())) {
640  basic::Transition tmp = left;
641  left = right;
642  right = tmp;
643  }
644  }
645  }
646 
652  const basic::ZoneEraBroker findZoneEra(int16_t year) const {
653  for (uint8_t i = 0; i < mZoneInfo.numEras(); i++) {
654  const basic::ZoneEraBroker era = mZoneInfo.era(i);
655  if (year < era.untilYearTiny() + LocalDate::kEpochYear) return era;
656  }
657  // Return the last ZoneEra if we run off the end.
658  return mZoneInfo.era(mZoneInfo.numEras() - 1);
659  }
660 
672  const basic::ZoneEraBroker findZoneEraPriorTo(int16_t year) const {
673  for (uint8_t i = 0; i < mZoneInfo.numEras(); i++) {
674  const basic::ZoneEraBroker era = mZoneInfo.era(i);
675  if (year <= era.untilYearTiny() + LocalDate::kEpochYear) return era;
676  }
677  // Return the last ZoneEra if we run off the end.
678  return mZoneInfo.era(mZoneInfo.numEras() - 1);
679  }
680 
688  void calcTransitions() const {
689  // Set the initial startEpochSeconds to be -Infinity
690  mPrevTransition.startEpochSeconds = kMinEpochSeconds;
691  const basic::Transition* prevTransition = &mPrevTransition;
692 
693  for (uint8_t i = 0; i < mNumTransitions; i++) {
694  basic::Transition& transition = mTransitions[i];
695  const int16_t year = transition.yearTiny + LocalDate::kEpochYear;
696 
697  if (transition.rule.isNull()) {
698  // If the transition is simple (has no named rule), then the
699  // ZoneEra applies for the entire year (since BasicZoneProcessor
700  // supports only whole year in the UNTIL field). The whole year UNTIL
701  // field has an implied 'w' modifier on 00:00, we don't need to call
702  // calcRuleOffsetCode() with a 'w', we can just use the previous
703  // transition's offset to calculate the startDateTime of this
704  // transition.
705  //
706  // Also, when transition.rule == nullptr, the mNumTransitions should
707  // be 1, since only a single transition is added by
708  // addRulesForYear().
709  const int8_t prevOffsetCode = prevTransition->offsetCode;
711  year, 1, 1, 0, 0, 0,
712  TimeOffset::forOffsetCode(prevOffsetCode));
713  transition.startEpochSeconds = startDateTime.toEpochSeconds();
714  } else {
715  // In this case, the transition points to a named ZonePolicy, which
716  // means that there could be multiple ZoneRules associated with the
717  // given year. For each transition, determine the startEpochSeconds,
718  // and the effective offset code.
719 
720  // Determine the start date of the rule.
721  const basic::MonthDay monthDay = calcStartDayOfMonth(
722  year, transition.rule.inMonth(), transition.rule.onDayOfWeek(),
723  transition.rule.onDayOfMonth());
724 
725  // Determine the offset of the 'atTimeModifier'. The 'w' modifier
726  // requires the offset of the previous transition.
727  const int8_t prevOffsetCode = calcRuleOffsetCode(
728  prevTransition->offsetCode,
729  transition.era.offsetCode(),
730  transition.rule.atTimeModifier());
731 
732  // startDateTime
733  const uint16_t minutes = transition.rule.atTimeMinutes();
734  const uint8_t atHour = minutes / 60;
735  const uint8_t atMinute = minutes % 60;
737  year, monthDay.month, monthDay.day,
738  atHour, atMinute, 0 /*second*/,
739  TimeOffset::forOffsetCode(prevOffsetCode));
740  transition.startEpochSeconds = startDateTime.toEpochSeconds();
741  }
742 
743  prevTransition = &transition;
744  }
745  }
746 
753  static int8_t calcRuleOffsetCode(int8_t prevEffectiveOffsetCode,
754  int8_t currentBaseOffsetCode, uint8_t atModifier) {
755  if (atModifier == basic::ZoneContext::TIME_MODIFIER_W) {
756  return prevEffectiveOffsetCode;
757  } else if (atModifier == basic::ZoneContext::TIME_MODIFIER_S) {
758  return currentBaseOffsetCode;
759  } else { // 'u', 'g' or 'z'
760  return 0;
761  }
762  }
763 
765  void calcAbbreviations() const {
766  calcAbbreviation(&mPrevTransition);
767  for (uint8_t i = 0; i < mNumTransitions; i++) {
768  calcAbbreviation(&mTransitions[i]);
769  }
770  }
771 
773  static void calcAbbreviation(basic::Transition* transition) {
774  createAbbreviation(
775  transition->abbrev,
777  transition->era.format(),
778  transition->deltaCode,
779  transition->abbrev[0]);
780  }
781 
817  static void createAbbreviation(char* dest, uint8_t destSize,
818  const char* format, uint8_t deltaCode, char letter) {
819  // Check if RULES column empty.
820  if (deltaCode == 0 && letter == '\0') {
821  strncpy(dest, format, destSize);
822  dest[destSize - 1] = '\0';
823  return;
824  }
825 
826  // Check if FORMAT contains a '%'.
827  if (strchr(format, '%') != nullptr) {
828  copyAndReplace(dest, destSize, format, '%', letter);
829  } else {
830  // Check if FORMAT contains a '/'.
831  const char* slashPos = strchr(format, '/');
832  if (slashPos != nullptr) {
833  if (deltaCode == 0) {
834  uint8_t headLength = (slashPos - format);
835  if (headLength >= destSize) headLength = destSize - 1;
836  memcpy(dest, format, headLength);
837  dest[headLength] = '\0';
838  } else {
839  uint8_t tailLength = strlen(slashPos+1);
840  if (tailLength >= destSize) tailLength = destSize - 1;
841  memcpy(dest, slashPos+1, tailLength);
842  dest[tailLength] = '\0';
843  }
844  } else {
845  // Just copy the FORMAT disregarding the deltaCode and letter.
846  strncpy(dest, format, destSize);
847  dest[destSize - 1] = '\0';
848  }
849  }
850  }
851 
857  static void copyAndReplace(char* dst, uint8_t dstSize, const char* src,
858  char oldChar, char newChar) {
859  while (*src != '\0' && dstSize > 0) {
860  if (*src == oldChar) {
861  if (newChar == '-') {
862  src++;
863  } else {
864  *dst = newChar;
865  dst++;
866  src++;
867  dstSize--;
868  }
869  } else {
870  *dst++ = *src++;
871  dstSize--;
872  }
873  }
874 
875  if (dstSize == 0) {
876  --dst;
877  }
878  *dst = '\0';
879  }
880 
882  const basic::Transition* findMatch(acetime_t epochSeconds) const {
883  const basic::Transition* closestMatch = &mPrevTransition;
884  for (uint8_t i = 0; i < mNumTransitions; i++) {
885  const basic::Transition* m = &mTransitions[i];
886  if (m->startEpochSeconds <= epochSeconds) {
887  closestMatch = m;
888  }
889  }
890  return closestMatch;
891  }
892 
893  basic::ZoneInfoBroker mZoneInfo;
894 
895  mutable int16_t mYear = 0; // maybe create LocalDate::kInvalidYear?
896  mutable bool mIsFilled = false;
897  mutable uint8_t mNumTransitions = 0;
898  mutable basic::Transition mTransitions[kMaxCacheEntries];
899  mutable basic::Transition mPrevTransition; // previous year's transition
900 };
901 
902 }
903 
904 #endif
Base interface for ZoneProcessor classes.
Definition: ZoneProcessor.h:45
ZoneEraBroker era
The ZoneEra that matched the given year.
const char * getAbbrev(acetime_t epochSeconds) const override
Return the time zone abbreviation at epochSeconds.
void log() const
Used only for debugging.
A cache of ZoneProcessors that provides a ZoneProcessor to the TimeZone upon request.
static const uint8_t kAbbrevSize
Longest abbreviation currently seems to be 5 characters (https://www.timeanddate.com/time/zones/) but...
-
Data broker for accessing ZonePolicy in PROGMEM.
Definition: Brokers.h:297
+
Data broker for accessing ZonePolicy in PROGMEM.
Definition: Brokers.h:322
char abbrev[kAbbrevSize]
The calculated effective time zone abbreviation, e.g.
static OffsetDateTime forLocalDateTimeAndOffset(const LocalDateTime &localDateTime, TimeOffset timeOffset)
Factory method from LocalDateTime and TimeOffset.
BasicZoneProcessor(const basic::ZoneInfo *zoneInfo=nullptr)
Constructor.
-
Representation of a given time zone, implemented as an array of ZoneEra records.
Definition: ZoneInfo.h:77
+
Representation of a given time zone, implemented as an array of ZoneEra records.
Definition: ZoneInfo.h:86
The result of calcStartDayOfMonth().
virtual const void * getZoneInfo() const =0
Return the opaque zoneInfo.
uint32_t getZoneId() const override
Return the unique stable zoneId.
static OffsetDateTime forComponents(int16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, TimeOffset timeOffset)
Factory method using separated date, time, and UTC offset fields.
acetime_t startEpochSeconds
The calculated transition time of the given rule.
-
uint8_t day() const
Return the day of the month.
Definition: LocalDate.h:237
+
uint8_t day() const
Return the day of the month.
Definition: LocalDate.h:245
int8_t yearTiny
Year which applies to the ZoneEra or ZoneRule.
static OffsetDateTime forEpochSeconds(acetime_t epochSeconds, TimeOffset timeOffset)
Factory method.
ZoneRuleBroker rule
The Zone transition rule that matched for the the given year.
-
acetime_t toEpochSeconds() const
Return seconds since AceTime epoch 2000-01-01 00:00:00Z, after assuming that the date and time compon...
-
const LocalDate & localDate() const
Return the LocalDate.
+
acetime_t toEpochSeconds() const
Return seconds since AceTime epoch 2000-01-01 00:00:00Z, after assuming that the date and time compon...
+
const LocalDate & localDate() const
Return the LocalDate.
TimeOffset getDeltaOffset(acetime_t epochSeconds) const override
Return the DST delta offset at epochSeconds.
const void * getZoneInfo() const override
Return the underlying ZoneInfo.
void log() const
Used only for debugging.
-
static const int8_t kErrorCode
Sentinel value that represents an error.
Definition: TimeOffset.h:61
+
static const int8_t kErrorCode
Sentinel value that represents an error.
Definition: TimeOffset.h:64
An implementation of ZoneProcessor that supports a subset of the zones containing in the TZ Database...
static LocalDate forEpochSeconds(acetime_t epochSeconds)
Factory method using the number of seconds since AceTime epoch of 2000-01-01.
Definition: LocalDate.h:145
-
static TimeOffset forOffsetCode(int8_t offsetCode)
Create TimeOffset from the offset code.
Definition: TimeOffset.h:117
+
static TimeOffset forOffsetCode(int8_t offsetCode)
Create TimeOffset from the offset code.
Definition: TimeOffset.h:120
The classes provide a thin layer of indirection for accessing the zoneinfo files stored in the zonedb...
The date (year, month, day), time (hour, minute, second) and offset from UTC (timeOffset).
Data structure that defines the start of a specific UTC offset as described by the matching ZoneEra a...
OffsetDateTime getOffsetDateTime(const LocalDateTime &ldt) const override
-
acetime_t toEpochSeconds() const
Return seconds since AceTime epoch (2000-01-01 00:00:00Z), taking into account the offset zone...
-
A thin wrapper that represents a time offset from a reference point, usually 00:00 at UTC...
Definition: TimeOffset.h:58
+
acetime_t toEpochSeconds() const
Return seconds since AceTime epoch (2000-01-01 00:00:00Z), taking into account the offset zone...
+
A thin wrapper that represents a time offset from a reference point, usually 00:00 at UTC...
Definition: TimeOffset.h:61
static LocalDate forComponents(int16_t year, uint8_t month, uint8_t day)
Factory method using separated year, month and day fields.
Definition: LocalDate.h:94
The date (year, month, day) representing the date without regards to time zone.
Definition: LocalDate.h:36
+
static const uint8_t TIME_MODIFIER_S
Represents &#39;s&#39; or standard time.
Definition: ZoneContext.h:16
static const int16_t kEpochYear
Base year of epoch.
Definition: LocalDate.h:39
int8_t offsetCode
The total effective UTC offsetCode at the start of transition, including DST offset.
static uint8_t daysInMonth(int16_t year, uint8_t month)
Return the number of days in the current month.
Definition: LocalDate.h:210
int8_t deltaCode
The delta offsetCode from "standard time" at the start of transition.
-
uint8_t month() const
Return the month with January=1, December=12.
Definition: LocalDate.h:231
+
uint8_t month() const
Return the month with January=1, December=12.
Definition: LocalDate.h:239
int16_t year() const
Return the full year instead of just the last 2 digits.
Definition: LocalDate.h:219
static basic::MonthDay calcStartDayOfMonth(int16_t year, uint8_t month, uint8_t onDayOfWeek, int8_t onDayOfMonth)
Calculate the actual (month, day) of the expresssion (onDayOfWeek >= onDayOfMonth) or (onDayOfWeek <=...
+
static const uint8_t TIME_MODIFIER_W
Represents &#39;w&#39; or wall time.
Definition: ZoneContext.h:13
static OffsetDateTime forError()
Factory method that returns an instance whose isError() is true.
Class that holds the date-time as the components (year, month, day, hour, minute, second) without reg...
Definition: LocalDateTime.h:27
diff --git a/docs/html/BasicZone_8h_source.html b/docs/html/BasicZone_8h_source.html index cc8f0664f..f94afa45a 100644 --- a/docs/html/BasicZone_8h_source.html +++ b/docs/html/BasicZone_8h_source.html @@ -22,7 +22,7 @@
AceTime -  0.6.1 +  0.7
Date and time classes for Arduino that support timezones from the TZ Database, and a system clock that can synchronize from an NTP server or an RTC chip.
@@ -69,7 +69,7 @@
1 /*
2  * MIT License
3  * Copyright (c) 2019 Brian T. Park
4  */
5 
6 #ifndef ACE_TIME_BASIC_ZONE_H
7 #define ACE_TIME_BASIC_ZONE_H
8 
9 #include "internal/ZoneInfo.h"
10 #include "internal/Brokers.h"
11 #include "common/compat.h"
12 
13 class __FlashStringHelper;
14 
15 namespace ace_time {
16 
22 class BasicZone {
23  public:
24  BasicZone(const basic::ZoneInfo* zoneInfo):
25  mZoneInfoBroker(zoneInfo) {}
26 
27  // use default copy constructor and assignment operator
28  BasicZone(const BasicZone&) = default;
29  BasicZone& operator=(const BasicZone&) = default;
30 
31 // TODO: Merge this with ExtendedZone.h now that they both use the same
32 // ACE_TIME_USE_PROGMEM macro.
33 #if ACE_TIME_USE_PROGMEM
34  const __FlashStringHelper* name() const {
35  return (const __FlashStringHelper*) mZoneInfoBroker.name();
36  }
37 
38  const __FlashStringHelper* shortName() const {
39  const char* name = mZoneInfoBroker.name();
40  const char* slash = strrchr_P(name, '/');
41  return (slash) ? (const __FlashStringHelper*) (slash + 1)
42  : (const __FlashStringHelper*) name;
43  }
44 #else
45  const char* name() const {
46  return (const char*) mZoneInfoBroker.name();
47  }
48 
49  const char* shortName() const {
50  const char* name = mZoneInfoBroker.name();
51  const char* slash = strrchr(name, '/');
52  return (slash) ? (slash + 1) : name;
53  }
54 #endif
55 
56  uint32_t zoneId() const {
57  return mZoneInfoBroker.zoneId();
58  }
59 
60  private:
61  const basic::ZoneInfoBroker mZoneInfoBroker;
62 };
63 
64 }
65 
66 #endif
-
Representation of a given time zone, implemented as an array of ZoneEra records.
Definition: ZoneInfo.h:77
+
Representation of a given time zone, implemented as an array of ZoneEra records.
Definition: ZoneInfo.h:86
A thin wrapper around a basic::ZoneInfo data structure to provide a stable API access to some useful ...
Definition: BasicZone.h:22
The classes provide a thin layer of indirection for accessing the zoneinfo files stored in the zonedb...
diff --git a/docs/html/Brokers_8h.html b/docs/html/Brokers_8h.html index 0236ec488..ccf979bee 100644 --- a/docs/html/Brokers_8h.html +++ b/docs/html/Brokers_8h.html @@ -22,7 +22,7 @@
AceTime -  0.6.1 +  0.7
Date and time classes for Arduino that support timezones from the TZ Database, and a system clock that can synchronize from an NTP server or an RTC chip.
@@ -66,7 +66,8 @@
Brokers.h File Reference
@@ -83,11 +84,11 @@ - + - - + +
@@ -180,6 +181,16 @@ typedef internal::FlashZoneRegistryBroker< ZoneInfo > ace_time::extended::ZoneRegistryBroker   + + + + + + + +

+Functions

uint16_t ace_time::internal::timeCodeToMinutes (uint8_t rawCode, uint8_t rawModifier)
 Convert (timeCode, timeModifier) fields in zoneinfo to minutes. More...
 
+uint8_t ace_time::internal::toModifier (uint8_t rawModifier)
 Extract the 'w', 's' 'u' suffix from the 'modifier' field, so that they can be compared against TIME_MODIFIER_W, TIME_MODIFIER_S and TIME_MODIFIER_U.
 

Detailed Description

The classes provide a thin layer of indirection for accessing the zoneinfo files stored in the zonedb/ and zonedbx/ directories.

@@ -189,7 +200,48 @@

The core broker classes live in the internal:: namespace and are templatized so that they can be used for both basic::Zone* classes and the extended::Zone* classes. Specific template instantiations are created in the basic:: and extended:: namespaces so that they can be used by the BasicZoneProcessor and ExtendedZoneProcessor respectively.

Definition in file Brokers.h.

-
+

Function Documentation

+ +

◆ timeCodeToMinutes()

+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
uint16_t ace_time::internal::timeCodeToMinutes (uint8_t rawCode,
uint8_t rawModifier 
)
+
+inline
+
+ +

Convert (timeCode, timeModifier) fields in zoneinfo to minutes.

+ +

Definition at line 48 of file Brokers.h.

+ +
+
+