简体   繁体   中英

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?

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.

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};
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM