Convert OLE Automation Date (OADate) double to struct tm without using VariantTimeToSystemTime

1.9k views Asked by At

I'm writing a Windows DLL in mostly std C++ (VS2010), which does not use MFC/ATL.

A parent module does use MFC and passes a COleDateTime.m_dt to my DLL, which arrives as a double. I believe this is an OLE Automation Date, also known as OADate.

I want to convert this to any type of standard struct (tm...) that has days, hours, etc without pulling MFC, OLE, etc into my DLL.

This has been asked before (Convert Date/Time (as Double) to struct* tm in C++) however, the answer is always using VariantTimeToSystemTime(), which misses the point of that question - not using MFC / OLE, etc.

VariantTimeToSystemTime's requirements are:

Header - OleAuto.h
Library - OleAut32.lib
DLL - OleAut32.dll

My DLL has basically no dependencies at the moment, so I would prefer not to pull OleAut32.dll in for this one conversion.

The best thing I've found so far has been this C# mono code, which I may convert to C++.

1

There are 1 answers

10
Andre Kampling On BEST ANSWER

I have 2 solutions, the first is working with a function that implements gmtime_r so that this solution will don't use any standard functions. The second solution is using the standard function gmtime_r.

1. First solution: Own implementation of gmtime_r (01-Jan-1601 to 31-Dec-9999):

It will work for dates between 01-Jan-1601 and 31-Dec-9999. I've implemented a fromOADate function which uses the SecondsSinceEpochToDateTime function from this answer on SO wich converts seconds before or after 01-Jan-1970 to a tm structure but works only from 01-Jan-1601 on.

I changed the function from that answer to work also with 32 bit by adding one ULL suffix. That requires that the long long types are 64 bit wide, if that's not the case this solution will not work.

If you need dates before year 1601 you could change the SecondsSinceEpochToDateTime as it is well documentated.
To test different values this online conversion is very nice which also supports unix timestamp and the OADate type.

Full working code and example on ideone:

#include <iostream>
#include <ctime>
#include <cstring>

struct tm* SecondsSinceEpochToDateTime(struct tm* pTm, uint64_t SecondsSinceEpoch)
{
   uint64_t sec;
   unsigned int quadricentennials, centennials, quadrennials, annuals/*1-ennial?*/;
   unsigned int year, leap;
   unsigned int yday, hour, min;
   unsigned int month, mday, wday;
   static const unsigned int daysSinceJan1st[2][13]=
   {
      {0,31,59,90,120,151,181,212,243,273,304,334,365}, // 365 days, non-leap
      {0,31,60,91,121,152,182,213,244,274,305,335,366}  // 366 days, leap
  };
/*
   400 years:

   1st hundred, starting immediately after a leap year that's a multiple of 400:
   n n n l  \
   n n n l   } 24 times
   ...      /
   n n n l /
   n n n n

   2nd hundred:
   n n n l  \
   n n n l   } 24 times
   ...      /
   n n n l /
   n n n n

   3rd hundred:
   n n n l  \
   n n n l   } 24 times
   ...      /
   n n n l /
   n n n n

   4th hundred:
   n n n l  \
   n n n l   } 24 times
   ...      /
   n n n l /
   n n n L <- 97'th leap year every 400 years
*/

   // Re-bias from 1970 to 1601:
   // 1970 - 1601 = 369 = 3*100 + 17*4 + 1 years (incl. 89 leap days) =
   // (3*100*(365+24/100) + 17*4*(365+1/4) + 1*365)*24*3600 seconds
   sec = SecondsSinceEpoch + 11644473600ULL;

   wday = (uint)((sec / 86400 + 1) % 7); // day of week

   // Remove multiples of 400 years (incl. 97 leap days)
   quadricentennials = (uint)(sec / 12622780800ULL); // 400*365.2425*24*3600
   sec %= 12622780800ULL;

   // Remove multiples of 100 years (incl. 24 leap days), can't be more than 3
   // (because multiples of 4*100=400 years (incl. leap days) have been removed)
   centennials = (uint)(sec / 3155673600ULL); // 100*(365+24/100)*24*3600
   if (centennials > 3)
   {
      centennials = 3;
   }
   sec -= centennials * 3155673600ULL;

   // Remove multiples of 4 years (incl. 1 leap day), can't be more than 24
   // (because multiples of 25*4=100 years (incl. leap days) have been removed)
   quadrennials = (uint)(sec / 126230400); // 4*(365+1/4)*24*3600
   if (quadrennials > 24)
   {
      quadrennials = 24;
   }
   sec -= quadrennials * 126230400ULL;

   // Remove multiples of years (incl. 0 leap days), can't be more than 3
   // (because multiples of 4 years (incl. leap days) have been removed)
   annuals = (uint)(sec / 31536000); // 365*24*3600
   if (annuals > 3)
   {
      annuals = 3;
   }
   sec -= annuals * 31536000ULL;

   // Calculate the year and find out if it's leap
   year = 1601 + quadricentennials * 400 + centennials * 100 + quadrennials * 4 + annuals;
   leap = !(year % 4) && (year % 100 || !(year % 400));

   // Calculate the day of the year and the time
   yday = sec / 86400;
   sec %= 86400;
   hour = sec / 3600;
   sec %= 3600;
   min = sec / 60;
   sec %= 60;

   // Calculate the month
   for (mday = month = 1; month < 13; month++)
   {
      if (yday < daysSinceJan1st[leap][month])
      {
         mday += yday - daysSinceJan1st[leap][month - 1];
         break;
      }
   }

   // Fill in C's "struct tm"
   memset(pTm, 0, sizeof(*pTm));
   pTm->tm_sec = sec;          // [0,59]
   pTm->tm_min = min;          // [0,59]
   pTm->tm_hour = hour;        // [0,23]
   pTm->tm_mday = mday;        // [1,31]  (day of month)
   pTm->tm_mon = month - 1;    // [0,11]  (month)
   pTm->tm_year = year - 1900; // 70+     (year since 1900)
   pTm->tm_wday = wday;        // [0,6]   (day since Sunday AKA day of week)
   pTm->tm_yday = yday;        // [0,365] (day since January 1st AKA day of year)
   pTm->tm_isdst = -1;         // daylight saving time flag

   return pTm;
}


struct tm* fromOADate(struct tm* p_Tm, double p_OADate)
{
   static const int64_t OA_UnixTimestamp = -2209161600; /* 30-Dec-1899 */

   if (!(   -109205 <= p_OADate               /* 01-Jan-1601 */
         &&            p_OADate <= 2958465))  /* 31-Dec-9999 */
   {
      throw std::string("OADate must be between 109205 and 2958465!");
   }

   int64_t OADatePassedDays = p_OADate;
   double  OADateDayTime    = p_OADate - OADatePassedDays;
   int64_t OADateSeconds    = OA_UnixTimestamp
                            + OADatePassedDays * 24LL * 3600LL
                            + OADateDayTime * 24.0 * 3600.0;

   return SecondsSinceEpochToDateTime(p_Tm, OADateSeconds);
}


int main()
{
   struct tm timeVal;

   std::cout << asctime(fromOADate(&timeVal, -109205));         /* 01-Jan-1601 00:00:00 */
   std::cout << asctime(fromOADate(&timeVal, 0));               /* 30-Dec-1899 00:00:00 */
   std::cout << asctime(fromOADate(&timeVal, 25569));           /* 01-Jan-1970 00:00:00 */
   std::cout << asctime(fromOADate(&timeVal, 50424.134803241)); /* 19-Jan-2038 03:14:07 */
   std::cout << asctime(fromOADate(&timeVal, 2958465));         /* 31-Dec-9999 00:00:00 */

   return 0;
}

2. Second solution: Using gmtime_r (01-Jan-1970 to 19-Jan-2038/31-Dec-9999 (32/64 bit)):

As already said this solution has not that wide range as the variant from above but just uses a standard function (full working example at ideone):

#include <iostream>
#include <ctime>

struct tm* fromOADate(struct tm* p_Tm, double p_OADate)
{
   static const int64_t OA_UnixTimestamp = -2209161600; /* 30-Dec-1899 */

   if (!(   25569 <= p_OADate              /* 01-Jan-1970 00:00:00 */
         &&          p_OADate <= 2958465)) /* 31-Dec-9999 00:00:00 */
   {
      throw std::string("OADate must be between 25569 and 2958465!");
   }

   time_t OADatePassedDays = p_OADate;
   double OADateDayTime    = p_OADate - OADatePassedDays;
   time_t OADateSeconds    = OA_UnixTimestamp
                           + OADatePassedDays * 24LL * 3600LL
                           + OADateDayTime * 24.0 * 3600.0;

   /* date was greater than 19-Jan-2038 and build is 32 bit */
   if (0 > OADateSeconds)
   {
      throw std::string("OADate must be between 25569 and 50424.134803241!");
   }

   return gmtime_r(&OADateSeconds, p_Tm);
}


int main()
{
   struct tm timeVal;

   std::cout << asctime(fromOADate(&timeVal, 25569));           /* 01-Jan-1970 00:00:00 */
   std::cout << asctime(fromOADate(&timeVal, 50424.134803241)); /* 19-Jan-2038 03:14:07 */

   return 0;
}