1

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++.

BSMP
  • 4,596
  • 8
  • 33
  • 44
Philip Beck
  • 375
  • 2
  • 10

1 Answers1

1

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;
}
Andre Kampling
  • 5,476
  • 2
  • 20
  • 47
  • Thanks for the detailed answer. However, when I compiled it and passed in my COleDateTime.m_dt (double) the answer that came out was not expected. COleDateTime time(1999, 3, 19, 22, 15, 0); came out as tm_sec 16 int tm_min 43 int tm_hour 4 int tm_mday 26 int tm_mon 3 int tm_year 235 int tm_wday 2 int tm_yday 115 int tm_isdst -1 int – Philip Beck Jul 18 '17 at 15:19
  • What is the doubles value exactly? – Andre Kampling Jul 18 '17 at 15:22
  • Sorry still struggling with SO formatting. std::cout << asctime(fromOADate(&timeVal, 36238.927083333336)); – Philip Beck Jul 18 '17 at 15:24
  • OK and what is the value of the double? – Andre Kampling Jul 18 '17 at 15:25
  • 36238.927083333336 – Philip Beck Jul 18 '17 at 15:26
  • Fixed that problem by adding `ULL` and `LL` suffixes to some variables. Now it is working on a 32 bit machine too if the `long long` type is 64 bit wide which should be the case on most modern compilers. – Andre Kampling Jul 18 '17 at 15:54
  • @PhilipBeck: Hi I extend my answer also I fixed something, so take the new code. I added also one method not using an own implementation but using a standard C function (gmtime_r). Also think of accepting the answer to show a wider community that this works. – Andre Kampling Jul 19 '17 at 06:39
  • Hi Andre In the hours before your first solution yesterday I had arrived at a simplified version of your second solution. tm convertFromOADate(double d) { int passedDays = (int)d; // 1.1.1970 ->18.1.2038 if ((passedDays >= 25569) && (passedDays <= 50423)) { time_t t = static_cast((d - 25569) * 86400); tm s = *gmtime(&t); There's a nice grid of conversions on this page https://www.codeproject.com/Articles/144159/Time-Format-Conversion-Made-Easy I dropped this after seeing your first solution due all of the leap calcs, centuries, etc. – Philip Beck Jul 19 '17 at 07:41
  • @PhilipBeck: You simple solution is just using the integral part of the double the passed days but not the time on that day (fractional part). I'm a little bit confused about what you're saying... Well which solution are you using now? If my answer was helpful consider to upvote or/and accept it. If anybody has the same problem he see the answer but thinks it doesn't work as there are no acceptance. – Andre Kampling Jul 20 '17 at 07:32
  • Thanks Andre, I ultimately used your first solution. – Philip Beck Jul 20 '17 at 09:13