1

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?

Andrew
  • 626
  • 6
  • 16
  • Yes in VS2019 using C++. – Andrew May 20 '21 at 17:31
  • I have no way of verifying the value presented by an external program (using Debugger). The value displayed in the external program in GMT format was 02:00 so I used that value here. – Andrew May 20 '21 at 17:47

2 Answers2

5

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • The code is missing the definition for MSecsPerDay. – Andrew May 20 '21 at 18:51
  • There's a typo error in your first define and the std::round requires this trick: https://stackoverflow.com/questions/31464146/rounding-to-nearest-even-number-in-c – Andrew May 20 '21 at 20:22
3

Here's a very simple way to do it using my free, open-source, header-only C++20 chrono preview library (works with C++11/14/17):

#include "date/date.h"
#include <iostream>

date::sys_seconds
convert(double d)
{
    using namespace date;
    using namespace std::chrono;
    using ddays = duration<double, days::period>;

    return round<seconds>(sys_days{1899_y/12/30} + ddays{d});
}

int
main()
{
    using namespace date;
    using namespace std;
    using namespace std::chrono;

    auto tp = convert(37838.001388888886);
    cout << tp << " = " << (tp-sys_seconds{})/1s << '\n';
}

Output:

2003-08-05 00:02:00 = 1060041720

An IEEE 64bit double has plenty of precision to round to the nearest second in this range. The precision is finer than 1µs until some time in the year 2079. And the precision will stay below 10µs for another thousand years.

Fwiw, here's the reverse conversion:

double
convert(date::sys_seconds tp)
{
    using namespace date;
    using namespace std::chrono;
    using ddays = duration<double, days::period>;

    return (tp - sys_days{1899_y/12/30})/ddays{1};
}

So with that, this:

cout << setprecision(17) << convert(convert(37838.001388888886)) << '\n';

outputs:

37838.001388888886

Good round trip.

Update C++20

This can now be done in C++20 (assuming your std::lib vendor has updated <chrono> to C++20).

#include <chrono>
#include <iostream>

std::chrono::sys_seconds
convert(double d)
{
    using namespace std::chrono;
    using ddays = duration<double, days::period>;

    return round<seconds>(sys_days{1899y/12/30} + ddays{d});
}

double
convert(std::chrono::sys_seconds tp)
{
    using namespace std::chrono;
    using ddays = duration<double, days::period>;

    return (tp - sys_days{1899y/12/30})/ddays{1};
}
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • Thanks for your introduction to your brilliant library! – Andrew May 20 '21 at 19:07
  • Had problems compiling it due to a conflict with min and max which have been redefined by an external provider in my project. Looks good though. Thanks for sharing. – Andrew May 21 '21 at 08:35
  • `#define NOMINMAX` before including any header. I believe it is Windows.h that defines these C++-destroying macros. – Howard Hinnant May 21 '21 at 12:33