1

我需要使用 c++ 从一个双精度值的 Pascal TDateTime 对象转换为 Unix 纪元。

提出了一个可能的解决方案(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));
}

但是,此转换代码会产生错误,例如:

TDateTime 时间值 = 37838.001388888886 (05.08.2003 02:00)

它转换为 Unix 纪元 1060041719 (05.08.2003 00:01:59),这显然是不正确的。

如何准确转换此 TDateTime 值?

4

2 回答 2

5

Delphi/C++Builder RTL 具有DateTimeToUnix()用于此目的的功能。

在 aTDateTime中,整数部分是从 开始的天数December 30 1899,小数部分是从 开始的时间00:00:00.000。使用涉及超过一整天的原始数学可能有点棘手,因为浮点数学是不准确的

例如,0.001388888886不完全是00:02:00,它更接近00:01:59.999。所以你遇到了一个四舍五入的问题,这正是你必须注意的。 TDateTime具有毫秒精度,86400000一天中.001388888886119999.9997504毫秒,过去的毫秒也是如此,也就是说00:00:00.00000:01:59如果这些毫秒被截断为119999,或者00:02:00它们被四舍五入为120000

TDateTime由于细微的精度损失,RTL 几年前就停止使用浮点运算。现代TDateTime操作通过现在进行往返TTimeStamp以避免这种情况。

由于您尝试从 RTL 外部执行此操作,因此您需要在代码中实现相关算法。您展示的算法是多年前RTL用于将 a 转换为 Unix 时间戳的方式,但它不再是这样了。TDateTime当前的算法现在看起来更像这样(从原始 Pascal 转换为 C++):

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

请注意我在 中的评论DateTimeToTimeStamp()。我不确定是否对所有值std::round()产生与 Delphi完全相同的结果。System::Round()你将不得不尝试它。

于 2021-05-20T18:33:28.557 回答
2

这是使用我的免费、开源、仅标头的 C++20 计时预览库(适用于 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';
}

输出:

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

IEEE 64 位双精度数具有足够的精度,可以在此范围内四舍五入到最接近的秒数。直到 2079 年的某个时候,精度都小于 1µs。而且精度将保持在 10µs 以下一千年。

Fwiw,这是反向转换:

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

因此,这样:

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

输出:

37838.001388888886

好往返。

于 2021-05-20T18:57:23.327 回答