问题
What is the best/easiest way to deal with Julian dates in C++? I want to be able to convert between Julian dates and Gregorian dates. I have C++11 and C++14. Can the <chrono>
library help with this problem?
回答1:
To convert between a Julian date and std::chrono::system_clock::time_point
the first thing one needs to do is find out the difference between the epochs.
The system_clock
has no official epoch, but the de facto standard epoch is 1970-01-01 00:00:00 UTC (Gregorian calendar). For convenience, it is handy to state the Julian date epoch in terms of the proleptic Gregorian calendar. This calendar extends the current rules backwards, and includes a year 0. This makes the arithmetic easier, but one has to take care to convert years BC into negative years by subtracting 1 and negating (e.g. 2BC is year -1). The Julian date epoch is -4713-11-24 12:00:00 UTC (roughly speaking).
The <chrono>
library can conveniently handle time units on this scale. Additionally, this date library can conveniently convert between Gregorian dates and system_clock::time_point
. To find the difference between these two epochs is simply:
constexpr
auto
jdiff()
{
using namespace date;
using namespace std::chrono_literals;
return sys_days{jan/1/1970} - (sys_days{nov/24/-4713} + 12h);
}
This returns a std::chrono::duration
with a period of hours. In C++14 this can be constexpr
and we can use the chrono duration literal 12h
instead of std::chrono::hours{12}
.
If you don't want to use the date library, this is just a constant number of hours and can be rewritten to this more cryptic form:
constexpr
auto
jdiff()
{
using namespace std::chrono_literals;
return 58574100h;
}
Either way you write it, the efficiency is identical. This is just a function that returns the constant 58574100
. This could also be a constexpr
global, but then you have to leak your using declarations, or decide not to use them.
Next it is handy to create a Julian date clock (jdate_clock
). Since we need to deal with units at least as fine as a half a day, and it is common to express julian dates as floating point days, I will make the jdate_clock::time_point
a count of double-based days from the 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()};
}
};
Implementation note:
I converted the return from
system_clock::now()
toduration
immediately to avoid overflow for those systems wheresystem_clock::duration
is nanoseconds.
jdate_clock
is now a fully conforming and fully functioning <chrono>
clock. For example I can find out what time it is now with:
std::cout << std::fixed;
std::cout << jdate_clock::now().time_since_epoch().count() << '\n';
which just output:
2457354.310832
This is a type-safe system in that jdate_clock::time_point
and system_clock::time_point
are two distinct types which one can not accidentally perform mixed arithmetic in. And yet you can still get all of the rich benefits from the <chrono>
library, such as add and subtract durations to/from your 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);
But if I accidentally said:
auto tomorrow = system_clock::now() + 24h;
auto diff = tomorrow - jnow;
I would get an error such as this:
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;
~~~~~~~~ ^ ~~~~
In English: You can't subtract a jdate_clock::time_point
from a std::chrono::system_clock::time_point
.
But sometimes I do want to convert a jdate_clock::time_point
to a system_clock::time_point
or vice-versa. For that one can easily write a couple of helper functions:
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};
}
Implementation note:
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
.
The general recipe is to get the duration
since the epoch (duration
s are "clock neutral"), add or subtract the offset between the epochs, and then convert the duration
into the desired time_point
.
These will convert among the two clock's time_point
s using any precision, all in a type-safe manner. If it compiles, it works. If you made a programming error, it shows up at compile time. Valid example uses include:
auto tp = sys_to_jdate(system_clock::now());
tp
is a jdate::time_point
except that it has integral representation with the precision of whatever your system_clock::duration
is (for me that is microseconds). Be forewarned that if it is nanoseconds for you (gcc), this will overflow as nanoseconds only has a range of +/- 292 years.
You can force the precision like so:
auto tp = sys_to_jdate(time_point_cast<hours>(system_clock::now()));
And now tp
is an integral count of hours since the jdate
epoch.
If you are willing to use this date library, one can easily use the utilities above to convert a floating point julian date into a Gregorian date, with any accuracy you want. For example:
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";
We use our jdate_clock
to create a jdate_clock::time_point
. Then we use our jdate_to_sys
conversion function to convert jtp
into a system_clock::time_point
. This will have a representation of double and a period of hours. That isn't really important though. What is important is to convert it into whatever representation and precision you want. I've done that above with floor<seconds>
. I also could have used time_point_cast<seconds>
and it would have done the same thing. floor
comes from the date library, always truncates towards negative infinity, and is easier to spell.
This will output:
Julian date 2457354.310832 is 2015-11-27 19:27:35 UTC
If I wanted to round to the nearest second instead of floor, that would simply be:
auto tp = round<seconds>(jdate_to_sys(jtp));
Julian date 2457354.310832 is 2015-11-27 19:27:36 UTC
Or if I wanted it to the nearest millisecond:
auto tp = round<milliseconds>(jdate_to_sys(jtp));
Julian date 2457354.310832 is 2015-11-27 19:27:35.885 UTC
Update
The floor
and round
functions mentioned above as part of Howard Hinnant's date library are now also available under namespace std::chrono
as part of C++17.
来源:https://stackoverflow.com/questions/33964461/handling-julian-dates-in-c11-14