从1601年1月1日起,将100纳秒间隔的uint64时间转换为日期时间字符串

Convert uint64 time for number of 100 nanosecond intervals since jan 1st 1601 to date time string

本文关键字:时间 uint64 转换 字符串 日期 1日起 1月 1601年      更新时间:2023-10-16

我有一个开始存储在uint64中的时间值。该值是自1601年1月1日以来的100纳秒间隔数。我知道Windows FILETIME类型使用这种格式。我需要将这个uint64转换为一些对象,在那里我可以以字符串格式读取年份、日期、小时、分钟等,这样我就可以构建一个自定义的日期-时间字符串。

我怎样才能把uint64转换成有用的东西。我尝试将uint64转换为文件时间的所有方法都会出现编译错误,例如

uint64 big_int;  // this will end up containing the nanosecond interval time
.
.
.
FILETIME t = static_cast<FILETIME>(big_int);

我认为使用chrono-兼容的低级别日期算法、<chrono>实用程序和C++11 <chrono>工具执行此计算是一项挑战。

来自chrono-兼容的低级别日期算法我们需要:

template <class Int>
constexpr
Int
days_from_civil(Int y, unsigned m, unsigned d) noexcept;

它将y/m/d三重转换为1970-01-01之前/之后的天数,以及:

template <class Int>
constexpr
std::tuple<Int, unsigned, unsigned>
civil_from_days(Int z) noexcept;

其将1970-01-01之前/之后的天数转换为y/m/d三元组。

<chrono>实用程序我们需要:

template <class To, class Rep, class Period>
To
floor(const std::chrono::duration<Rep, Period>& d);

它与std::chrono::duration_cast非常相似,只是它向负无穷大取整,而不是向零取整(对负值取整有影响)。

首先,我们需要一个日期时间对象来保存我们想要的所有信息:

struct datetime
{
    int year;
    int month;
    int day;
    int hour;
    int minute;
    int second;
    int nanoseconds;
};

打印设施便于演示(应根据需要定制任何格式):

std::ostream&
operator<<(std::ostream& os, const datetime& dt)
{
    char fill = os.fill();
    os.fill('0');
    os << dt.year << '-';
    os << std::setw(2) << dt.month << '-';
    os << std::setw(2) << dt.day << " T ";
    os << std::setw(2) << dt.hour << ':';
    os << std::setw(2) << dt.minute << ':';
    os << std::setw(2) << dt.second << '.';
    os << std::setw(9) << dt.nanoseconds;
    os.fill(fill);
    return os;
}

我选择了nanoseconds的单位作为datetime的精度,这有点过头了。您可以很容易地将其设置为所需的任何值。

接下来,可以方便地声明两个自定义std::chrono::duration的:

一个代表天(正好24小时):

typedef std::chrono::duration
        <
            int,
            std::ratio_multiply<std::ratio<24>, std::chrono::hours::period>
        > days;

其中一个表示问题陈述的100纳秒间隔:

typedef std::chrono::duration
        <
            std::int64_t, std::ratio_multiply<std::ratio<100>, std::nano>
        > wtick;

现在我们有了将std::uint64_t转换为datetime所需的所有工具(本问题的主题):

datetime
datetime_from_wtick(wtick t)
{
    // Get the number of days between 1601-01-01 and 1970-01-01
    constexpr days epoch{days_from_civil(1601, 1, 1)};
    // eppoch is a negative number of days, so add it to get time since 1970-01-01
    wtick utc_time = t + epoch;
    days d = floor<days>(utc_time);  // Get #days before/since 1970-01-01
    datetime r;
    // Split #days into year/month/day
    std::tie(r.year, r.month, r.day) = civil_from_days(d.count());
    utc_time -= d;  // Subtract off #days to leave hours:minutes:seconds.fractional
    auto h = floor<std::chrono::hours>(utc_time);  // Get hours
    r.hour = h.count();
    utc_time -= h;  // Subtract off hours to leave minutes:seconds.fractional
    auto m = floor<std::chrono::minutes>(utc_time);  // Get minutes
    r.minute = m.count();
    utc_time -= m;  // Subtract off minutes to leave seconds.fractional
    auto s = floor<std::chrono::seconds>(utc_time);  // Get seconds
    r.second = s.count();
    utc_time -= s;  // Subtract off seconds to leave fractional seconds
    std::chrono::nanoseconds ns = utc_time;  // Get nanoseconds
    r.nanoseconds = ns.count();
    return r;
}

这个解决方案只需将历元移动到1970-01-01,然后截断天数、小时数、分钟数等,直到我们减少到几分之一秒。天数进一步分为三个部分:y/m/d。

改变历元的唯一原因是利用chrono兼容低级别数据算法中经过充分调试、高性能和极其稳健的公式。

这个解决方案忽略了闰秒的存在。我的猜测是Windows FILETIME也有。但是,如果没有,您可以通过构建一个将datetime映射到要添加的#秒的表来考虑闰秒。例如,如果datetime扩展到当前闰秒表之外,则添加25秒以获得"闰秒";真";现在和1601-01-01之间的差异。我的经验是,计算机正在使用Unix时间,它只是掩盖了闰秒。

使用全面测试

int
main()
{
    std::cout << datetime_from_wtick(wtick(130330211760000005)) << 'n';
}

哪个应该给出:

2014-01-01 T 03:39:36.000000500

其旨在表示UTC时区中的datetime

这篇文章主要有两点:

  1. 如果使用得当,<chrono>可以用来消除所有的转换常数。本例仅介绍了几个用于定义自定义std::chrono:durations的转换常数和epoch移位。在那之后,机器就可以正常工作,消除了常见的错误。

  2. chrono兼容的低级别数据算法有一些非常有用和有效的算法。

更新

使用无符号持续时间确实很容易出错,63位wticks的范围很大。将wtick::rep更改为int64_t。这样可以得到更好的结果:

int
main()
{
    std::cout << datetime_from_wtick(wtick(0)) << 'n';
    std::cout << datetime_from_wtick(wtick(864000000000)) << 'n';
    std::cout << datetime_from_wtick(wtick(130330211760000005)) << 'n';
    std::cout << datetime_from_wtick(wtick(0x7FFFFFFFFFFFFFFF)) << 'n';
}
1601-01-01 T 00:00:00.000000000
1601-01-02 T 00:00:00.000000000
2014-01-01 T 03:39:36.000000500
30828-09-14 T 02:48:05.477580700

C++20更新

多年来,自从这个答案首次发布以来,chrono-Compatible Low Level Date Algorithms演变成了一个免费的、开源的日期/时间库,它进一步演变成了C++标准提案,并被接受为C++20的<chrono>库(您的供应商可能会也可能尚未实现它)。

结果是datetime_from_wtick现在可以更容易地实现:

std::string
datetime_from_wtick(wtick wt)
{
    auto constexpr epoch_diff =
      std::chrono::sys_days{} - std::chrono::sys_days{std::chrono::January/1/1601};
    return std::format("{:%F T %T}", std::chrono::sys_time{wt - epoch_diff});
}

这个简单的小函数给出了与前面所示完全相同的输出。但它更容易阅读和理解。