Обработка юлианских дат в C++11/14
Какой самый лучший / самый простой способ иметь дело с юлианскими датами в C++? Я хочу иметь возможность конвертировать между юлианскими и григорианскими датами. У меня есть C++11 и C++14. Может ли <chrono>
библиотека поможет с этой проблемой?
2 ответа
Для преобразования между юлианской датой и std::chrono::system_clock::time_point
Первое, что нужно сделать, это выяснить разницу между эпохами.
system_clock
не имеет официальной эпохи, но де-факто стандартной эпохой является 1970-01-01 00:00:00 UTC (григорианский календарь). Для удобства удобно указать эпоху юлианских дат в терминах пролептического григорианского календаря. Этот календарь расширяет текущие правила в обратном направлении и включает год 0. Это облегчает арифметику, но нужно позаботиться о том, чтобы преобразовать годы до нашей эры в отрицательные, вычитая 1 и отрицая (например, 2BC - это год -1). Юлианская эпоха датировки -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{jan/1/1970} - (sys_days{nov/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
глобальный, но тогда вы должны утратить ваши объявления об использовании или принять решение не использовать их.
Далее удобно создать юлианские часы с датой (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()};
}
};
Примечание о реализации:
Я преобразовал возвращение из
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_point
а также system_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;
~~~~~~~~ ^ ~~~~
На английском: вы не можете вычесть jdate_clock::time_point
из std::chrono::system_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};
}
Примечание о реализации:
Я добавил проверку статического диапазона, которая, вероятно, сработает, если вы будете использовать наносекунды или 32-битную минуту как длительность в вашем источнике
time_point
,
Общий рецепт - получить duration
с эпохи (duration
s являются "нейтральными по отношению к часам"), складывают или вычитают смещение между эпохами, а затем преобразуют duration
в желаемый time_point
,
Они будут конвертировать среди двух часов time_point
с использованием любой точности, все в типобезопасной манере. Если он компилируется, он работает. Если вы допустили программную ошибку, она появляется во время компиляции. Допустимые примеры использования включают в себя:
auto tp = sys_to_jdate(system_clock::now());
tp
это jdate::time_point
за исключением того, что он имеет интегральное представление с точностью независимо от вашего system_clock::duration
есть (для меня это микросекунды). Имейте в виду, что если для вас это наносекунды (ГКК), это будет переполнено, поскольку наносекунды имеют диапазон +/- 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 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
происходит из библиотеки дат, всегда усекается до отрицательной бесконечности и проще в написании.
Это выведет:
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
Обновить
floor
а также round
функции, упомянутые выше как часть библиотеки дат Говарда Хиннанта, теперь также доступны в пространстве имен std::chrono
как часть C++17.
Спасибо, Говард, за предоставление этих полезных примеров. В итоге я использовал слегка модифицированные версии функций sys_to_jdate и jdate_to_sys для успешной компиляции с использованием MSVC/C++17. Исходные формы компилировались с использованием 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 Studio\2019\BuildTools\VC\Tools\MSVC\14.28.29333\include\chrono(182,23): error C2338: duration must be an instance of std::duration
(видимо спровоцировано последней строкой jdate_to_sys)
Я очень новичок в API-интерфейсах хронографа и даты, поэтому буду рад мнению о правильности моих исправлений.