Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations strongm on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Calendar Appointments to and from UTC

Status
Not open for further replies.

vernpace

Programmer
Feb 22, 2015
209
US
We have requirements to add a Calendar to an existing application. After much deliberation, we decided on using the ctxCalendar ActiveX control from DBI - a bit of a learning curve, but has met expectations.

One of the requirements was to store (when saved) appointment start and end datetimes as UTC and then convert back when the calendar loaded the appointments. The idea is that when users travel with laptops, their calendar appointments will show the correct datetimes no matter the timezone - for this to work properly, users will have to create appointments to their local (before travel) timezones. We are testing the code below (note that the code to update ctxCalendar is not included):

Code:
DECLARE INTEGER GetTimeZoneInformation IN kernel32.dll ; 
         STRING @lpTimeZoneInformation 

#DEFINE TIME_ZONE_ID_UNKNOWN   0
#DEFINE TIME_ZONE_ID_STANDARD  1
#DEFINE TIME_ZONE_ID_DAYLIGHT  2

ltAppBeg = DATETIME(2021, 11, 9, 10, 30)  && Sample Appointment start
ltAppEnd = DATETIME(2021, 11, 9, 12,  0)  && Sample Appointment end

ltUTCBeg = GetUTCtime(ltAppBeg)  && Sample Appointment UTC start stored in database
ltUTCEnd = GetUTCtime(ltAppEnd)  && Sample Appointment UTC end stored in database

ltCalBeg = ltUTCBeg - (GetTimeZone("BIAS", .F.) * 60)  && Sample Appointment UTC start converted from database
ltCalEnd = ltUTCEnd - (GetTimeZone("BIAS", .F.) * 60)  && Sample Appointment UTC end converted from database

?ltAppBeg
?ltAppEnd
?
?ltUTCBeg
?ltUTCEnd
?
?ltCalBeg
?ltCalEnd

FUNCTION GetUTCtime(ttDate AS Datetime) AS Datetime
   RETURN ttDate + (GetTimeZone("BIAS", .F.) * 60)
ENDFUNC

*!*    Returns the Time Zone OffSet Bias or Time Zone Name
*!*    Parameters: tcFunc = "BIAS" (or blank) returns the offset bias in minutes: UTC (Coordinated Universal Time) = Local Time + Offset Bias
*!*                tcFunc = "NAME" returns the time zone name.
*!*                tlDSToffset = Option to include DST in offset bias.

FUNCTION GetTimeZone(tcFunc AS String, tlDSToffset AS Logical) AS Variant
   LOCAL lcTZInfo, lcDesc, liInfo, liBias

   #DEFINE TIME_ZONE_SIZE  172
   lcTZInfo = REPLICATE(CHR(0), TIME_ZONE_SIZE)

   liInfo = GetTimeZoneInformation(@lcTZInfo)
   liBias = Buf2DWord(SUBSTR(lcTZInfo, 1,4)) - IIF(tlDSToffset AND (liInfo = TIME_ZONE_ID_DAYLIGHT), 60, 0)

   IF VARTYPE(tcFunc) = "C" AND UPPER(tcFunc) = "NAME"
      DO CASE
         CASE liInfo = TIME_ZONE_ID_STANDARD
              lcDesc = STRTRAN(SUBSTR(lcTZInfo, 5, 64), CHR(0), "")

         CASE liInfo = TIME_ZONE_ID_DAYLIGHT
              lcDesc = STRTRAN(SUBSTR(lcTZInfo, 89, 64), CHR(0), "")

         CASE liInfo = TIME_ZONE_ID_UNKNOWN
              lcDesc = "Time Zone Unknown"

         OTHERWISE
              lcDesc = "System Error"

      ENDCASE

      RETURN lcDesc

   ENDIF

   RETURN liBias

ENDFUNC

FUNCTION Buf2DWord(tcBuffer AS String)
   RETURN ASC(SUBSTR(tcBuffer, 1,1)) + ;
          BITLSHIFT(ASC(SUBSTR(tcBuffer, 2,1)),  8) + ;
          BITLSHIFT(ASC(SUBSTR(tcBuffer, 3,1)), 16) + ;
          BITLSHIFT(ASC(SUBSTR(tcBuffer, 4,1)), 24)
ENDFUNC

Note that daylight savings time is not factored into the offset bias. This was successfully tested before and after the recent switch in DST. If the DST offset was included, then the conversion from UTC would be inaccurate with a DST switch.

As stated above, this will only work if users create appointments in their local (before travel) timezones. However, users will want to create appointments in different timezones. I'm not quite clear on a solution yet. Will probably have to have a dropdown of all timezones for users to select and then also store the UTC time offset... and then figure out how to covert (with this offset) back to the local (before travel) timezone?? Confusing. Has anyone done anything like this? Any help will be appreciated.
 
There are two Windows functions which you may be interested in:
Code:
DECLARE GetSystemTime IN kernel32.dll STRING @cOut16Bytes
DECLARE GetLocalTime IN kernel32.dll STRING @cOut16Bytes

lcTime = SPACE(16)
*GetLocalTime(@lcTime)
GetSystemTime(@lcTime)
* The offsets obtain the 16-bit values:
* +1 wYear
* +3 wMonth
* +5 wDayOfWeek
* +7 wDay
* +9 wHour
* +11 wMinute
* +13 wSecond
* +15 wMilliseconds

lnYear  = CTOBIN(SUBSTR(lcTime,  1, 2), "2RS")
lnMonth = CTOBIN(SUBSTR(lcTime,  3, 2), "2RS")
lnDow   = CTOBIN(SUBSTR(lcTime,  5, 2), "2RS")
lnDay   = CTOBIN(SUBSTR(lcTime,  7, 2), "2RS")
lnHour  = CTOBIN(SUBSTR(lcTime,  9, 2), "2RS")
lnMin   = CTOBIN(SUBSTR(lcTime, 11, 2), "2RS")
lnSec   = CTOBIN(SUBSTR(lcTime, 13, 2), "2RS")
lnMs    = CTOBIN(SUBSTR(lcTime, 15, 2), "2RS")

? lnYear, lnMonth, lnDay, lnDow, lnHour, lnMin, lnSec, lnMs

Those return times in the local timezone, or in a UTC time. It might be easier to work with.

GetSystemTime()
The SYSTEMTIME structure used for both functions

--
Rick C. Hodgin
 
You can use these functions to convert them to 64-bit unsigned integers that lets you get the difference between your two times (local and system) to know what factor to add / subtract to your times to adjust them. If you always store in UTC, then you can derive the factor at any point:

Code:
SystemTimeToFileTime()
FileTimeToSystemTime()

This is most easily handled in a DLL using native Win32 functions, but it's possible in VFP. If you'd like some source code to create a DLL to handle this for you, let me know.

--
Rick C. Hodgin
 
Here is the source code for one that handles the adjustments. By computing the delta between the current time (in UTC and local time) you get the number of seconds to adjust. It works without regard to parsing the local timezone. And as long as you stored the time in UTC to begin with, you can always display it properly.

If you want to work with something that allows world-wide timezones, then you'll be on a different path.

Code:
CLEAR

lcTimeDll = "C:\temp\time.dll"

DECLARE        time_getLocalTime  IN (lcTimeDll) STRING @cTime16Bytes, INTEGER @nYear, INTEGER @nMonth, INTEGER @nDow, INTEGER @nDay, INTEGER @nHour, INTEGER @nMin, INTEGER @nSec, INTEGER @nMs
DECLARE        time_getSystemTime IN (lcTimeDll) STRING @cTime16Bytes, INTEGER @nYear, INTEGER @nMonth, INTEGER @nDow, INTEGER @nDay, INTEGER @nHour, INTEGER @nMin, INTEGER @nSec, INTEGER @nMs
DECLARE DOUBLE time_getDelta      IN (lcTimeDll) STRING cTime16Bytes_1, STRING cTime16Bytes_2
DECLARE        time_adjustByDelta IN (lcTimeDll) STRING @cTime16Bytes, DOUBLE fSeconds, INTEGER @nYear, INTEGER @nMonth, INTEGER @nDow, INTEGER @nDay, INTEGER @nHour, INTEGER @nMin, INTEGER @nSec, INTEGER @nMs

LOCAL lcLocal, lcSystem
LOCAL lnYear, lnMonth, lnDay, lnDow, lnHour, lnMin, lnSec, lnMs
LOCAL lfDeltaInSeconds

* Get local time
lcLocal = SPACE(16)
STORE 0 TO lnYear, lnMonth, lnDay, lnDow, lnHour, lnMin, lnSec, lnMs
time_getLocalTime(@lcLocal, @lnYear, @lnMonth, @lnDay, @lnDow, @lnHour, @lnMin, @lnSec, @lnMs)
? "Local", lnYear, lnMonth, lnDay, lnDow, lnHour, lnMin, lnSec, lnMs

* Get system time (UTC time)
lcSystem = SPACE(16)
time_getSystemTime(@lcSystem, @lnYear, @lnMonth, @lnDay, @lnDow, @lnHour, @lnMin, @lnSec, @lnMs)
? "System", lnYear, lnMonth, lnDay, lnDow, lnHour, lnMin, lnSec, lnMs

* Compute the delta
lfDeltaInSeconds = time_getDelta(lcSystem, lcLocal)
? "Delta in seconds", lfDeltaInSeconds

* Adjust our system time by the delta
time_adjustByDelta(@lcSystem, lfDeltaInSeconds, @lnYear, @lnMonth, @lnDay, @lnDow, @lnHour, @lnMin, @lnSec, @lnMs)
? "System adjusted to local", lnYear, lnMonth, lnDay, lnDow, lnHour, lnMin, lnSec, lnMs

And attached is the source code and time.dll to test it.

--
Rick C. Hodgin
 
 https://files.engineering.com/getfile.aspx?folder=d79c9685-44f9-4267-8cbf-fb59f0fd8f78&file=time.zip
Actually, I was wrong. Just tested creating calendar appointments with local timezones in Argentina, Moscow, Paris, London, and Singapore (via changing timezone in Windows). When I switched back to Eastern Time, the appointment dates were all correctly showing in local time. This is due to: When a new (or modified) appointment is saved in any timezone, the UTC for ALL appointments (start and end dates) are recalculted and stored. That's it! Our Calendar creates appointments and converts correctly across all timezones.

Rick, thank you for all your help. I should have tested more thoroughly before making the original post
 
Hi Rick,

Just happened to see this thread.
Great information! Thanks a lot (the original post is not mine, though!)

I have already incorporated the GetLocalTime & GetSystemTime Windows functions into my functions library PRG.

Now, if I do something like below in VFP itself, won't that give the same result of "time_getDelta" in the Time.DLL ?
Or, is there any problem in calculating this way?

Code:
*/ 'gettime' is a single function I wrote based on the Windows Functions from your reply above.
*/ The parameters, U=UTC, L=Local.
*/ It returns an object with all values in its properties
*/
lo = gettime('U')  && get the UTC time
utime = DATETIME(lo.year, lo.month, lo.day, lo.hour, lo.min, lo.sec)
lo = gettime('L')
ltime = DATETIME(lo.year, lo.month, lo.day, lo.hour, lo.min, lo.sec)
ldiff = ltime - utime
calculated_local = utime + ldiff

? 'UTC              ', utime
? 'Local            ', ltime
? 'Diff             ', ldiff
? 'Calculated Local ', clocal

Rajesh
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top