From 063cfdcd7c70be501d67df22dbf01684d6e307d1 Mon Sep 17 00:00:00 2001 From: Timur Safin <tsafin@tarantool.org> Date: Mon, 16 May 2022 01:05:48 +0300 Subject: [PATCH] datetime: implement date.isdst Properly calculate `isdst` field in datetime Lua object and for attribute returned by `:totable()` function. NO_DOC=next commit (cherry picked from commit aec6fbac1f22096bb686bca4785742feec7b9e1b) --- .../unreleased/gh-6751-olson-timezones.md | 4 + extra/exports | 1 + src/lib/core/datetime.c | 45 ++++- src/lib/core/datetime.h | 12 ++ src/lib/tzcode/gen-zone-abbrevs.pl | 7 + src/lib/tzcode/timezone.c | 37 ++++- src/lib/tzcode/timezone.h | 11 ++ src/lib/tzcode/timezones.h | 156 +++++++++--------- src/lua/datetime.lua | 10 +- src/lua/tnt_datetime.c | 6 + test/app-tap/datetime.test.lua | 55 +++++- 11 files changed, 257 insertions(+), 87 deletions(-) create mode 100644 changelogs/unreleased/gh-6751-olson-timezones.md diff --git a/changelogs/unreleased/gh-6751-olson-timezones.md b/changelogs/unreleased/gh-6751-olson-timezones.md new file mode 100644 index 0000000000..f9c577d4e9 --- /dev/null +++ b/changelogs/unreleased/gh-6751-olson-timezones.md @@ -0,0 +1,4 @@ +## feature/datetime + + * `isdst` field in datetime object is now properly calculated according to + IANA tzdata (aka Olson DB) rules for given date/time moment (gh-6751); diff --git a/extra/exports b/extra/exports index 9dc404f010..c88f0304b4 100644 --- a/extra/exports +++ b/extra/exports @@ -432,6 +432,7 @@ title_set_status title_update tnt_datetime_datetime_sub tnt_datetime_increment_by +tnt_datetime_isdst tnt_datetime_now tnt_datetime_parse_full tnt_datetime_parse_tz diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index 62fd5da0fa..a9d9bd84aa 100644 --- a/src/lib/core/datetime.c +++ b/src/lib/core/datetime.c @@ -47,6 +47,48 @@ local_secs(const struct datetime *date) return (int64_t)date->epoch + date->tzoffset * 60; } +/** + * Resolve tzindex encoded timezone from @sa date using Olson facilities. + * @param[in] date decode input datetime value. + * @param[out] gmtoff return resolved timezone offset (in seconds). + * @param[out] isdst return resolved daylight saving time status for the zone. + */ +static inline bool +datetime_timezone_lookup(const struct datetime *date, long *gmtoff, int *isdst) +{ + if (date->tzindex == 0) + return false; + + struct tnt_tm tm = {.tm_epoch = date->epoch}; + if (!timezone_tzindex_lookup(date->tzindex, &tm)) + return false; + + *gmtoff = tm.tm_gmtoff; + *isdst = tm.tm_isdst; + + return true; +} + +bool +datetime_isdst(const struct datetime *date) +{ + int isdst = 0; + long gmtoff = 0; + + return datetime_timezone_lookup(date, &gmtoff, &isdst) && (isdst != 0); +} + +long +datetime_gmtoff(const struct datetime *date) +{ + int isdst = 0; + long gmtoff = date->tzoffset * 60; + + datetime_timezone_lookup(date, &gmtoff, &isdst); + + return gmtoff; +} + void datetime_to_tm(const struct datetime *date, struct tnt_tm *tm) { @@ -721,7 +763,7 @@ datetime_increment_by(struct datetime *self, int direction, int64_t secs = local_secs(self); int64_t dt = local_dt(secs); int nsec = self->nsec; - int offset = self->tzindex; + int offset = self->tzoffset; bool is_ymd_updated = false; int64_t years = ival->year; @@ -809,6 +851,7 @@ datetime_increment_by(struct datetime *self, int direction, self->epoch = utc_secs(secs, offset); self->nsec = nsec; + self->tzoffset = datetime_gmtoff(self) / 60; return 0; } diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h index ca5dbee1b4..91f8bce8aa 100644 --- a/src/lib/core/datetime.h +++ b/src/lib/core/datetime.h @@ -164,6 +164,18 @@ datetime_to_tm(const struct datetime *date, struct tnt_tm *tm); bool tm_to_datetime(struct tnt_tm *tm, struct datetime *date); +/** + * Return whether given @sa datetime moment is DST. + */ +bool +datetime_isdst(const struct datetime *date); + +/** + * Return gmtoff of a given @sa datetime moment. + */ +long +datetime_gmtoff(const struct datetime *date); + /** * Parse datetime text in ISO-8601 given format, and construct output * datetime value diff --git a/src/lib/tzcode/gen-zone-abbrevs.pl b/src/lib/tzcode/gen-zone-abbrevs.pl index 37dda6219f..61ae07b428 100755 --- a/src/lib/tzcode/gen-zone-abbrevs.pl +++ b/src/lib/tzcode/gen-zone-abbrevs.pl @@ -110,6 +110,9 @@ BEGIN { TZ_MILITARY => 0x04, TZ_AMBIGUOUS => 0x08, TZ_NYI => 0x10, + TZ_OLSON => 0x20, + TZ_ALIAS => 0x40, + TZ_DST => 0x80, ); require constant; constant->import(\%flags); %FlagName = reverse %flags; @@ -156,6 +159,10 @@ sub read_abbrevs_file($) { $offset = $h * 60 + $m; } + if (/(Daylight|Summer)/) { + $flags |= TZ_DST; + } + if ( exists $ZoneAbbrevs{$encoded} ) { $flags |= TZ_AMBIGUOUS; $offset = 0; diff --git a/src/lib/tzcode/timezone.c b/src/lib/tzcode/timezone.c index c4cd0a5a2d..b53777723d 100644 --- a/src/lib/tzcode/timezone.c +++ b/src/lib/tzcode/timezone.c @@ -79,6 +79,12 @@ timezone_flags(const struct date_time_zone *zone) return zone->flags; } +bool +timezone_isdst(const struct date_time_zone *zone) +{ + return !!(zone->flags & TZ_DST); +} + const char* timezone_name(int64_t index) { @@ -124,10 +130,9 @@ timezone_raw_lookup(const char *str, size_t len, if (found != NULL) { /* lua assumes that single bit is set, not both */ - assert((found->flags & (TZ_NYI | TZ_AMBIGUOUS)) != - (TZ_NYI | TZ_AMBIGUOUS)); - if (found->flags & (TZ_NYI | TZ_AMBIGUOUS)) - return -found->flags; + assert((found->flags & TZ_ERROR_MASK) != TZ_ERROR_MASK); + if (found->flags & TZ_ERROR_MASK) + return -(found->flags & TZ_ERROR_MASK); *zone = found; return len; } @@ -147,7 +152,7 @@ timezone_tm_lookup(const char *str, size_t len, if ((found->flags & TZ_OLSON) == 0) { tm->tm_gmtoff = found->offset * 60; tm->tm_tzindex = found->id; - tm->tm_isdst = false; + tm->tm_isdst = !!(found->flags & TZ_DST); return rc; } timezone_t tz = tzalloc(str); // FIXME - cache loaded @@ -199,3 +204,25 @@ timezone_epoch_lookup(const char *str, size_t len, time_t base, tzfree(tz); return 0; } + +bool +timezone_tzindex_lookup(int16_t tzindex, struct tnt_tm *tm) +{ + assert(tm != NULL); + if (tzindex == 0) + return false; + + timezone_t tz = tzalloc(timezone_name(tzindex)); + if (tz == NULL) + return false; + time_t epoch = (int64_t)tm->tm_epoch; + struct tnt_tm *result = tnt_localtime_rz(tz, &epoch, tm); + if (result == NULL) + goto exit_failure; + tzfree(tz); + return true; +exit_failure: + if (tz != NULL) + tzfree(tz); + return false; +} diff --git a/src/lib/tzcode/timezone.h b/src/lib/tzcode/timezone.h index b8eaf8e3d9..ea9b3c6565 100644 --- a/src/lib/tzcode/timezone.h +++ b/src/lib/tzcode/timezone.h @@ -21,6 +21,7 @@ enum { TZ_NYI = 0x10, TZ_OLSON = 0x20, TZ_ALIAS = 0x40, + TZ_DST = 0x80, TZ_ERROR_MASK = TZ_AMBIGUOUS | TZ_NYI, }; @@ -67,6 +68,13 @@ timezone_tm_lookup(const char *str, size_t len, const struct date_time_zone **zone, struct tnt_tm *tm); +/** + * Parse timezone string corresponding to @sa tzindex like + * @sa timezone_tm_lookup does. + */ +bool +timezone_tzindex_lookup(int16_t tzindex, struct tnt_tm *tm); + /** Return offset in minutes for given zone */ int16_t timezone_offset(const struct date_time_zone *zone); @@ -76,6 +84,9 @@ timezone_index(const struct date_time_zone *zone); /** Return attributes flags for given zone */ uint16_t timezone_flags(const struct date_time_zone *zone); +/** Return DST flag for given zone */ +bool +timezone_isdst(const struct date_time_zone *zone); /** Translate tzindex to zone name */ const char* timezone_name(int64_t index); diff --git a/src/lib/tzcode/timezones.h b/src/lib/tzcode/timezones.h index b412b585b6..12d76d8302 100644 --- a/src/lib/tzcode/timezones.h +++ b/src/lib/tzcode/timezones.h @@ -37,13 +37,13 @@ ZONE_ABBREV( 104, 780, "ST", 0) ZONE_ABBREV( 112, 0, "UT", TZ_UTC|TZ_RFC) ZONE_ABBREV( 120, 0, "WT", 0) ZONE_ABBREV( 128, 0, "ACT", TZ_AMBIGUOUS) -ZONE_ABBREV( 129, 0, "ADT", TZ_AMBIGUOUS) +ZONE_ABBREV( 129, 0, "ADT", TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 130, 0, "AET", TZ_AMBIGUOUS) ZONE_ABBREV( 131, 270, "AFT", 0) ZONE_ABBREV( 132, 0, "AMT", TZ_AMBIGUOUS) ZONE_ABBREV( 133, -720, "AoE", 0) ZONE_ABBREV( 134, -180, "ART", 0) -ZONE_ABBREV( 135, 0, "AST", TZ_AMBIGUOUS) +ZONE_ABBREV( 135, 0, "AST", TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 136, 240, "AZT", 0) ZONE_ABBREV( 144, 0, "BDT", TZ_AMBIGUOUS) ZONE_ABBREV( 145, 480, "BNT", 0) @@ -53,7 +53,7 @@ ZONE_ABBREV( 148, 0, "BST", TZ_AMBIGUOUS) ZONE_ABBREV( 149, 360, "BTT", 0) ZONE_ABBREV( 152, 120, "CAT", 0) ZONE_ABBREV( 153, 390, "CCT", 0) -ZONE_ABBREV( 154, -300, "CDT", TZ_RFC|TZ_AMBIGUOUS) +ZONE_ABBREV( 154, -300, "CDT", TZ_RFC|TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 155, 60, "CET", 0) ZONE_ABBREV( 156, -300, "CIT", 0) ZONE_ABBREV( 157, -600, "CKT", 0) @@ -64,7 +64,7 @@ ZONE_ABBREV( 161, -60, "CVT", 0) ZONE_ABBREV( 162, 420, "CXT", 0) ZONE_ABBREV( 168, 180, "EAT", 0) ZONE_ABBREV( 169, 0, "ECT", TZ_AMBIGUOUS) -ZONE_ABBREV( 170, -240, "EDT", TZ_RFC|TZ_AMBIGUOUS) +ZONE_ABBREV( 170, -240, "EDT", TZ_RFC|TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 171, 120, "EET", 0) ZONE_ABBREV( 172, -60, "EGT", 0) ZONE_ABBREV( 173, -300, "EST", TZ_RFC|TZ_AMBIGUOUS) @@ -83,7 +83,7 @@ ZONE_ABBREV( 194, -240, "HAE", 0) ZONE_ABBREV( 195, -420, "HAP", 0) ZONE_ABBREV( 196, -360, "HAR", 0) ZONE_ABBREV( 197, -90, "HAT", 0) -ZONE_ABBREV( 198, -540, "HDT", 0) +ZONE_ABBREV( 198, -540, "HDT", TZ_DST) ZONE_ABBREV( 199, 480, "HKT", 0) ZONE_ABBREV( 200, -210, "HLV", 0) ZONE_ABBREV( 201, -240, "HNA", 0) @@ -94,7 +94,7 @@ ZONE_ABBREV( 205, -420, "HNR", 0) ZONE_ABBREV( 206, -150, "HNT", 0) ZONE_ABBREV( 207, -600, "HST", 0) ZONE_ABBREV( 208, 420, "ICT", 0) -ZONE_ABBREV( 209, 0, "IDT", TZ_AMBIGUOUS) +ZONE_ABBREV( 209, 0, "IDT", TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 210, 360, "IOT", 0) ZONE_ABBREV( 211, 0, "IST", TZ_AMBIGUOUS) ZONE_ABBREV( 216, 540, "JST", 0) @@ -102,25 +102,25 @@ ZONE_ABBREV( 224, 360, "KGT", 0) ZONE_ABBREV( 225, 300, "KIT", 0) ZONE_ABBREV( 226, 540, "KST", 0) ZONE_ABBREV( 232, 180, "MCK", 0) -ZONE_ABBREV( 233, -360, "MDT", TZ_RFC) +ZONE_ABBREV( 233, -360, "MDT", TZ_RFC|TZ_DST) ZONE_ABBREV( 234, 60, "MEZ", 0) ZONE_ABBREV( 235, 720, "MHT", 0) ZONE_ABBREV( 236, 390, "MMT", 0) -ZONE_ABBREV( 237, 240, "MSD", 0) +ZONE_ABBREV( 237, 240, "MSD", TZ_DST) ZONE_ABBREV( 238, 180, "MSK", 0) ZONE_ABBREV( 239, -420, "MST", TZ_RFC|TZ_AMBIGUOUS) ZONE_ABBREV( 240, 240, "MUT", 0) ZONE_ABBREV( 241, 300, "MVT", 0) ZONE_ABBREV( 242, 480, "MYT", 0) ZONE_ABBREV( 248, 660, "NCT", 0) -ZONE_ABBREV( 249, -90, "NDT", 0) +ZONE_ABBREV( 249, -90, "NDT", TZ_DST) ZONE_ABBREV( 250, 660, "NFT", 0) ZONE_ABBREV( 251, 345, "NPT", 0) ZONE_ABBREV( 252, 720, "NRT", 0) ZONE_ABBREV( 253, -150, "NST", 0) ZONE_ABBREV( 254, -660, "NUT", 0) ZONE_ABBREV( 256, 120, "OEZ", 0) -ZONE_ABBREV( 264, -420, "PDT", TZ_RFC) +ZONE_ABBREV( 264, -420, "PDT", TZ_RFC|TZ_DST) ZONE_ABBREV( 265, -300, "PET", 0) ZONE_ABBREV( 266, 600, "PGT", 0) ZONE_ABBREV( 267, 480, "PHT", 0) @@ -148,7 +148,7 @@ ZONE_ABBREV( 298, 300, "UZT", 0) ZONE_ABBREV( 304, -210, "VET", 0) ZONE_ABBREV( 305, 660, "VUT", 0) ZONE_ABBREV( 312, 0, "WAT", TZ_AMBIGUOUS) -ZONE_ABBREV( 313, 540, "WDT", 0) +ZONE_ABBREV( 313, 540, "WDT", TZ_DST) ZONE_ABBREV( 314, 0, "WET", 0) ZONE_ABBREV( 315, 0, "WEZ", 0) ZONE_ABBREV( 316, 720, "WFT", 0) @@ -156,78 +156,78 @@ ZONE_ABBREV( 317, -180, "WGT", 0) ZONE_ABBREV( 318, 420, "WIB", 0) ZONE_ABBREV( 319, 540, "WIT", 0) ZONE_ABBREV( 320, 0, "WST", TZ_AMBIGUOUS) -ZONE_ABBREV( 328, 630, "ACDT", 0) +ZONE_ABBREV( 328, 630, "ACDT", TZ_DST) ZONE_ABBREV( 329, 570, "ACST", 0) -ZONE_ABBREV( 330, 0, "ADST", TZ_AMBIGUOUS) -ZONE_ABBREV( 331, 660, "AEDT", 0) +ZONE_ABBREV( 330, 0, "ADST", TZ_AMBIGUOUS|TZ_DST) +ZONE_ABBREV( 331, 660, "AEDT", TZ_DST) ZONE_ABBREV( 332, 600, "AEST", 0) -ZONE_ABBREV( 333, -480, "AKDT", 0) +ZONE_ABBREV( 333, -480, "AKDT", TZ_DST) ZONE_ABBREV( 334, -540, "AKST", 0) ZONE_ABBREV( 335, 360, "ALMT", 0) -ZONE_ABBREV( 336, 300, "AMDT", 0) -ZONE_ABBREV( 337, 0, "AMST", TZ_AMBIGUOUS) +ZONE_ABBREV( 336, 300, "AMDT", TZ_DST) +ZONE_ABBREV( 337, 0, "AMST", TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 338, 720, "ANAT", 0) ZONE_ABBREV( 339, 300, "AQTT", 0) -ZONE_ABBREV( 340, 540, "AWDT", 0) +ZONE_ABBREV( 340, 540, "AWDT", TZ_DST) ZONE_ABBREV( 341, 480, "AWST", 0) ZONE_ABBREV( 342, -60, "AZOT", 0) -ZONE_ABBREV( 343, 300, "AZST", 0) -ZONE_ABBREV( 344, 60, "BDST", 0) -ZONE_ABBREV( 345, -120, "BRST", 0) +ZONE_ABBREV( 343, 300, "AZST", TZ_DST) +ZONE_ABBREV( 344, 60, "BDST", TZ_DST) +ZONE_ABBREV( 345, -120, "BRST", TZ_DST) ZONE_ABBREV( 352, 480, "CAST", 0) -ZONE_ABBREV( 353, 0, "CDST", TZ_AMBIGUOUS) -ZONE_ABBREV( 354, 120, "CEDT", 0) -ZONE_ABBREV( 355, 120, "CEST", 0) +ZONE_ABBREV( 353, 0, "CDST", TZ_AMBIGUOUS|TZ_DST) +ZONE_ABBREV( 354, 120, "CEDT", TZ_DST) +ZONE_ABBREV( 355, 120, "CEST", TZ_DST) ZONE_ABBREV( 356, 480, "CHOT", 0) ZONE_ABBREV( 357, 600, "ChST", 0) ZONE_ABBREV( 358, 600, "CHUT", 0) ZONE_ABBREV( 359, -300, "CIST", 0) -ZONE_ABBREV( 360, -180, "CLDT", 0) +ZONE_ABBREV( 360, -180, "CLDT", TZ_DST) ZONE_ABBREV( 361, 0, "CLST", TZ_AMBIGUOUS) ZONE_ABBREV( 368, 420, "DAVT", 0) ZONE_ABBREV( 369, 600, "DDUT", 0) -ZONE_ABBREV( 376, -300, "EADT", 0) +ZONE_ABBREV( 376, -300, "EADT", TZ_DST) ZONE_ABBREV( 377, -300, "EAST", 0) -ZONE_ABBREV( 378, 120, "ECST", 0) -ZONE_ABBREV( 379, 0, "EDST", TZ_AMBIGUOUS) -ZONE_ABBREV( 380, 180, "EEDT", 0) -ZONE_ABBREV( 381, 180, "EEST", 0) -ZONE_ABBREV( 382, 0, "EGST", 0) -ZONE_ABBREV( 384, 780, "FJDT", 0) -ZONE_ABBREV( 385, 780, "FJST", 0) -ZONE_ABBREV( 386, -180, "FKDT", 0) -ZONE_ABBREV( 387, 0, "FKST", TZ_AMBIGUOUS) +ZONE_ABBREV( 378, 120, "ECST", TZ_DST) +ZONE_ABBREV( 379, 0, "EDST", TZ_AMBIGUOUS|TZ_DST) +ZONE_ABBREV( 380, 180, "EEDT", TZ_DST) +ZONE_ABBREV( 381, 180, "EEST", TZ_DST) +ZONE_ABBREV( 382, 0, "EGST", TZ_DST) +ZONE_ABBREV( 384, 780, "FJDT", TZ_DST) +ZONE_ABBREV( 385, 780, "FJST", TZ_DST) +ZONE_ABBREV( 386, -180, "FKDT", TZ_DST) +ZONE_ABBREV( 387, 0, "FKST", TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 392, -360, "GALT", 0) ZONE_ABBREV( 393, -540, "GAMT", 0) ZONE_ABBREV( 394, 720, "GILT", 0) -ZONE_ABBREV( 400, -540, "HADT", 0) +ZONE_ABBREV( 400, -540, "HADT", TZ_DST) ZONE_ABBREV( 401, -600, "HAST", 0) ZONE_ABBREV( 402, 420, "HOVT", 0) -ZONE_ABBREV( 408, 270, "IRDT", 0) +ZONE_ABBREV( 408, 270, "IRDT", TZ_DST) ZONE_ABBREV( 409, 480, "IRKT", 0) -ZONE_ABBREV( 410, 0, "IRST", TZ_AMBIGUOUS) +ZONE_ABBREV( 410, 0, "IRST", TZ_AMBIGUOUS|TZ_DST) ZONE_ABBREV( 416, 660, "KOST", 0) ZONE_ABBREV( 417, 420, "KRAT", 0) ZONE_ABBREV( 418, 240, "KUYT", 0) -ZONE_ABBREV( 424, 660, "LHDT", 0) +ZONE_ABBREV( 424, 660, "LHDT", TZ_DST) ZONE_ABBREV( 425, 630, "LHST", 0) ZONE_ABBREV( 426, 840, "LINT", 0) ZONE_ABBREV( 432, 600, "MAGT", 0) ZONE_ABBREV( 433, -510, "MART", 0) ZONE_ABBREV( 434, 300, "MAWT", 0) -ZONE_ABBREV( 435, -360, "MDST", 0) +ZONE_ABBREV( 435, -360, "MDST", TZ_DST) ZONE_ABBREV( 436, 120, "MESZ", 0) -ZONE_ABBREV( 440, 720, "NFDT", 0) +ZONE_ABBREV( 440, 720, "NFDT", TZ_DST) ZONE_ABBREV( 441, 360, "NOVT", 0) -ZONE_ABBREV( 442, 780, "NZDT", 0) +ZONE_ABBREV( 442, 780, "NZDT", TZ_DST) ZONE_ABBREV( 443, 720, "NZST", 0) ZONE_ABBREV( 448, 180, "OESZ", 0) ZONE_ABBREV( 449, 360, "OMST", 0) ZONE_ABBREV( 450, 300, "ORAT", 0) -ZONE_ABBREV( 456, -420, "PDST", 0) +ZONE_ABBREV( 456, -420, "PDST", TZ_DST) ZONE_ABBREV( 457, 720, "PETT", 0) ZONE_ABBREV( 458, 780, "PHOT", 0) -ZONE_ABBREV( 459, -120, "PMDT", 0) +ZONE_ABBREV( 459, -120, "PMDT", TZ_DST) ZONE_ABBREV( 460, -180, "PMST", 0) ZONE_ABBREV( 461, 660, "PONT", 0) ZONE_ABBREV( 462, 0, "PYST", TZ_AMBIGUOUS) @@ -239,56 +239,56 @@ ZONE_ABBREV( 482, 120, "SAST", 0) ZONE_ABBREV( 483, 660, "SRET", 0) ZONE_ABBREV( 484, 180, "SYOT", 0) ZONE_ABBREV( 488, -600, "TAHT", 0) -ZONE_ABBREV( 489, 840, "TOST", 0) +ZONE_ABBREV( 489, 840, "TOST", TZ_DST) ZONE_ABBREV( 496, 480, "ULAT", 0) -ZONE_ABBREV( 497, -120, "UYST", 0) +ZONE_ABBREV( 497, -120, "UYST", TZ_DST) ZONE_ABBREV( 504, 600, "VLAT", 0) ZONE_ABBREV( 505, 360, "VOST", 0) ZONE_ABBREV( 512, 720, "WAKT", 0) -ZONE_ABBREV( 513, 120, "WAST", 0) -ZONE_ABBREV( 514, 60, "WEDT", 0) -ZONE_ABBREV( 515, 60, "WEST", 0) +ZONE_ABBREV( 513, 120, "WAST", TZ_DST) +ZONE_ABBREV( 514, 60, "WEDT", TZ_DST) +ZONE_ABBREV( 515, 60, "WEST", TZ_DST) ZONE_ABBREV( 516, 60, "WESZ", 0) -ZONE_ABBREV( 517, -120, "WGST", 0) +ZONE_ABBREV( 517, -120, "WGST", TZ_DST) ZONE_ABBREV( 518, 480, "WITA", 0) ZONE_ABBREV( 520, 540, "YAKT", 0) ZONE_ABBREV( 521, 600, "YAPT", 0) ZONE_ABBREV( 522, 300, "YEKT", 0) ZONE_ABBREV( 528, 525, "ACWST", 0) -ZONE_ABBREV( 529, 720, "ANAST", 0) -ZONE_ABBREV( 530, 0, "AZODT", 0) -ZONE_ABBREV( 531, 0, "AZOST", TZ_AMBIGUOUS) -ZONE_ABBREV( 536, 825, "CHADT", 0) +ZONE_ABBREV( 529, 720, "ANAST", TZ_DST) +ZONE_ABBREV( 530, 0, "AZODT", TZ_DST) +ZONE_ABBREV( 531, 0, "AZOST", TZ_AMBIGUOUS|TZ_DST) +ZONE_ABBREV( 536, 825, "CHADT", TZ_DST) ZONE_ABBREV( 537, 765, "CHAST", 0) -ZONE_ABBREV( 538, 540, "CHODT", 0) -ZONE_ABBREV( 539, 540, "CHOST", 0) -ZONE_ABBREV( 540, -240, "CIDST", 0) -ZONE_ABBREV( 544, -300, "EASST", 0) +ZONE_ABBREV( 538, 540, "CHODT", TZ_DST) +ZONE_ABBREV( 539, 540, "CHOST", TZ_DST) +ZONE_ABBREV( 540, -240, "CIDST", TZ_DST) +ZONE_ABBREV( 544, -300, "EASST", TZ_DST) ZONE_ABBREV( 545, 660, "EFATE", 0) -ZONE_ABBREV( 552, 480, "HOVDT", 0) -ZONE_ABBREV( 553, 480, "HOVST", 0) -ZONE_ABBREV( 560, 540, "IRKST", 0) -ZONE_ABBREV( 568, 480, "KRAST", 0) -ZONE_ABBREV( 576, 720, "MAGST", 0) -ZONE_ABBREV( 584, -300, "NACDT", 0) +ZONE_ABBREV( 552, 480, "HOVDT", TZ_DST) +ZONE_ABBREV( 553, 480, "HOVST", TZ_DST) +ZONE_ABBREV( 560, 540, "IRKST", TZ_DST) +ZONE_ABBREV( 568, 480, "KRAST", TZ_DST) +ZONE_ABBREV( 576, 720, "MAGST", TZ_DST) +ZONE_ABBREV( 584, -300, "NACDT", TZ_DST) ZONE_ABBREV( 585, -360, "NACST", 0) -ZONE_ABBREV( 586, -240, "NAEDT", 0) +ZONE_ABBREV( 586, -240, "NAEDT", TZ_DST) ZONE_ABBREV( 587, -300, "NAEST", 0) -ZONE_ABBREV( 588, -360, "NAMDT", 0) +ZONE_ABBREV( 588, -360, "NAMDT", TZ_DST) ZONE_ABBREV( 589, -420, "NAMST", 0) -ZONE_ABBREV( 590, -420, "NAPDT", 0) +ZONE_ABBREV( 590, -420, "NAPDT", TZ_DST) ZONE_ABBREV( 591, -480, "NAPST", 0) -ZONE_ABBREV( 592, 420, "NOVST", 0) -ZONE_ABBREV( 600, 420, "OMSST", 0) -ZONE_ABBREV( 608, 720, "PETST", 0) -ZONE_ABBREV( 616, 240, "SAMST", 0) -ZONE_ABBREV( 624, 540, "ULAST", 0) -ZONE_ABBREV( 632, 660, "VLAST", 0) -ZONE_ABBREV( 640, -180, "WARST", 0) -ZONE_ABBREV( 648, 600, "YAKST", 0) -ZONE_ABBREV( 649, 360, "YEKST", 0) -ZONE_ABBREV( 656, 540, "CHODST", 0) -ZONE_ABBREV( 664, 480, "HOVDST", 0) +ZONE_ABBREV( 592, 420, "NOVST", TZ_DST) +ZONE_ABBREV( 600, 420, "OMSST", TZ_DST) +ZONE_ABBREV( 608, 720, "PETST", TZ_DST) +ZONE_ABBREV( 616, 240, "SAMST", TZ_DST) +ZONE_ABBREV( 624, 540, "ULAST", TZ_DST) +ZONE_ABBREV( 632, 660, "VLAST", TZ_DST) +ZONE_ABBREV( 640, -180, "WARST", TZ_DST) +ZONE_ABBREV( 648, 600, "YAKST", TZ_DST) +ZONE_ABBREV( 649, 360, "YEKST", TZ_DST) +ZONE_ABBREV( 656, 540, "CHODST", TZ_DST) +ZONE_ABBREV( 664, 480, "HOVDST", TZ_DST) ZONE_UNIQUE( 672, "Africa/Abidjan") ZONE_UNIQUE( 673, "Africa/Algiers") ZONE_UNIQUE( 674, "Africa/Bissau") diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index f5c53932de..7a44fb02d2 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -77,6 +77,7 @@ size_t tnt_datetime_strptime(struct datetime *date, const char *buf, void tnt_datetime_now(struct datetime *now); bool tnt_datetime_totable(const struct datetime *date, struct interval *out); +bool tnt_datetime_isdst(const struct datetime *date); /* Tarantool interval support functions */ size_t tnt_interval_to_string(const struct interval *, char *, ssize_t); @@ -446,6 +447,11 @@ local function epoch_from_dt(dt) return (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY end +-- Use Olson facilities to determine whether local time in obj is DST +local function datetime_isdst(obj) + return builtin.tnt_datetime_isdst(obj) +end + --[[ Parse timezone name similar way as datetime_parse_full parse full literal. @@ -953,7 +959,7 @@ local function datetime_totable(self) hour = tmp_ival.hour, min = tmp_ival.min, sec = tmp_ival.sec, - isdst = false, + isdst = datetime_isdst(self), nsec = self.nsec, tzoffset = self.tzoffset, } @@ -1163,7 +1169,7 @@ local datetime_index_fields = { sec = function(self) return self.epoch % 60 end, usec = function(self) return self.nsec / 1e3 end, msec = function(self) return self.nsec / 1e6 end, - isdst = function(_) return false end, + isdst = function(self) return datetime_isdst(self) end, tz = function(self) return self:format('%Z') end, } diff --git a/src/lua/tnt_datetime.c b/src/lua/tnt_datetime.c index f243d8e3c5..e948057c8a 100644 --- a/src/lua/tnt_datetime.c +++ b/src/lua/tnt_datetime.c @@ -96,3 +96,9 @@ tnt_interval_unpack(const char **data, struct interval *itv) { return interval_unpack(data, itv); } + +bool +tnt_datetime_isdst(const struct datetime *date) +{ + return datetime_isdst(date); +} diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index 08988cf84c..dca81104cd 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -11,7 +11,7 @@ local ffi = require('ffi') --]] if jit.arch == 'arm64' then jit.off() end -test:plan(37) +test:plan(38) -- minimum supported date - -5879610-06-22 local MIN_DATE_YEAR = -5879610 @@ -676,6 +676,59 @@ test:test("Parsing of timezone names (errors)", function(test) end end) +test:test("Daylight saving checks", function (test) + --[[ + Check various dates in `Europe/Moscow` timezone for their + proper daylight saving settings. + + Tzdata defines these rules for `Europe/Moscow` time-zone: +``` +Zone Europe/Moscow 2:30:17 - LMT 1880 + 2:30:17 - MMT 1916 Jul 3 # Moscow Mean Time + 2:31:19 Russia %s 1919 Jul 1 0:00u + 3:00 Russia %s 1921 10 + 3:00 Russia Europe/Moscow/Europe/Moscow 1922 10 + 2:00 - EET 1930 Jun 21 + 3:00 Russia Europe/Moscow/Europe/Moscow 1991 03 31 2:00s + 2:00 Russia EE%sT 1992 Jan 19 2:00s + 3:00 Russia Europe/Moscow/Europe/Moscow 2011 03 27 2:00s + 4:00 - Europe/Moscow 2014 10 26 2:00s + 3:00 - Europe/Moscow +``` + Either you could see the same table dumped in more or less + human-readable form using `zdump` utility: + + `zdump -c 2004,2022 -v Europe/Moscow` + ]] + test:plan(30) + local moments = { + -- string, isdst?, tzoffset (mins) + {'2004-10-31T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2005-03-27T03:00:00 Europe/Moscow', true, 4 * 60}, + {'2005-10-30T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2006-03-26T03:00:00 Europe/Moscow', true, 4 * 60}, + {'2006-10-29T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2007-03-25T03:00:00 Europe/Moscow', true, 4 * 60}, + {'2007-10-28T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2008-03-30T03:00:00 Europe/Moscow', true, 4 * 60}, + {'2008-10-26T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2009-03-29T03:00:00 Europe/Moscow', true, 4 * 60}, + {'2009-10-25T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2010-03-28T03:00:00 Europe/Moscow', true, 4 * 60}, + {'2010-10-31T02:00:00 Europe/Moscow', false, 3 * 60}, + {'2011-03-27T03:00:00 Europe/Moscow', false, 4 * 60}, + {'2014-10-26T01:00:00 Europe/Moscow', false, 3 * 60}, + } + for _, row in pairs(moments) do + local str, isdst, tzoffset = unpack(row) + local dt = date.parse(str) + test:is(dt.isdst, isdst, + ('%s: isdst = %s'):format(tostring(dt), dt.isdst)) + test:is(dt.tzoffset, tzoffset, + ('%s: tzoffset = %s'):format(tostring(dt), dt.tzoffset)) + end +end) + test:test("Datetime string formatting", function(test) test:plan(10) local t = date.new() -- GitLab