如何在 C++ 中将 Pascal TDateTime(double) 时间转换为 Unix 纪元
How to convert a Pascal TDateTime(double) time to a Unix epoch in C++
我需要使用 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 值?
Delphi/C++Builder RTL 有一个 DateTimeToUnix()
函数正是为了这个目的。
在TDateTime
中,整数部分是从December 30 1899
开始的天数,小数部分是从00:00:00.000
开始的时间。使用涉及超过一整天的原始数学可能有点棘手,因为 floating-point math is inaccurate.
例如,0.001388888886
不完全是00:02:00
,它更接近00:01:59.999
。所以你遇到了一个舍入问题,这正是你必须注意的。 TDateTime
具有毫秒精度,一天有 86400000
毫秒,因此 .001388888886
比 00:00:00.000
晚 119999.9997504
毫秒,如果 00:01:59
这些毫秒被截断为 119999
,或者如果它们四舍五入为 120000
.
,则为 00:02:00
由于细微的精度损失,RTL 在 TDateTime
年前停止使用浮点运算。现代 TDateTime
操作现在通过 TTimeStamp
进行往返以避免这种情况。
由于您试图从 RTL 外部执行此操作,因此您需要在代码中实现相关算法。您展示的算法是多年前 RTL 使用 将 TDateTime
转换为 Unix 时间戳的方式,但现在不再是这样了。当前的算法现在看起来更像这样(从原始的 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()
produces exactly the same result as Delphi's System::Round()
是否适用于所有值。您将不得不尝试一下。
这是使用我的 free, open-source, header-only C++20 chrono preview library(适用于 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
往返不错。
我需要使用 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 值?
Delphi/C++Builder RTL 有一个 DateTimeToUnix()
函数正是为了这个目的。
在TDateTime
中,整数部分是从December 30 1899
开始的天数,小数部分是从00:00:00.000
开始的时间。使用涉及超过一整天的原始数学可能有点棘手,因为 floating-point math is inaccurate.
例如,0.001388888886
不完全是00:02:00
,它更接近00:01:59.999
。所以你遇到了一个舍入问题,这正是你必须注意的。 TDateTime
具有毫秒精度,一天有 86400000
毫秒,因此 .001388888886
比 00:00:00.000
晚 119999.9997504
毫秒,如果 00:01:59
这些毫秒被截断为 119999
,或者如果它们四舍五入为 120000
.
00:02:00
由于细微的精度损失,RTL 在 TDateTime
年前停止使用浮点运算。现代 TDateTime
操作现在通过 TTimeStamp
进行往返以避免这种情况。
由于您试图从 RTL 外部执行此操作,因此您需要在代码中实现相关算法。您展示的算法是多年前 RTL 使用 将 TDateTime
转换为 Unix 时间戳的方式,但现在不再是这样了。当前的算法现在看起来更像这样(从原始的 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()
produces exactly the same result as Delphi's System::Round()
是否适用于所有值。您将不得不尝试一下。
这是使用我的 free, open-source, header-only C++20 chrono preview library(适用于 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
往返不错。