在 C++11/14 中处理 Julian 日期

Handling Julian dates in C++11/14

在 C++ 中 best/easiest 处理 Julian dates 的方法是什么?我希望能够在 Julian 日期和 Gregorian 日期之间进行转换。我有 C++11 和 C++14。 <chrono> 库可以帮助解决这个问题吗?

要在 Julian datestd::chrono::system_clock::time_point 之间转换,首先需要做的是找出纪元之间的差异。

system_clock 没有官方纪​​元,但事实上的标准纪元是 1970-01-01 00:00:00 UTC(公历)。为方便起见,根据 proleptic Gregorian calendar 来表述 Julian date 纪元很方便。此日历向后扩展了当前规则,并包括了第 0 年。这使得算术更容易,但必须注意通过减去 1 和取反(例如 2BC 是第 -1 年)将 BC 年转换为负数年。 Julian date 纪元是 -4713-11-24 12:00:00 UTC(粗略地说)。

<chrono> 库可以方便地处理这种规模的时间单位。此外,this date library 可以方便地在公历日期和 system_clock::time_point 之间进行转换。要找到这两个时期之间的差异很简单:

constexpr
auto
jdiff()
{
    using namespace date;
    using namespace std::chrono_literals;
    return sys_days{jan/1/1970} - (sys_days{nov/24/-4713} + 12h);
}

这个 return 是一个 std::chrono::duration 的时间段。在 C++14 中,这可以是 constexpr 并且我们可以使用计时持续时间文字 12h 而不是 std::chrono::hours{12}.

如果您不想使用 date library,这只是一个固定的小时数,可以重写为这种更神秘的形式:

constexpr
auto
jdiff()
{
    using namespace std::chrono_literals;
    return 58574100h;
}

不管怎么写,效率都是一样的。这只是一个 return 常量 58574100 的函数。这也可以是一个 constexpr 全局的,但是你必须泄露你的 using 声明,或者决定不使用它们。

接下来创建 Julian 日期时钟很方便 (jdate_clock)。由于我们需要处理至少半天这样精细的单位,并且通常将儒略日期表示为浮点天数,因此我将 jdate_clock::time_point 设为从纪元开始的双倍天数:

struct jdate_clock
{
    using rep        = double;
    using period     = std::ratio<86400>;
    using duration   = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<jdate_clock>;

    static constexpr bool is_steady = false;

    static time_point now() noexcept
    {
        using namespace std::chrono;
        return time_point{duration{system_clock::now().time_since_epoch()} + jdiff()};
    }
};

执行说明:

I converted the return from system_clock::now() to duration immediately to avoid overflow for those systems where system_clock::duration is nanoseconds.

jdate_clock 现在是一个完全一致且功能齐全的 <chrono> 时钟。例如,我可以找出现在几点:

std::cout << std::fixed;
std::cout << jdate_clock::now().time_since_epoch().count() << '\n';

刚刚输出:

2457354.310832

这是一个类型安全的系统,因为 jdate_clock::time_pointsystem_clock::time_point 是两种不同的类型,人们不会意外地在其中执行混合算术。而且您仍然可以获得所有丰富的好处来自 <chrono> 库,例如添加和减去持续时间 to/from 您的 jdate_clock::time_point.

using namespace std::chrono_literals;
auto jnow = jdate_clock::now();
auto jpm = jnow + 1min;
auto jph = jnow + 1h;
auto tomorrow = jnow + 24h;
auto diff = tomorrow - jnow;
assert(diff == 24h);

但是如果我不小心说了:

auto tomorrow = system_clock::now() + 24h;
auto diff = tomorrow - jnow;

我会得到这样的错误:

error: invalid operands to binary expression
  ('std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<long long,
  std::ratio<1, 1000000> > >' and 'std::chrono::time_point<jdate_clock, std::chrono::duration<double,
  std::ratio<86400, 1> > >')
auto diff = tomorrow - jnow;
            ~~~~~~~~ ^ ~~~~

用英语说:你不能从 std::chrono::system_clock::time_point 中减去 jdate_clock::time_point

但有时我 想将 jdate_clock::time_point 转换为 system_clock::time_point,反之亦然。为此,可以轻松编写几个辅助函数:

template <class Duration>
constexpr
auto
sys_to_jdate(std::chrono::time_point<std::chrono::system_clock, Duration> tp) noexcept
{
    using namespace std::chrono;
    static_assert(jdate_clock::duration{jdiff()} < Duration::max(),
                  "Overflow in sys_to_jdate");
    const auto d = tp.time_since_epoch() + jdiff();
    return time_point<jdate_clock, decltype(d)>{d};
}

template <class Duration>
constexpr
auto
jdate_to_sys(std::chrono::time_point<jdate_clock, Duration> tp) noexcept
{
    using namespace std::chrono;
    static_assert(jdate_clock::duration{-jdiff()} > Duration::min(),
                  "Overflow in jdate_to_sys");
    const auto d = tp.time_since_epoch() - jdiff();
    return time_point<system_clock, decltype(d)>{d};
}

执行说明:

I've added static range checking which is likely to fire if you use nanoseconds or a 32bit-based minute as a duration in your source time_point.

一般的方法是得到duration自epoch(durations是“clock neutral”),加上或减去epoch之间的偏移量,然后转换duration 到想要的 time_point.

这些将使用 any 精度在两个时钟的 time_point 之间转换,全部以类型安全的方式进行。如果它编译,它就可以工作。如果你犯了一个编程错误,它会在编译时出现。有效示例使用包括:

auto tp = sys_to_jdate(system_clock::now());

tp 是一个 jdate::time_point 除了它具有整数表示形式,无论您的 system_clock::duration 是什么精度(对我来说是微秒)。预先警告,如果它对你来说是纳秒 (gcc),这将溢出,因为纳秒的范围只有 +/- 292 年。

你可以像这样强制精度:

auto tp = sys_to_jdate(time_point_cast<hours>(system_clock::now()));

现在 tp 是自 jdate 纪元以来的小时数。

如果您愿意使用 this date library,可以很容易地使用上面的实用程序将浮点儒略日期转换为公历日期,并具有您想要的任何精度。例如:

using namespace std::chrono;
using namespace date;
std::cout << std::fixed;
auto jtp = jdate_clock::time_point{jdate_clock::duration{2457354.310832}};
auto tp = floor<seconds>(jdate_to_sys(jtp));
std::cout << "Julian date " << jtp.time_since_epoch().count()
          << " is " << tp << " UTC\n";

我们使用我们的 jdate_clock 创建一个 jdate_clock::time_point。然后我们使用我们的 jdate_to_sys 转换函数将 jtp 转换为 system_clock::time_point。这将有一个双倍的表示和一个小时的时间段。但这并不重要。 重要的是将其转换为您想要 的任何表示形式和精度。我已经用 floor<seconds> 完成了上面的操作。我也可以使用 time_point_cast<seconds> 并且它会做同样的事情。 floor 来自 the date library,总是向负无穷截断,并且更容易拼写。

这将输出:

Julian date 2457354.310832 is 2015-11-27 19:27:35 UTC

如果我想四舍五入到最接近的秒而不是底数,那将是:

auto tp = round<seconds>(jdate_to_sys(jtp));
Julian date 2457354.310832 is 2015-11-27 19:27:36 UTC

或者如果我想精确到毫秒:

auto tp = round<milliseconds>(jdate_to_sys(jtp));
Julian date 2457354.310832 is 2015-11-27 19:27:35.885 UTC

C++17 更新

上面提到的作为 Howard Hinnant's date library 一部分的 floorround 函数现在也作为 C++17 的一部分在命名空间 std::chrono 下可用。

C++20 更新

Howard Hinnant's date library 在很大程度上被投票加入了 C++20,因此 jdate_clock 现在可以完全按照 std::chrono.

来编写

另外还有一个方便的std::chrono::clock_cast功能,jdate_clock可以参与。这方便了time_point不同时钟之间的转换,甚至可以帮助实现jdate_clock:

#include <chrono>

struct jdate_clock;

template <class Duration>
    using jdate_time = std::chrono::time_point<jdate_clock, Duration>;

struct jdate_clock
{
    using rep        = double;
    using period     = std::chrono::days::period;
    using duration   = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<jdate_clock>;

    static constexpr bool is_steady = false;

    static time_point now() noexcept;

    template <class Duration>
    static
    auto
    from_sys(std::chrono::sys_time<Duration> const& tp) noexcept;

    template <class Duration>
    static
    auto
    to_sys(jdate_time<Duration> const& tp) noexcept;
};

template <class Duration>
auto
jdate_clock::from_sys(std::chrono::sys_time<Duration> const& tp) noexcept
{
    using namespace std::chrono;
    return jdate_time{tp - (sys_days{November/24/-4713}+12h)};
}

template <class Duration>
auto
jdate_clock::to_sys(jdate_time<Duration> const& tp) noexcept
{
    using namespace std::chrono;
    return sys_time{tp - clock_cast<jdate_clock>(sys_days{})};
}

jdate_clock::time_point
jdate_clock::now() noexcept
{
    using namespace std::chrono;
    return clock_cast<jdate_clock>(system_clock::now());
}

jdate_time只是一个方便的类型别名,它是按照std::chrono提供的新的方便类型别名的风格编写的。它缩短了 jdate_clock 实现中的一些签名,并使客户更容易用任意 duration 制作 jdate_clocktime_point

jdate_clock有两个新的static成员函数:from_systo_sys。它们取代了之前的命名空间作用域函数 sys_to_jdatejdate_to_sysfrom_systo_sys 使 jdate_clock 能够参与 std::chrono::clock_cast 设施。

clock_cast 查找这些静态成员函数并使用它们在 jdate_clock 参与的所有其他 时钟之间进行转换,计时定义与否在 clock_cast 设施中。

now()可以简单地clock_castsystem_clock::now()到return当前时间。

from_sys 简单地减去给定的基于 system_clocktime_point 和 Julian 纪元:-4713-11-24 12:00:00 UTC。 return 类型必须至少与小时一样精细,因为纪元的精度为小时。

to_sys 可以重用 from_sys 中的纪元,方法是使用 clock_cast 找到 system_clock 纪元的儒略日期:clock_cast<jdate_clock>(sys_days{})。这是从 Julian 日期中减去以找到自 system_clock 纪元以来的时间。

客户端代码现在可以使用通用的 clock_cast 代替不太通用的 jdate_to_sys API:

using namespace std::chrono;

auto jtp = jdate_clock::time_point{jdate_clock::duration{2457354.310832}};
auto tp = round<milliseconds>(clock_cast<system_clock>(jtp));
std::cout << "Julian date " << jtp.time_since_epoch()
          << " is " << tp << " UTC\n";

输出:

Julian date 2457354.310832d is 2015-11-27 19:27:35.885 UTC

最后请注意,尽管 jdate_clockstd::chrono::tai_clock 一无所知,clock_cast 仍然可以 与它相互转换。

auto jtp = jdate_clock::time_point{jdate_clock::duration{2457354.310832}};
auto tp = round<milliseconds>(clock_cast<tai_clock>(jtp));
std::cout << "Julian date " << jtp.time_since_epoch()
          << " is " << tp << " TAI\n";

输出:

Julian date 2457354.310832d is 2015-11-27 19:28:11.885 TAI