Обработка юлианских дат в 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-интерфейсах хронографа и даты, поэтому буду рад мнению о правильности моих исправлений.

Другие вопросы по тегам