Каков наилучший/самый простой способ справиться с юлианскими датами на С++? Я хочу, чтобы иметь возможность конвертировать между юлианскими датами и григорианскими датами. У меня есть С++ 11 и С++ 14. Может ли библиотека <chrono>
помочь с этой проблемой?
Обработка юлианских дат в С++ 11/14
Ответ 1
Чтобы преобразовать между Julian date и std::chrono::system_clock::time_point
, первое, что нужно сделать, это выяснить разницу между эпохами.
system_clock
не имеет официальной эпохи, но фактическая стандартная эпоха - 1970-01-01 00:00:00 UTC (григорианский календарь). Для удобства удобно указать юлианскую дату в терминах прорептический григорианский календарь. Этот календарь расширяет текущие правила назад и включает в себя год 0. Это упрощает арифметику, но нужно позаботиться о преобразовании лет BC в отрицательные годы путем вычитания 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);
}
Это возвращает a std::chrono::duration
с периодом времени. В С++ 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
.
Но иногда я do хочу преобразовать 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
является "нейтральной тактовой частотой" ), добавляет или вычитает смещение между эпохами, а затем преобразует duration
в желаемый time_point
.
Они преобразуются между двумя часами 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
.
Если вы хотите использовать эту библиотеку дат, можно использовать утилиты выше, чтобы преобразовать дату юлианской точки с плавающей точкой в григорианский дату, с любой точностью, которую вы хотите. Например:
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
как часть С++ 17.