Подтвердить что ты не робот

Обработка юлианских дат в С++ 11/14

Каков наилучший/самый простой способ справиться с юлианскими датами на С++? Я хочу, чтобы иметь возможность конвертировать между юлианскими датами и григорианскими датами. У меня есть С++ 11 и С++ 14. Может ли библиотека <chrono> помочь с этой проблемой?

4b9b3361

Ответ 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.