Browse Source

HPCC-16883 Add time zone support to Std.Date

Signed-off-by: Dan S. Camper <dan.camper@lexisnexisrisk.com>
Dan S. Camper 7 years ago
parent
commit
82e9d3aa85
2 changed files with 848 additions and 1 deletions
  1. 786 1
      ecllibrary/std/Date.ecl
  2. 62 0
      ecllibrary/teststd/Date/TestDate.ecl

+ 786 - 1
ecllibrary/std/Date.ecl

@@ -1354,4 +1354,789 @@ EXPORT DateTime_rec CreateDateTimeFromSeconds(Seconds_t seconds) := TRANSFORM
     SELF.second := timeParts.second;
 END;
 
-END; // Module
+//------------------------------------------------------------------------------
+// Time Zone Module
+//------------------------------------------------------------------------------
+
+EXPORT TimeZone := MODULE, FORWARD
+
+/**
+ * Record definition for exported time zone information
+ */
+
+EXPORT TZDataLayout := RECORD
+    STRING5         tzAbbrev;       // Time zone abbreviation; always uppercase; may be duplicated between records
+    INTEGER4        secondsOffset;  // Number of seconds east (positive) or west (negative) of UTC
+    SET OF STRING15 locations;      // Names of locations that use the given time zone abbreviation
+END;
+
+/**
+ * Hardcoded time zone definitions; a general description of each time zone
+ * is included as a line comment.  This information was collected from
+ * https://www.timeanddate.com/time/zones/ with one modification (see below).
+ *
+ * The IST abbreviation can indicate three different time zones:
+ *      India Standard Time
+ *      Irish Standard Time
+ *      Israel Standard Time
+ *
+ * Unfortunately, two of those IST time zones lie in the same location:  ASIA.
+ * That makes it impossible to differentiate between them, and they have very
+ * different offsets.  As a consequence, locations for Israel Standard Time and
+ * Israel Daylight Time have been changed from ASIA to ISRAEL.
+ */
+
+EXPORT TZ_DATA := DATASET
+    (
+        [
+            {'A', 3600, ['MILITARY']}, // Alpha Time Zone
+            {'ACDT', 37800, ['AUSTRALIA']}, // Australian Central Daylight Time
+            {'ACST', 34200, ['AUSTRALIA']}, // Australian Central Standard Time
+            {'ACT', -18000, ['SOUTH AMERICA']}, // Acre Time
+            {'ACT', 34200, ['AUSTRALIA']}, // Australian Central Time
+            {'ACWST', 31500, ['AUSTRALIA']}, // Australian Central Western Standard Time
+            {'ADT', 10800, ['ASIA']}, // Arabia Daylight Time
+            {'ADT', -10800, ['NORTH AMERICA', 'ATLANTIC']}, // Atlantic Daylight Time
+            {'AEDT', 39600, ['AUSTRALIA']}, // Australian Eastern Daylight Time
+            {'AEST', 36000, ['AUSTRALIA']}, // Australian Eastern Standard Time
+            {'AET', 36000, ['AUSTRALIA']}, // Australian Eastern Time
+            {'AFT', 16200, ['ASIA']}, // Afghanistan Time
+            {'AKDT', -28800, ['NORTH AMERICA']}, // Alaska Daylight Time
+            {'AKST', -32400, ['NORTH AMERICA']}, // Alaska Standard Time
+            {'ALMT', 21600, ['ASIA']}, // Alma-Ata Time
+            {'AMST', -10800, ['SOUTH AMERICA']}, // Amazon Summer Time
+            {'AMST', 18000, ['ASIA']}, // Armenia Summer Time
+            {'AMT', -14400, ['SOUTH AMERICA']}, // Amazon Time
+            {'AMT', 14400, ['ASIA']}, // Armenia Time
+            {'ANAST', 43200, ['ASIA']}, // Anadyr Summer Time
+            {'ANAT', 43200, ['ASIA']}, // Anadyr Time
+            {'AOE', -43200, ['PACIFIC']}, // Anywhere on Earth
+            {'AQTT', 18000, ['ASIA']}, // Aqtobe Time
+            {'ART', -10800, ['ANTARCTICA', 'SOUTH AMERICA']}, // Argentina Time
+            {'AST', 7200, ['ASIA']}, // Arabia Standard Time
+            {'AST', -14400, ['NORTH AMERICA', 'ATLANTIC' , 'CARIBBEAN']}, // Atlantic Standard Time
+            {'AT', -14400, ['NORTH AMERICA', 'ATLANTIC', 'CARIBBEAN']}, // Atlantic Time
+            {'AWDT', 32400, ['AUSTRALIA']}, // Australian Western Daylight Time
+            {'AWST', 28800, ['AUSTRALIA']}, // Australian Western Standard Time
+            {'AZOST', 0, ['ATLANTIC']}, // Azores Summer Time
+            {'AZOT', -3600, ['ATLANTIC']}, // Azores Time
+            {'AZST', 18000, ['ASIA']}, // Azerbaijan Summer Time
+            {'AZT', 14400, ['ASIA']}, // Azerbaijan Time
+            {'B', 7200, ['MILITARY']}, // Bravo Time Zone
+            {'BNT', 28800, ['ASIA']}, // Brunei Darussalam Time
+            {'BOT', -14400, ['SOUTH AMERICA']}, // Bolivia Time
+            {'BRST', -7200, ['SOUTH AMERICA']}, // Brazil Summer Time
+            {'BRT', -10800, ['SOUTH AMERICA']}, // Brazil Time
+            {'BST', 21600, ['ASIA']}, // Bangladesh Standard Time
+            {'BST', 39600, ['PACIFIC']}, // Bougainville Standard Time
+            {'BST', 3600, ['EUROPE']}, // British Summer Time
+            {'BTT', 21600, ['ASIA']}, // Bhutan Time
+            {'C', 10800, ['MILITARY']}, // Charlie Time Zone
+            {'CAST', 28800, ['ANTARCTICA']}, // Casey Time
+            {'CAT', 7200, ['AFRICA']}, // Central Africa Time
+            {'CCT', 23400, ['INDIAN OCEAN']}, // Cocos Islands Time
+            {'CDT', -18000, ['NORTH AMERICA']}, // Central Daylight Time
+            {'CDT', -14400, ['CARIBBEAN']}, // Cuba Daylight Time
+            {'CEST', 7200, ['EUROPE', 'ANTARCTICA']}, // Central European Summer Time
+            {'CET', 3600, ['EUROPE', 'AFRICA']}, // Central European Time
+            {'CHADT', 49500, ['PACIFIC']}, // Chatham Island Daylight Time
+            {'CHAST', 45900, ['PACIFIC']}, // Chatham Island Standard Time
+            {'CHOST', 32400, ['ASIA']}, // Choibalsan Summer Time
+            {'CHOT', 28800, ['ASIA']}, // Choibalsan Time
+            {'ChST', 36000, ['PACIFIC']}, // Chamorro Standard Time
+            {'CHUT', 36000, ['PACIFIC']}, // Chuuk Time
+            {'CIDST', -14400, ['CARIBBEAN']}, // Cayman Islands Daylight Saving Time
+            {'CIST', -18000, ['CARIBBEAN']}, // Cayman Islands Standard Time
+            {'CKT', -36000, ['PACIFIC']}, // Cook Island Time
+            {'CLST', -10800, ['SOUTH AMERICA', 'ANTARCTICA']}, // Chile Summer Time
+            {'CLT', -14400, ['SOUTH AMERICA', 'ANTARCTICA']}, // Chile Standard Time
+            {'COT', -18000, ['SOUTH AMERICA']}, // Colombia Time
+            {'CST', -21600, ['NORTH AMERICA', 'CENTRAL AMERICA']}, // Central Standard Time
+            {'CST', 28800, ['ASIA']}, // China Standard Time
+            {'CST', -18000, ['CARIBBEAN']}, // Cuba Standard Time
+            {'CT', -21600, ['NORTH AMERICA', 'CENTRAL AMERICA']}, // Central Time
+            {'CVT', -3600, ['AFRICA']}, // Cape Verde Time
+            {'CXT', 25200, ['AUSTRALIA']}, // Christmas Island Time
+            {'D', 14400, ['MILITARY']}, // Delta Time Zone
+            {'DAVT', 25200, ['ANTARCTICA']}, // Davis Time
+            {'DDUT', 36000, ['ANTARCTICA']}, // Dumont-d'Urville Time
+            {'E', 18000, ['MILITARY']}, // Echo Time Zone
+            {'EASST', -18000, ['PACIFIC']}, // Easter Island Summer Time
+            {'EAST', -21600, ['PACIFIC']}, // Easter Island Standard Time
+            {'EAT', 10800, ['AFRICA', 'INDIAN OCEAN']}, // Eastern Africa Time
+            {'ECT', -18000, ['SOUTH AMERICA']}, // Ecuador Time
+            {'EDT', -14400, ['NORTH AMERICA', 'CARIBBEAN']}, // Eastern Daylight Time
+            {'EEST', 10800, ['EUROPE', 'ASIA']}, // Eastern European Summer Time
+            {'EET', 7200, ['EUROPE', 'ASIA', 'AFRICA']}, // Eastern European Time
+            {'EGST', 0, ['NORTH AMERICA']}, // Eastern Greenland Summer Time
+            {'EGT', -3600, ['NORTH AMERICA']}, // East Greenland Time
+            {'EST', -18000, ['NORTH AMERICA', 'CARIBBEAN', 'CENTRAL AMERICA']}, // Eastern Standard Time
+            {'ET', -18000, ['NORTH AMERICA', 'CARIBBEAN', 'CENTRAL AMERICA']}, // Eastern Time
+            {'F', 21600, ['MILITARY']}, // Foxtrot Time Zone
+            {'FET', 10800, ['EUROPE']}, // Further-Eastern European Time
+            {'FJST', 46800, ['PACIFIC']}, // Fiji Summer Time
+            {'FJT', 43200, ['PACIFIC']}, // Fiji Time
+            {'FKST', -10800, ['SOUTH AMERICA']}, // Falkland Islands Summer Time
+            {'FKT', -14400, ['SOUTH AMERICA']}, // Falkland Island Time
+            {'FNT', -7200, ['SOUTH AMERICA']}, // Fernando de Noronha Time
+            {'G', 25200, ['MILITARY']}, // Golf Time Zone
+            {'GALT', -21600, ['PACIFIC']}, // Galapagos Time
+            {'GAMT', -32400, ['PACIFIC']}, // Gambier Time
+            {'GET', 14400, ['ASIA']}, // Georgia Standard Time
+            {'GFT', -10800, ['SOUTH AMERICA']}, // French Guiana Time
+            {'GILT', 43200, ['PACIFIC']}, // Gilbert Island Time
+            {'GMT', 0, ['EUROPE', 'AFRICA', 'NORTH AMERICA', 'ANTARCTICA']}, // Greenwich Mean Time
+            {'GST', 14400, ['ASIA']}, // Gulf Standard Time
+            {'GST', -7200, ['SOUTH AMERICA']}, // South Georgia Time
+            {'GYT', -14400, ['SOUTH AMERICA']}, // Guyana Time
+            {'H', 28800, ['MILITARY']}, // Hotel Time Zone
+            {'HADT', -32400, ['NORTH AMERICA']}, // Hawaii-Aleutian Daylight Time
+            {'HAST', -36000, ['NORTH AMERICA', 'PACIFIC']}, // Hawaii-Aleutian Standard Time
+            {'HKT', 28800, ['ASIA']}, // Hong Kong Time
+            {'HOVST', 28800, ['ASIA']}, // Hovd Summer Time
+            {'HOVT', 25200, ['ASIA']}, // Hovd Time
+            {'I', 32400, ['MILITARY']}, // India Time Zone
+            {'ICT', 25200, ['ASIA']}, // Indochina Time
+            {'IDT', 10800, ['ISRAEL']}, // Israel Daylight Time; location was ASIA
+            {'IOT', 21600, ['INDIAN OCEAN']}, // Indian Chagos Time
+            {'IRDT', 16200, ['ASIA']}, // Iran Daylight Time
+            {'IRKST', 32400, ['ASIA']}, // Irkutsk Summer Time
+            {'IRKT', 28800, ['ASIA']}, // Irkutsk Time
+            {'IRST', 12600, ['ASIA']}, // Iran Standard Time
+            {'IST', 19800, ['ASIA']}, // India Standard Time
+            {'IST', 3600, ['EUROPE']}, // Irish Standard Time
+            {'IST', 7200, ['ISRAEL']}, // Israel Standard Time; location was ASIA
+            {'JST', 32400, ['ASIA']}, // Japan Standard Time
+            {'K', 36000, ['MILITARY']}, // Kilo Time Zone
+            {'KGT', 21600, ['ASIA']}, // Kyrgyzstan Time
+            {'KOST', 39600, ['PACIFIC']}, // Kosrae Time
+            {'KRAST', 28800, ['ASIA']}, // Krasnoyarsk Summer Time
+            {'KRAT', 25200, ['ASIA']}, // Krasnoyarsk Time
+            {'KST', 32400, ['ASIA']}, // Korea Standard Time
+            {'KUYT', 14400, ['EUROPE']}, // Kuybyshev Time
+            {'L', 39600, ['MILITARY']}, // Lima Time Zone
+            {'LHDT', 39600, ['AUSTRALIA']}, // Lord Howe Daylight Time
+            {'LHST', 37800, ['AUSTRALIA']}, // Lord Howe Standard Time
+            {'LINT', 50400, ['PACIFIC']}, // Line Islands Time
+            {'M', 43200, ['MILITARY']}, // Mike Time Zone
+            {'MAGST', 43200, ['ASIA']}, // Magadan Summer Time
+            {'MAGT', 39600, ['ASIA']}, // Magadan Time
+            {'MART', -34200, ['PACIFIC']}, // Marquesas Time
+            {'MAWT', 18000, ['ANTARCTICA']}, // Mawson Time
+            {'MDT', -21600, ['NORTH AMERICA']}, // Mountain Daylight Time
+            {'MHT', 43200, ['PACIFIC']}, // Marshall Islands Time
+            {'MMT', 23400, ['ASIA']}, // Myanmar Time
+            {'MSD', 14400, ['EUROPE']}, // Moscow Daylight Time
+            {'MSK', 10800, ['EUROPE', 'ASIA']}, // Moscow Standard Time
+            {'MST', -25200, ['NORTH AMERICA']}, // Mountain Standard Time
+            {'MT', -25200, ['NORTH AMERICA']}, // Mountain Time
+            {'MUT', 14400, ['AFRICA']}, // Mauritius Time
+            {'MVT', 18000, ['ASIA']}, // Maldives Time
+            {'MYT', 28800, ['ASIA']}, // Malaysia Time
+            {'N', -3600, ['MILITARY']}, // November Time Zone
+            {'NCT', 39600, ['PACIFIC']}, // New Caledonia Time
+            {'NDT', -9000, ['NORTH AMERICA']}, // Newfoundland Daylight Time
+            {'NFT', 39600, ['AUSTRALIA']}, // Norfolk Time
+            {'NOVST', 25200, ['ASIA']}, // Novosibirsk Summer Time
+            {'NOVT', 21600, ['ASIA']}, // Novosibirsk Time
+            {'NPT', 20700, ['ASIA']}, // Nepal Time
+            {'NRT', 43200, ['PACIFIC']}, // Nauru Time
+            {'NST', -12600, ['NORTH AMERICA']}, // Newfoundland Standard Time
+            {'NUT', -39600, ['PACIFIC']}, // Niue Time
+            {'NZDT', 46800, ['PACIFIC', 'ANTARCTICA']}, // New Zealand Daylight Time
+            {'NZST', 43200, ['PACIFIC', 'ANTARCTICA']}, // New Zealand Standard Time
+            {'O', -7200, ['MILITARY']}, // Oscar Time Zone
+            {'OMSST', 25200, ['ASIA']}, // Omsk Summer Time
+            {'OMST', 21600, ['ASIA']}, // Omsk Standard Time
+            {'ORAT', 18000, ['ASIA']}, // Oral Time
+            {'P', -10800, ['MILITARY']}, // Papa Time Zone
+            {'PDT', -25200, ['NORTH AMERICA']}, // Pacific Daylight Time
+            {'PET', -18000, ['SOUTH AMERICA']}, // Peru Time
+            {'PETST', 43200, ['ASIA']}, // Kamchatka Summer Time
+            {'PETT', 43200, ['ASIA']}, // Kamchatka Time
+            {'PGT', 36000, ['PACIFIC']}, // Papua New Guinea Time
+            {'PHOT', 46800, ['PACIFIC']}, // Phoenix Island Time
+            {'PHT', 28800, ['ASIA']}, // Philippine Time
+            {'PKT', 18000, ['ASIA']}, // Pakistan Standard Time
+            {'PMDT', -7200, ['NORTH AMERICA']}, // Pierre & Miquelon Daylight Time
+            {'PMST', -10800, ['NORTH AMERICA']}, // Pierre & Miquelon Standard Time
+            {'PONT', 39600, ['PACIFIC']}, // Pohnpei Standard Time
+            {'PST', -28800, ['NORTH AMERICA']}, // Pacific Standard Time
+            {'PST', -28800, ['PACIFIC']}, // Pitcairn Standard Time
+            {'PT', -28800, ['NORTH AMERICA']}, // Pacific Time
+            {'PWT', 32400, ['PACIFIC']}, // Palau Time
+            {'PYST', -10800, ['SOUTH AMERICA']}, // Paraguay Summer Time
+            {'PYT', -14400, ['SOUTH AMERICA']}, // Paraguay Time
+            {'PYT', 30600, ['ASIA']}, // Pyongyang Time
+            {'Q', -14400, ['MILITARY']}, // Quebec Time Zone
+            {'QYZT', 21600, ['ASIA']}, // Qyzylorda Time
+            {'R', -18000, ['MILITARY']}, // Romeo Time Zone
+            {'RET', 14400, ['AFRICA']}, // Reunion Time
+            {'ROTT', -10800, ['ANTARCTICA']}, // Rothera Time
+            {'S', -21600, ['MILITARY']}, // Sierra Time Zone
+            {'SAKT', 39600, ['ASIA']}, // Sakhalin Time
+            {'SAMT', 14400, ['EUROPE']}, // Samara Time
+            {'SAST', 7200, ['AFRICA']}, // South Africa Standard Time
+            {'SBT', 39600, ['PACIFIC']}, // Solomon Islands Time
+            {'SCT', 14400, ['AFRICA']}, // Seychelles Time
+            {'SGT', 28800, ['ASIA']}, // Singapore Time
+            {'SRET', 39600, ['ASIA']}, // Srednekolymsk Time
+            {'SRT', -10800, ['SOUTH AMERICA']}, // Suriname Time
+            {'SST', -39600, ['PACIFIC']}, // Samoa Standard Time
+            {'SYOT', 10800, ['ANTARCTICA']}, // Syowa Time
+            {'T', -25200, ['MILITARY']}, // Tango Time Zone
+            {'TAHT', -36000, ['PACIFIC']}, // Tahiti Time
+            {'TFT', 18000, ['INDIAN OCEAN']}, // French Southern and Antarctic Time
+            {'TJT', 18000, ['ASIA']}, // Tajikistan Time
+            {'TKT', 46800, ['PACIFIC']}, // Tokelau Time
+            {'TLT', 32400, ['ASIA']}, // East Timor Time
+            {'TMT', 18000, ['ASIA']}, // Turkmenistan Time
+            {'TOST', 50400, ['PACIFIC']}, // Tonga Summer Time
+            {'TOT', 46800, ['PACIFIC']}, // Tonga Time
+            {'TRT', 10800, ['ASIA', 'EUROPE']}, // Turkey Time
+            {'TVT', 43200, ['PACIFIC']}, // Tuvalu Time
+            {'U', -28800, ['MILITARY']}, // Uniform Time Zone
+            {'ULAST', 32400, ['ASIA']}, // Ulaanbaatar Summer Time
+            {'ULAT', 28800, ['ASIA']}, // Ulaanbaatar Time
+            {'UTC', 0, ['WORLDWIDE']}, // Coordinated Universal Time
+            {'UYST', -7200, ['SOUTH AMERICA']}, // Uruguay Summer Time
+            {'UYT', -10800, ['SOUTH AMERICA']}, // Uruguay Time
+            {'UZT', 18000, ['ASIA']}, // Uzbekistan Time
+            {'V', -32400, ['MILITARY']}, // Victor Time Zone
+            {'VET', -14400, ['SOUTH AMERICA']}, // Venezuelan Standard Time
+            {'VLAST', 39600, ['ASIA']}, // Vladivostok Summer Time
+            {'VLAT', 36000, ['ASIA']}, // Vladivostok Time
+            {'VOST', 21600, ['ANTARCTICA']}, // Vostok Time
+            {'VUT', 39600, ['PACIFIC']}, // Vanuatu Time
+            {'W', -36000, ['MILITARY']}, // Whiskey Time Zone
+            {'WAKT', 43200, ['PACIFIC']}, // Wake Time
+            {'WARST', -10800, ['SOUTH AMERICA']}, // Western Argentine Summer Time
+            {'WAST', 7200, ['AFRICA']}, // West Africa Summer Time
+            {'WAT', 3600, ['AFRICA']}, // West Africa Time
+            {'WEST', 3600, ['EUROPE', 'AFRICA']}, // Western European Summer Time
+            {'WET', 0, ['EUROPE', 'AFRICA']}, // Western European Time
+            {'WFT', 43200, ['PACIFIC']}, // Wallis and Futuna Time
+            {'WGST', -7200, ['NORTH AMERICA']}, // Western Greenland Summer Time
+            {'WGT', -10800, ['NORTH AMERICA']}, // West Greenland Time
+            {'WIB', 25200, ['ASIA']}, // Western Indonesian Time
+            {'WIT', 32400, ['ASIA']}, // Eastern Indonesian Time
+            {'WITA', 28800, ['ASIA']}, // Central Indonesian Time
+            {'WST', 50400, ['PACIFIC']}, // West Samoa Time
+            {'WST', 3600, ['AFRICA']}, // Western Sahara Summer Time
+            {'WT', 0, ['AFRICA']}, // Western Sahara Standard Time
+            {'X', -39600, ['MILITARY']}, // X-ray Time Zone
+            {'Y', -43200, ['MILITARY']}, // Yankee Time Zone
+            {'YAKST', 36000, ['ASIA']}, // Yakutsk Summer Time
+            {'YAKT', 32400, ['ASIA']}, // Yakutsk Time
+            {'YAPT', 36000, ['PACIFIC']}, // Yap Time
+            {'YEKST', 21600, ['ASIA']}, // Yekaterinburg Summer Time
+            {'YEKT', 18000, ['ASIA']}, // Yekaterinburg Time
+            {'Z', 0, ['MILITARY']} // Zulu Time Zone
+        ],
+        TZDataLayout
+    );
+
+/**
+ * Return a list of unique time zone abbreviations from the hardcoded dataset.
+ * All abbreviations are in uppercase.
+ *
+ * @return              A new DATASET({STRING5 tzAbbrev}) containing the
+ *                      unique time zone abbreviations.
+ */
+
+EXPORT UniqueTZAbbreviations() := FUNCTION
+    RETURN TABLE(TZ_DATA, {tzAbbrev}, tzAbbrev);
+END;
+
+/**
+ * Return a list of unique location names from the hardcoded dataset.
+ * All names are in uppercase.
+ *
+ * @return              A new DATASET({STRING name}) containing the
+ *                      unique location names.
+ */
+
+EXPORT UniqueTZLocations() := FUNCTION
+    NameRec := {STRING name};
+
+    // Gather all locations as a collection of child datasets
+    collectedNames := PROJECT
+        (
+            TZ_DATA,
+            TRANSFORM
+                (
+                    {
+                        DATASET(NameRec)    names
+                    },
+                    SELF.names := DATASET(LEFT.locations, NameRec)
+                )
+        );
+
+    // Flatten collected names, so there is one name per record
+    flattenedNames := NORMALIZE
+        (
+            collectedNames,
+            LEFT.names,
+            TRANSFORM
+                (
+                    NameRec,
+                    SELF.name := RIGHT.name
+                )
+        );
+
+    // Deduplicate the names
+    ds3 := TABLE(flattenedNames, {name}, name);
+
+    RETURN ds3;
+END;
+
+/**
+ * Finds the time zone records for a given location.
+ *
+ * @param   location        The name of the location to search for; must be a
+ *                          non-empty uppercase string; REQUIRED
+ * @return                  A new DATASET(STRING5 tzAbbrev, INTEGER4 secondsOffset)
+ *                          containing the found records
+ * @see     FindTZData
+ */
+
+EXPORT TZDataForLocation(STRING location) := FUNCTION
+    ResultRec := RECORD
+        STRING5         tzAbbrev;
+        INTEGER4        secondsOffset;
+    END;
+
+    foundRecs := TZ_DATA(location IN locations);
+    foundTrimmed := PROJECT
+        (
+            foundRecs,
+            TRANSFORM
+                (
+                    ResultRec,
+                    SELF := LEFT
+                )
+        );
+
+    RETURN foundTrimmed;
+END;
+
+/**
+ * Finds the time zone records for a given abbreviation and optional location.
+ * A location should be provided as a method of differentiation if the
+ * abbreviation has duplicate entries.
+ *
+ * @param   timeZoneAbbrev  The time zone abbreviation to search for;
+ *                          must be a non-empty uppercase string; REQUIRED
+ * @param   location        The name of the location to search for; if a
+ *                          location is not provided or is an empty string,
+ *                          all records matching only the abbreviation are
+ *                          returned; OPTIONAL, defaults to an empty string
+ * @return                  A new DATASET(TZDataLayout) containing the found
+ *                          records
+ * @see     TZDataForLocation
+ */
+
+EXPORT DATASET(TZDataLayout) FindTZData(STRING5 timeZoneAbbrev, STRING location = '') := FUNCTION
+    RETURN TZ_DATA(tzAbbrev = timeZoneAbbrev AND (location = '' OR location IN locations));
+END;
+
+/**
+ * Compute the offset, in seconds, between two different time zones.  Each
+ * time zone is designated by a required time zone abbreviation and an
+ * optional location name.  The result is the number of seconds (which can be
+ * either positive or negative) that would have to be applied to a time when
+ * traveling from 'fromTimeZoneAbbrev' to 'toTimeZoneAbbrev'.
+ *
+ * Be aware that some time zones explicitly represent daylight savings time, so
+ * it is entirely possible to change not only time zones but DST observance as
+ * well in a single call.
+ *
+ * @param   fromTimeZoneAbbrev  The time zone abbreviation designated as the
+ *                              starting point; must be a non-empty uppercase
+ *                              string; REQUIRED
+ * @param   toTimeZoneAbbrev    The time zone abbreviation designated as the
+ *                              ending point; must be a non-empty uppercase
+ *                              string; REQUIRED
+ * @param   fromLocation        The name of the location that goes along with
+ *                              fromTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching fromTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @param   toLocation          The name of the location that goes along with
+ *                              toTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching toTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @return                      The number of seconds between the two time
+ *                              zones; will return zero if either time zone
+ *                              cannot be found
+ * @see     AdjustTimeTZ
+ */
+
+EXPORT INTEGER4 SecondsBetweenTZ(STRING5 fromTimeZoneAbbrev,
+                                 STRING5 toTimeZoneAbbrev,
+                                 STRING fromLocation = '',
+                                 STRING toLocation = '') := FUNCTION
+    fromTZ := FindTZData(fromTimeZoneAbbrev, fromLocation);
+    toTZ := FindTZData(toTimeZoneAbbrev, toLocation);
+    hasTZInfo := EXISTS(fromTZ) AND EXISTS(toTZ);
+
+    fromSecondsOffset := fromTZ[1].secondsOffset;
+    toSecondsOffset := toTZ[1].secondsOffset;
+
+    RETURN IF
+        (
+            hasTZInfo,
+            toSecondsOffset - fromSecondsOffset,
+            0
+        );
+END;
+
+/**
+ * Adjust a given Time_t time value for another time zone.  Both the given time
+ * and the destination time zone are designated by a required time zone
+ * abbreviation and an optional location name.
+ *
+ * @param   time                The time value to adjust; REQUIRED
+ * @param   fromTimeZoneAbbrev  The time zone abbreviation that the time
+ *                              value is assumed to be within; must be a
+ *                              non-empty uppercase string; REQUIRED
+ * @param   toTimeZoneAbbrev    The time zone abbreviation designated as the
+ *                              ending point; must be a non-empty uppercase
+ *                              string; REQUIRED
+ * @param   fromLocation        The name of the location that goes along with
+ *                              fromTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching fromTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @param   toLocation          The name of the location that goes along with
+ *                              toTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching toTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @return                      The given time value adjusted by the difference
+ *                              between the two given time zones; if either
+ *                              time zone cannot be found then the original
+ *                              time value will be returned unchanged
+ * @see     SecondsBetweenTZ
+ */
+
+EXPORT Time_t AdjustTimeTZ(Time_t time,
+                           STRING5 fromTimeZoneAbbrev,
+                           STRING5 toTimeZoneAbbrev,
+                           STRING fromLocation = '',
+                           STRING toLocation = '') := FUNCTION
+    diff := SecondsBetweenTZ(fromTimeZoneAbbrev, toTimeZoneAbbrev, fromLocation, toLocation);
+    newTime := AdjustTime(time, second_delta := diff);
+
+    RETURN newTime;
+END;
+
+/**
+ * Converts a UTC time to a time designated by a time zone abbreviation and
+ * optional location.
+ *
+ * @param   utcTime             The UTC time value to adjust; REQUIRED
+ * @param   toTimeZoneAbbrev    The time zone abbreviation designated as the
+ *                              ending point; must be a non-empty uppercase
+ *                              string; REQUIRED
+ * @param   toLocation          The name of the location that goes along with
+ *                              toTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching toTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @return                      The given UTC time value adjusted to the time
+ *                              zone defined by toTimeZoneAbbrev and toLocation;
+ *                              if the time zone cannot be found then the
+ *                              original time value will be returned unchanged
+ * @see     AdjustTimeTZ
+ * @see     ToUTCTime
+ */
+
+EXPORT Time_t ToLocalTime(Time_t utcTime,
+                          STRING5 toTimeZoneAbbrev,
+                          STRING toLocation = '') := FUNCTION
+    RETURN AdjustTimeTZ(utcTime, 'UTC', toTimeZoneAbbrev, toLocation := toLocation);
+END;
+
+/**
+ * Converts a local time, defined with a time zone abbreviation and optional
+ * location, to a UTC time.
+ *
+ * @param   localTime           The time value to adjust; REQUIRED
+ * @param   fromTimeZoneAbbrev  The time zone abbreviation that the localTime
+ *                              value is assumed to be within; must be a
+ *                              non-empty uppercase string; REQUIRED
+ * @param   fromLocation        The name of the location that goes along with
+ *                              fromTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching fromTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @return                      The given local time value adjusted to UTC time;
+ *                              if the given time zone cannot be found then the
+ *                              original UTC time value will be returned
+ *                              unchanged
+ * @see     AdjustTimeTZ
+ * @see     ToLocalTime
+ */
+
+EXPORT Time_t ToUTCTime(Time_t localTime,
+                        STRING5 fromTimeZoneAbbrev,
+                        STRING fromLocation = '') := FUNCTION
+    RETURN AdjustTimeTZ(localTime, fromTimeZoneAbbrev, 'UTC', fromLocation := fromLocation);
+END;
+
+/**
+ * Given a dataset that contains a time zone abbreviation and optional location,
+ * this function macro appends four new attributes to the dataset that contain
+ * useful information for translating a time value into another time zone.
+ * This could be useful as an ETL step where time data is made common in
+ * respect to one particular time zone (e.g. UTC).
+ *
+ * The actions within this function macro are conceptually similar to
+ * SecondsBetweenTZ() but applied to an entire dataset, and somewhat more
+ * efficiently.
+ *
+ * Note:  In order for this function macro to execute correctly, the calling
+ * code must import the Std library.
+ *
+ * @param   inFile              The dataset to process; REQUIRED
+ * @param   timeZoneAbbrevField The attribute within inFile that contains
+ *                              the time zone abbreviation to use for matching;
+ *                              the values in this attribute should be in
+ *                              uppercase; this is not a string; REQUIRED
+ * @param   newOffsetField      The attribute that will be appended to inFile
+ *                              and will contain the number of seconds offset
+ *                              from UTC; this is not a string; REQUIRED
+ * @param   fromLocationField   The attribute within inFile that contains the
+ *                              time zone location for the time zone cited by
+ *                              timeZoneAbbrevField; this is not a string;
+ *                              OPTIONAL, defaults to a null value (indicating
+ *                              that there is no time zone location attribute)
+ * @param   toTimeZoneAbbrev    The 'to' time zone abbreviation to use for all
+ *                              calculations, as a string; OPTIONAL, defaults
+ *                              to 'UTC'
+ * @param   toLocation          The name of the location that goes along with
+ *                              toTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching toTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @return                      A new dataset with the same record definition
+ *                              as inFile but with four new attributes added;
+ *                              the new attributes are named based on the name
+ *                              given as the newOffsetField attribute:
+ *                                  INTEGER4    <newOffsetField>            // Offset, in seconds, between original time zone and toTimeZoneAbbrev
+ *                                  BOOLEAN     <newOffsetField>_is_valid   // TRUE if <newOffsetField> contains a valid value
+ *                                  STRING5     <newOffsetField>_tz         // The value of toTimeZoneAbbrev
+ *                                  STRING15    <newOffsetField>_location   // The time zone location for <newOffsetField>_tz
+ *                              If <newOffsetField>_is_valid is FALSE then
+ *                              <newOffsetField> will be zero.
+ * @see     AppendTZAdjustedTime
+ *
+ * Examples:
+ *
+ *   ds := DATASET
+ *      (
+ *          [
+ *              {120000, 'CT'},
+ *              {120000, 'ET'}
+ *          ],
+ *          {Std.Date.Time_t time, STRING tz}
+ *      );
+ *
+ *  utcOffsetDS := Std.Date.TimeZone.AppendTZOffset(ds, tz, seconds_to_utc);
+ *  OUTPUT(utcOffsetDS, NAMED('offset_to_utc_result'));
+ *
+ *  ptOffsetDS := Std.Date.TimeZone.AppendTZOffset
+ *      (
+ *          ds,
+ *          tz,
+ *          seconds_to_pacific_time,
+ *          toTimeZoneAbbrev := 'PT',
+ *          toLocation := 'NORTH AMERICA'
+ *      );
+ *  OUTPUT(ptOffsetDS, NAMED('offset_to_pacific_time_result'));
+ */
+
+EXPORT AppendTZOffset(inFile,
+                      timeZoneAbbrevField,
+                      newOffsetField,
+                      fromLocationField = '',
+                      toTimeZoneAbbrev = '\'UTC\'',
+                      toLocation = '\'\'') := FUNCTIONMACRO
+    // Find the destination time zone information just once
+    #UNIQUENAME(destOffsetDS);
+    LOCAL %destOffsetDS% := Std.Date.TimeZone.FindTZData(toTimeZoneAbbrev, toLocation);
+    #UNIQUENAME(destOffsetFound);
+    LOCAL %destOffsetFound% := EXISTS(%destOffsetDS%);
+    #UNIQUENAME(destLocation);
+    LOCAL %destLocation% := IF(toLocation != '', toLocation, %destOffsetDS%[1].locations[1]);
+    #UNIQUENAME(destOffset);
+    LOCAL %destOffset% := %destOffsetDS%[1].secondsOffset;
+
+    RETURN JOIN
+        (
+            inFile,
+            Std.Date.TimeZone.TZ_DATA,
+            LEFT.timeZoneAbbrevField = RIGHT.tzAbbrev
+                #IF(#TEXT(fromLocationField) != '')
+                    AND LEFT.fromLocationField IN RIGHT.locations
+                #END
+                AND %destOffsetFound%,
+            TRANSFORM
+                (
+                    {
+                        RECORDOF(inFile),
+                        INTEGER4    newOffsetField,
+                        BOOLEAN     #EXPAND(#TEXT(newOffsetField) + '_is_valid'),
+                        STRING5     #EXPAND(#TEXT(newOffsetField) + '_tz'),
+                        STRING15    #EXPAND(#TEXT(newOffsetField) + '_location')
+                    },
+
+                    wasFound := RIGHT.tzAbbrev != '';
+
+                    SELF.newOffsetField := IF(wasFound, %destOffset% - RIGHT.secondsOffset, 0),
+                    SELF.#EXPAND(#TEXT(newOffsetField) + '_is_valid') := wasFound,
+                    SELF.#EXPAND(#TEXT(newOffsetField) + '_tz') := toTimeZoneAbbrev,
+                    SELF.#EXPAND(#TEXT(newOffsetField) + '_location') := %destLocation%,
+                    SELF := LEFT
+                ),
+            LEFT OUTER, LOOKUP
+        );
+ENDMACRO;
+
+/**
+ * Given a dataset that contains a time (in Time_t format), a time zone
+ * abbreviation, and an optional time zone location, this function macro
+ * appends four new attributes to the dataset:  A new Time_t attribute
+ * containing the original time expressed in a different time zone, and three
+ * attributes providing information regarding that destination time zone and
+ * the validity of the translation.  This could be useful as an ETL step where
+ * time data is made common in respect to one particular time zone (e.g. UTC).
+ *
+ * The actions within this function macro are conceptually similar to
+ * AdjustTimeTZ() but applied to an entire dataset, and somewhat more
+ * efficiently.
+ *
+ * Note:  In order for this function macro to execute correctly, the calling
+ * code must import the Std library.
+ *
+ * @param   inFile              The dataset to process; REQUIRED
+ * @param   timeField           The attribute within inFile that contains a
+ *                              time represented in Time_t format; this is not
+ *                              a string; REQUIRED
+ * @param   timeZoneAbbrevField The attribute within inFile that contains
+ *                              the time zone abbreviation to use for matching;
+ *                              the values in this attribute should be in
+ *                              uppercase; this is not a string; REQUIRED
+ * @param   newTimeField        The attribute that will be appended to inFile
+ *                              and will contain the adjusted value of timeField;
+ *                              this is not a string; REQUIRED
+ * @param   fromLocationField   The attribute within inFile that contains the
+ *                              time zone location for the time zone cited by
+ *                              timeZoneAbbrevField; this is not a string;
+ *                              OPTIONAL, defaults to a null value (indicating
+ *                              that there is no time zone location attribute)
+ * @param   toTimeZoneAbbrev    The 'to' time zone abbreviation to use for all
+ *                              calculations, as a string; OPTIONAL, defaults
+ *                              to 'UTC'
+ * @param   toLocation          The name of the location that goes along with
+ *                              toTimeZoneAbbrev; if a location is not
+ *                              provided or is an empty string, the first
+ *                              record matching toTimeZoneAbbrev will be used;
+ *                              OPTIONAL, defaults to an empty string
+ * @return                      A new dataset with the same record definition
+ *                              as inFile but with four new attributes added;
+ *                              the new attributes are named based on the name
+ *                              given as the newOffsetField attribute:
+ *                                  Std.Date.Time_t <newOffsetField>            // Value of timeField expressed in new time zone
+ *                                  BOOLEAN         <newOffsetField>_is_valid   // TRUE if <newOffsetField> contains a valid value
+ *                                  STRING5         <newOffsetField>_tz         // The value of toTimeZoneAbbrev
+ *                                  STRING15        <newOffsetField>_location   // The time zone location for <newOffsetField>_tz
+ *                              If <newOffsetField>_is_valid is FALSE then
+ *                              <newOffsetField> will have the same value as
+ *                              timeField.
+ * @see     AppendTZOffset
+ *
+ * Example:
+ *
+ *   ds := DATASET
+ *      (
+ *          [
+ *              {120000, 'CT'},
+ *              {120000, 'ET'}
+ *          ],
+ *          {Std.Date.Time_t time, STRING tz}
+ *      );
+ *
+ *  utcRewriteDS := Std.Date.TimeZone.AppendTZAdjustedTime(ds, tz, time, utc_time);
+ *  OUTPUT(utcRewriteDS, NAMED('utc_result'));
+ *
+ *  ptRewriteDS := Std.Date.TimeZone.AppendTZAdjustedTime
+ *      (
+ *          ds,
+ *          time,
+ *          tz,
+ *          pacific_time,
+ *          toTimeZoneAbbrev := 'PT',
+ *          toLocation := 'NORTH AMERICA'
+ *      );
+ *  OUTPUT(ptRewriteDS, NAMED('pacific_time_result'));
+ */
+
+EXPORT AppendTZAdjustedTime(inFile,
+                            timeField,
+                            timeZoneAbbrevField,
+                            newTimeField,
+                            fromLocationField = '',
+                            toTimeZoneAbbrev = '\'UTC\'',
+                            toLocation = '\'\'') := FUNCTIONMACRO
+    // Find the destination time zone information just once
+    #UNIQUENAME(destOffsetDS);
+    LOCAL %destOffsetDS% := Std.Date.TimeZone.FindTZData(toTimeZoneAbbrev, toLocation);
+    #UNIQUENAME(destOffsetFound);
+    LOCAL %destOffsetFound% := EXISTS(%destOffsetDS%);
+    #UNIQUENAME(destLocation);
+    LOCAL %destLocation% := IF(toLocation != '', toLocation, %destOffsetDS%[1].locations[1]);
+    #UNIQUENAME(destOffset);
+    LOCAL %destOffset% := %destOffsetDS%[1].secondsOffset;
+
+    RETURN JOIN
+        (
+            inFile,
+            Std.Date.TimeZone.TZ_DATA,
+            LEFT.timeZoneAbbrevField = RIGHT.tzAbbrev
+                #IF(#TEXT(fromLocationField) != '')
+                    AND LEFT.fromLocationField IN RIGHT.locations
+                #END
+                AND %destOffsetFound%,
+            TRANSFORM
+                (
+                    {
+                        RECORDOF(inFile),
+                        Std.Date.Time_t     newTimeField,
+                        BOOLEAN             #EXPAND(#TEXT(newTimeField) + '_is_valid'),
+                        STRING5             #EXPAND(#TEXT(newTimeField) + '_tz'),
+                        STRING15            #EXPAND(#TEXT(newTimeField) + '_location')
+                    },
+
+                    wasFound := RIGHT.tzAbbrev != '';
+
+                    SELF.newTimeField := IF
+                        (
+                            wasFound,
+                            Std.Date.AdjustTime(LEFT.timeField, second_delta := (%destOffset% - RIGHT.secondsOffset)),
+                            LEFT.timeField
+                        ),
+                    SELF.#EXPAND(#TEXT(newTimeField) + '_is_valid') := wasFound,
+                    SELF.#EXPAND(#TEXT(newTimeField) + '_tz') := toTimeZoneAbbrev,
+                    SELF.#EXPAND(#TEXT(newTimeField) + '_location') := %destLocation%,
+                    SELF := LEFT
+                ),
+            LEFT OUTER, LOOKUP
+        );
+ENDMACRO;
+
+END; // TimeZone Module
+
+END; // Date Module

+ 62 - 0
ecllibrary/teststd/Date/TestDate.ecl

@@ -2,6 +2,7 @@
 ## HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems®.  All rights reserved.
 ############################################################################## */
 
+IMPORT Std;
 IMPORT Std.Date;
 
 EXPORT TestDate := MODULE
@@ -21,6 +22,7 @@ EXPORT TestDate := MODULE
   SHARED vTimestamp := Date.CurrentTimestamp(); // UTC
   SHARED vTimestampLocal := Date.CurrentTimestamp(TRUE); // Local
   SHARED vLocalTimeZoneOffset := Date.LocalTimeZoneOffset();
+  SHARED vTZInputSample := NOFOLD(DATASET([{120000, 'CT'}], {Date.Time_t time, STRING tz}));
 
   EXPORT TestConstant := [
     ASSERT(Date.FromDaysSince1900(0) = 19000101, CONST);
@@ -107,6 +109,8 @@ EXPORT TestDate := MODULE
     ASSERT(Date.MonthWeekNumFromDate(20180302, 7) = 1, CONST);  // Start of week = Saturday
     ASSERT(Date.MonthWeekNumFromDate(20180303, 7) = 2, CONST);  // Start of week = Saturday
 
+    ASSERT(EXISTS(Date.TimeZone.TZ_Data), CONST);
+
     ASSERT(TRUE, CONST)
   ];
 
@@ -237,6 +241,64 @@ EXPORT TestDate := MODULE
 
     ASSERT(Date.SecondsFromDateTimeRec(vCreateDateTime) = 917872496);
 
+    ASSERT(COUNT(Date.TimeZone.UniqueTZAbbreviations()) = 227);
+    ASSERT(COUNT(Date.TimeZone.UniqueTZLocations()) = 15);
+    ASSERT(COUNT(Date.TimeZone.TZDataForLocation('NORTH AMERICA')) = 28);
+    // SecondsBetweenTZ for ambiguous abbreviations using first location found
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('EST', 'CST') = -3600);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST') = 41400);
+    // SecondsBetweenTZ for ambiguous abbreviations using explicit locations
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', fromLocation := 'NORTH AMERICA') = 41400);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', fromLocation := 'CENTRAL AMERICA') = 41400);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', fromLocation := 'ASIA') = -9000);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', fromLocation := 'CARIBBEAN') = 37800);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', toLocation := 'ASIA') = 41400);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', toLocation := 'EUROPE') = 25200);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', toLocation := 'ISRAEL') = 28800);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', fromLocation := 'NORTH AMERICA', toLocation := 'ASIA') = 41400);
+    ASSERT(Date.TimeZone.SecondsBetweenTZ('CST', 'IST', fromLocation := 'ASIA', toLocation := 'ISRAEL') = -21600);
+    // AdjustTimeTZ for ambiguous abbreviations using first location found
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'EST', 'CST') = 110000);
+    // AdjustTimeTZ for ambiguous abbreviations using explicit locations
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', fromLocation := 'NORTH AMERICA') = 233000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', fromLocation := 'CENTRAL AMERICA') = 233000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', fromLocation := 'ASIA') = 93000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', fromLocation := 'CARIBBEAN') = 223000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', toLocation := 'ASIA') = 233000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', toLocation := 'EUROPE') = 190000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', toLocation := 'ISRAEL') = 200000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', fromLocation := 'NORTH AMERICA', toLocation := 'ASIA') = 233000);
+    ASSERT(Date.TimeZone.AdjustTimeTZ(120000, 'CST', 'IST', fromLocation := 'ASIA', toLocation := 'ISRAEL') = 60000);
+    // ToLocalTime for ambiguous abbreviations using first location found
+    ASSERT(Date.TimeZone.ToLocalTime(120000, 'IST') = 173000);
+    // ToLocalTime for ambiguous abbreviations using explicit locations
+    ASSERT(Date.TimeZone.ToLocalTime(120000, 'IST', toLocation := 'ASIA') = 173000);
+    ASSERT(Date.TimeZone.ToLocalTime(120000, 'IST', toLocation := 'EUROPE') = 130000);
+    ASSERT(Date.TimeZone.ToLocalTime(120000, 'IST', toLocation := 'ISRAEL') = 140000);
+    // ToUTCTime for ambiguous abbreviations using first location found
+    ASSERT(Date.TimeZone.ToUTCTime(120000, 'IST') = 63000);
+    // ToUTCTime for ambiguous abbreviations using explicit locations
+    ASSERT(Date.TimeZone.ToUTCTime(120000, 'IST', fromLocation := 'ASIA') = 63000);
+    ASSERT(Date.TimeZone.ToUTCTime(120000, 'IST', fromLocation := 'EUROPE') = 110000);
+    ASSERT(Date.TimeZone.ToUTCTime(120000, 'IST', fromLocation := 'ISRAEL') = 100000);
+
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_utc)[1].seconds_to_utc_is_valid = TRUE);
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_utc)[1].seconds_to_utc = 21600);
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_utc)[1].seconds_to_utc_tz = 'UTC');
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_utc)[1].seconds_to_utc_location = 'WORLDWIDE');
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_pt, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].seconds_to_pt_is_valid = TRUE);
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_pt, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].seconds_to_pt = -7200);
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_pt, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].seconds_to_pt_tz = 'PT');
+    ASSERT(Date.TimeZone.AppendTZOffset(vTZInputSample, tz, seconds_to_pt, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].seconds_to_pt_location = 'NORTH AMERICA');
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, utc_time)[1].utc_time_is_valid = TRUE);
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, utc_time)[1].utc_time = 180000);
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, utc_time)[1].utc_time_tz = 'UTC');
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, utc_time)[1].utc_time_location = 'WORLDWIDE');
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, pt_time, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].pt_time_is_valid = TRUE);
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, pt_time, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].pt_time = 100000);
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, pt_time, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].pt_time_tz = 'PT');
+    ASSERT(Date.TimeZone.AppendTZAdjustedTime(vTZInputSample, time, tz, pt_time, toTimeZoneAbbrev := 'PT', toLocation := 'NORTH AMERICA')[1].pt_time_location = 'NORTH AMERICA');
+
     ASSERT(TRUE)
   ];