c++delphitdatetime

How to convert a Pascal TDateTime(double) time to a Unix epoch in C++


I need to convert from a Pascal TDateTime object which is a double value to Unix epoch using c++.

A possible solution is proposed (https://forextester.com/forum/viewtopic.php?f=8&t=1000):

unsigned int UnixStartDate = 25569;

unsigned int DateTimeToUnix(double ConvDate)
{
  return((unsigned int)((ConvDate - UnixStartDate) * 86400.0));
}

However, this conversion code produces errors such as:

TDateTime time value = 37838.001388888886 (05.08.2003 02:00)

which converts to Unix epoch 1060041719 (05.08.2003 00:01:59) which is clearly incorrect.

How is it possible to convert this TDateTime value accurately?


Solution

  • The Delphi/C++Builder RTL has a DateTimeToUnix() function for this exact purpose.

    In a TDateTime, the integral portion is the number of days from December 30 1899, and the fractional portion is the time of day from 00:00:00.000. Using raw math involving more than just whole days can be a little tricky, since floating-point math is inaccurate.

    For instance, 0.001388888886 is not exactly 00:02:00, it is closer to 00:01:59.999. So you are encountering a rounding issue, which is exactly what you have to watch out for. TDateTime has milliseconds precision, and there are 86400000 milliseconds in a day, so .001388888886 is 119999.9997504 milliseconds past 00:00:00.000, which is 00:01:59 if those milliseconds are truncated to 119999, or 00:02:00 if they are rounded up to 120000.

    The RTL stopped using floating-point arithmetic on TDateTime years ago due to subtle precision loss. Modern TDateTime operations do a round-trip through TTimeStamp now to avoid that.

    Since you are trying to do this from outside the RTL, you will need to implement the relevant algorithms in your code. The algorithm you have shown is how the RTL used to convert a TDateTime to a Unix timestamp many years ago, but that is not how it does so anymore. The current algorithm looks more like this now (translated to C++ from the original Pascal):

    #include <cmath>
    
    #define HoursPerDay   24
    #define MinsPerHour   60
    #define SecsPerMin    60
    #define MSecsPerSec   1000
    #define MinsPerDay    (HoursPerDay * MinsPerHour)
    #define SecsPerDay    (MinsPerDay * SecsPerMin)
    #define SecsPerHour   (SecsPerMin * MinsPerHour)
    #define MSecsPerDay   (SecsPerDay * MSecsPerSec)
    
    #define UnixDateDelta 25569 // Days between TDateTime basis (12/31/1899) and Unix time_t basis (1/1/1970)
    #define DateDelta 693594    // Days between 1/1/0001 and 12/31/1899
    
    const float FMSecsPerDay = MSecsPerDay;
    const int IMSecsPerDay = MSecsPerDay;
    
    struct TTimeStamp
    {
        int Time; // Number of milliseconds since midnight
        int Date; // One plus number of days since 1/1/0001
    };
    
    typedef double TDateTime;
    
    TTimeStamp DateTimeToTimeStamp(TDateTime DateTime)
    {
        __int64 LTemp = std::round(DateTime * FMSecsPerDay); // <-- this might require tweaking!
        __int64 LTemp2 = LTemp / IMSecsPerDay;
        TTimeStamp Result;
        Result.Date = DateDelta + LTemp2;
        Result.Time = std::abs(LTemp) % IMSecsPerDay;
        return Result;
    }
    
    __int64 DateTimeToMilliseconds(const TDateTime ADateTime)
    {
        TTimeStamp LTimeStamp = DateTimeToTimeStamp(ADateTime);
        return (__int64(LTimeStamp.Date) * MSecsPerDay) + LTimeStamp.Time;
    }
    
    __int64 SecondsBetween(const TDateTime ANow, const TDateTime AThen)
    {
        return std::abs(DateTimeToMilliseconds(ANow) - DateTimeToMilliseconds(AThen)) / MSecsPerSec;
    }
    
    __int64 DateTimeToUnix(const TDateTime AValue)
    {
        __int64 Result = SecondsBetween(UnixDateDelta, AValue);
        if (AValue < UnixDateDelta)
            Result = -Result;
        return Result;
    }
    

    Note my comment in DateTimeToTimeStamp(). I'm not sure if std::round() produces exactly the same result as Delphi's System::Round() for all values. You will have to experiment with it.