处理c++ 11/14中的儒略历日

Handling Julian days in C++11/14

本文关键字:c++ 处理      更新时间:2023-10-16

在c++中处理朱利安日最好/最简单的方法是什么?我希望能够在儒略历和公历日期之间进行转换。我有c++ 11和c++ 14。<chrono>库可以帮助解决这个问题吗?

要在儒略历日和std::chrono::system_clock::time_point之间进行转换,首先需要做的是找出两个历元之间的差异。

system_clock没有正式的纪元,但事实上的标准纪元是1970-01-01 00:00:00 UTC(公历)。为了方便起见,用预言的公历来陈述儒略历日期纪元是很方便的。这个日历向后扩展了当前的规则,并包含了年份0。这使得算术更容易,但必须注意通过减去1和负(例如2BC是-1年)将年BC转换为负年。儒略历日期纪元是-4713-11-24 12:00:00 UTC(粗略地说)。

<chrono>库可以方便地处理这个尺度上的时间单位。此外,这个日期库可以方便地在公历日期和system_clock::time_point日期之间进行转换。要找出这两个时代的区别,只需:

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

返回一个时间段为小时的std::chrono::duration。在c++ 14中,它可以是constexpr,我们可以使用计时持续时间文字12h代替std::chrono::hours{12}

如果你不想使用日期库,这只是一个常数小时数,可以重写为更神秘的形式:

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

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

接下来,它是方便的创建一个儒略历日时钟(jdate_clock)。由于我们需要处理至少像半天这样精确的单位,并且通常将儒略历日期表示为浮点日,因此我将jdate_clock::time_point设置为从epoch开始的双基数天数计数:

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

实现注:

我立即将返回值从system_clock::now()转换为duration,以避免system_clock::duration为纳秒的系统溢出。

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>库中获得所有丰富的好处,例如在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, std::remove_cv_t<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, std::remove_cv_t<decltype(d)>>{d};
}

实现注:

我已经添加了静态范围检查,如果您使用纳秒或基于32位的分钟作为源time_point的持续时间,则可能会触发。

一般的方法是获得自历元以来的duration(durations是"时钟中立的"),添加或减去历元之间的偏移量,然后将duration转换为所需的time_point

将使用任意精度在两个时钟的time_points之间进行转换,所有这些都以类型安全的方式进行。如果它能编译,它就能工作。如果您犯了一个编程错误,它会在编译时显示出来。有效的使用示例包括:

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

tpjdate::time_point,除了它具有与system_clock::duration的精度(对我来说是微秒)相同的积分表示。需要预先警告的是,如果对您来说是纳秒(gcc),则会溢出,因为纳秒只有+/- 292年的范围。

你可以像这样强制精度:

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

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

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

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 day " << jtp.time_since_epoch().count()
<< " is " << tp << " UTCn";

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

这将输出:

Julian day 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 day 2457354.310832 is 2015-11-27 19:27:35.885 UTC

c++ 17的更新

作为Howard Hinnant的日期库的一部分,上面提到的floorround函数现在也可以在命名空间std::chrono下作为c++ 17的一部分使用。

c++ 20的更新

Howard Hinnant的日期库在很大程度上被投进了c++ 20,所以jdate_clock现在可以完全用std::chrono来编写了。

另外还有一个方便的std::chrono::clock_cast功能,jdate_clock可以参与。这有助于不同时钟的time_points之间的转换,甚至可以帮助实现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实现中的一些签名,使客户端更容易用任意durations生成jdate_clocktime_points。

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()可以简单地从system_clock::now()返回clock_cast的当前时间。

from_sys简单地减去给定的基于system_clocktime_point和儒略历:-4713-11-24 12:00:00 UTC。返回类型必须至少精确到小时,因为epoch的精度为小时。

to_sys可以重用from_sys中的历元,通过使用clock_cast查找system_clock历元处的儒略历日期:clock_cast<jdate_clock>(sys_days{})。从儒略历日期中减去这个值,就可以找到自system_clock纪元以来的时间。

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

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 day " << jtp.time_since_epoch()
<< " is " << tp << " UTCn";

输出:

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 day " << jtp.time_since_epoch()
<< " is " << tp << " TAIn";

输出:

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

谢谢Howard提供这些有用的例子。为了使用MSVC/c++ 17成功编译,我最终使用了稍微修改过的sys_to_jdate和jdate_to_sys函数。原来的表单是使用clang 12.0.0.12000032和gcc 8.3.1编译的。

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

以上修改使以下编译错误消失:

C:Program Files (x86)Microsoft Visual Studio2019BuildToolsVCToolsMSVC14.28.29333includechrono(182,23): error C2338: duration must be an instance of std::duration

(显然是由jdate_to_sys的最后一行引起的)

我对时间和日期api非常陌生,因此欢迎输入关于我修复的正确性。