Program Listing for File Timezone.hpp

Return to documentation for file (src/rdf4cpp/Timezone.hpp)

#ifndef RDF4CPP_TIMEZONE_HPP
#define RDF4CPP_TIMEZONE_HPP

#include <chrono>
#include <format>
#include <string_view>

#include <dice/hash.hpp>

#include <rdf4cpp/datatypes/rdf.hpp>
#include <rdf4cpp/datatypes/registry/util/CharConvExt.hpp>
#include <rdf4cpp/Assert.hpp>

#include <boost/multiprecision/cpp_int.hpp>

namespace rdf4cpp {
struct Timezone {
    // heavily inspired by https://howardhinnant.github.io/date/tz.html#Examples

    std::chrono::minutes offset = std::chrono::minutes{0};

    static constexpr const char *begin_tokens = "Z+-";

    constexpr Timezone() = default;

    inline explicit Timezone(const std::chrono::time_zone *tz, std::chrono::time_point<std::chrono::system_clock> n = std::chrono::system_clock::now())
        : offset(std::chrono::duration_cast<std::chrono::minutes>(tz->get_info(n).offset)) {
    }

    constexpr explicit Timezone(std::chrono::hours h) noexcept
        : offset(h) {}

    constexpr explicit Timezone(std::chrono::minutes h) noexcept
        : offset(h) {}

    constexpr auto operator<=>(const Timezone &) const noexcept = default;

    static constexpr Timezone parse(std::string_view v, std::string_view dt) {
        Timezone tz{};
        if (v == "Z") {
            return tz;
        }
        bool negative = false;
        if (v[0] == '-') {
            negative = true;
        }
        v = v.substr(1);
        auto sep = v.find(':');
        if (sep == std::string::npos) {
            throw InvalidNode{std::format("{} parsing error: timezone expected :", dt)};
        }
        std::chrono::hours const h{datatypes::registry::util::from_chars<int32_t, "timezone">(v.substr(0, sep))};
        tz.offset = std::chrono::minutes{datatypes::registry::util::from_chars<int32_t, "timezone">(v.substr(sep + 1))} + std::chrono::minutes{h};
        if (negative) {
            tz.offset *= -1;
        }
        if (tz.offset.count() < -840 || tz.offset.count() > 840) {
            throw InvalidNode{std::format("{} parsing error: timezone offset too big", dt)};
        }
        return tz;
    }

    static constexpr std::optional<Timezone> parse_optional(std::string_view &s, std::string_view dt) {
        auto p = s.find_first_of(begin_tokens, 1);
        if (p == 0 || p == std::string::npos)
            return std::nullopt;
        auto pre = s.substr(0, p);
        auto tz = parse(s.substr(p), dt);
        s = pre;
        return tz;
    }

    // sign, hours, :, minutes
    static constexpr size_t max_canonical_string_chars = 1+(std::numeric_limits<int64_t>::digits10+1)+1+2;
    template<std::output_iterator<char> T>
    T to_canonical_string(T o) const noexcept {
        if (offset == std::chrono::minutes{0}) {
            *o = 'Z';
            ++o;
            return o;
        }
        auto h = std::chrono::floor<std::chrono::hours>(std::chrono::abs(offset));
        auto m = std::chrono::abs(offset) - h;
        return std::format_to(o, "{}{:02}:{:02}", offset >= std::chrono::minutes{0} ? '+' : '-', h.count(), m.count());
    }
    [[nodiscard]] std::string to_canonical_string() const noexcept {
        std::string buf{};
        buf.reserve(max_canonical_string_chars);
        to_canonical_string(std::back_inserter(buf));
        return buf;
    }

    [[nodiscard]] const std::chrono::time_zone *get_tz(std::chrono::time_point<std::chrono::system_clock> n = std::chrono::system_clock::now()) const {
        for (const auto &tz : std::chrono::get_tzdb().zones) {
            if (tz.get_info(n).offset == std::chrono::seconds(offset)) {
                return &tz;
            }
        }
        return nullptr;
    }

    template<typename Duration>
    [[nodiscard]] auto to_sys(const std::chrono::local_time<Duration> &tp) const noexcept {
        return std::chrono::sys_time<std::common_type_t<Duration, std::chrono::seconds>>{(tp - offset).time_since_epoch()};
    }

    template<typename Duration>
    [[nodiscard]] auto to_local(const std::chrono::sys_time<Duration> &tp) const noexcept {
        return std::chrono::local_time<std::common_type_t<Duration, std::chrono::seconds>>{(tp + offset).time_since_epoch()};
    }

    template<typename Duration>
    [[nodiscard]] std::chrono::sys_info get_info(const std::chrono::sys_time<Duration> &) const noexcept {
        return std::chrono::sys_info{
                std::chrono::sys_seconds{std::chrono::seconds{0L}},
                std::chrono::sys_seconds{std::chrono::seconds{std::numeric_limits<int64_t>::max()}},
                offset,
                std::chrono::minutes{0},
                to_canonical_string()};
    }

    const Timezone *operator->() const noexcept {
        return this;
    }

    static constexpr Timezone max_value() noexcept {
        return Timezone{std::chrono::hours{14}};
    };
    static constexpr Timezone min_value() noexcept {
        return Timezone{std::chrono::hours{-14}};
    };
};

using OptionalTimezone = std::optional<Timezone>;

using Month = std::chrono::month;
using Day = std::chrono::day;

struct Year {
private:
    int64_t value_;

public:
    explicit constexpr Year(int64_t y = 0) noexcept : value_{y} {
    }

    constexpr explicit operator int64_t() const noexcept {
        return value_;
    }

    [[nodiscard]] constexpr bool is_leap() const noexcept(noexcept(value_ % 100)) {
        return value_ % 4 == 0 && (value_ % 100 != 0 || value_ % 400 == 0);
    }

    constexpr auto operator<=>(Year const &) const noexcept = default;

    friend constexpr Year operator+(Year const &y, std::chrono::years d) noexcept {
        return Year{y.value_ + d.count()};
    }
    friend constexpr Year operator+(std::chrono::years d, Year const &y) noexcept {
        return Year{y.value_ + d.count()};
    }

    constexpr Year operator+=(std::chrono::years d) noexcept {
        *this = *this + d;
        return *this;
    }

    friend constexpr Year operator-(Year const &y, std::chrono::years d) noexcept {
        return Year{y.value_ - d.count()};
    }
    friend constexpr std::chrono::years operator-(Year const &a, Year const &b) noexcept {
        return std::chrono::years{a.value_ - b.value_};
    }

    constexpr Year operator-=(std::chrono::years d) noexcept {
        *this = *this - d;
        return *this;
    }

    constexpr Year &operator++() noexcept {
        *this += std::chrono::years{1};
        return *this;
    }
    constexpr Year operator++(int) noexcept {
        Year r = *this;
        ++(*this);
        return r;
    }

    constexpr Year &operator--() noexcept {
        *this -= std::chrono::years{1};
        return *this;
    }
    constexpr Year operator--(int) noexcept {
        Year r = *this;
        --(*this);
        return r;
    }

    static constexpr Year max() noexcept {
        return Year{std::numeric_limits<int64_t>::max()};
    }
    static constexpr Year min() noexcept {
        return Year{std::numeric_limits<int64_t>::min()};
    }
};

struct YearMonth {
private:
    Year year_ = Year{0};
    Month month_ = Month{1};

    static constexpr YearMonth create_normalized(int64_t y, int64_t mo) noexcept {
        --mo;
        y += mo / 12;
        mo %= 12;
        if (mo < 0) { // fix result of % being in [-11,11]
            --y;
            mo += 12;
        }
        return YearMonth{Year{y}, std::chrono::month{static_cast<unsigned int>(mo+1)}};
    }

public:
    constexpr YearMonth() noexcept = default;

    constexpr YearMonth(Year y, std::chrono::month m) noexcept : year_{y}, month_{m} {
    }

    [[nodiscard]] constexpr Year year() const noexcept {
        return year_;
    }

    [[nodiscard]] constexpr Month month() const noexcept {
        return month_;
    }

    constexpr auto operator<=>(YearMonth const &) const noexcept = default;

    [[nodiscard]] constexpr bool ok() const noexcept {
        return month_.ok();
    }

    friend constexpr YearMonth operator+(YearMonth const &ym, std::chrono::years d) noexcept {
        return YearMonth{ym.year_ + d, ym.month_};
    }
    friend constexpr YearMonth operator+(std::chrono::years d, YearMonth const &ym) noexcept {
        return YearMonth{ym.year_ + d, ym.month_};
    }

    constexpr YearMonth& operator+=(std::chrono::years d) noexcept {
        *this = *this + d;
        return *this;
    }

    friend constexpr YearMonth operator+(YearMonth const &ym, std::chrono::months d) noexcept {
        return create_normalized(static_cast<int64_t>(ym.year_), static_cast<unsigned int>(ym.month_) + d.count());
    }
    friend constexpr YearMonth operator+(std::chrono::months d, YearMonth const &ym) noexcept {
        return create_normalized(static_cast<int64_t>(ym.year_), static_cast<unsigned int>(ym.month_) + d.count());
    }

    constexpr YearMonth& operator+=(std::chrono::months d) noexcept {
        *this = *this + d;
        return *this;
    }

    friend constexpr YearMonth operator-(YearMonth const &ym, std::chrono::years d) noexcept {
        return {ym.year_ - d, ym.month_};
    }
    friend constexpr YearMonth operator-(YearMonth const &ym, std::chrono::months d) noexcept {
        return create_normalized(static_cast<int64_t>(ym.year_), static_cast<unsigned int>(ym.month_) - d.count());
    }

    friend constexpr std::chrono::months operator-(YearMonth const &a, YearMonth const &b) noexcept {
        return (a.year_ - b.year_) + (a.month_ - b.month_);
    }

    constexpr YearMonth& operator-=(std::chrono::years d) noexcept {
        *this = *this - d;
        return *this;
    }
    constexpr YearMonth& operator-=(std::chrono::months d) noexcept {
        *this = *this - d;
        return *this;
    }
};

struct YearMonthDay {
    template<typename P>
    using time_point = std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<P, std::chrono::days::period>>;
    template<typename P>
    using time_point_local = std::chrono::time_point<std::chrono::local_t, std::chrono::duration<P, std::chrono::days::period>>;

private:
    // adapted from https://howardhinnant.github.io/date_algorithms.html
    Year year_ = Year{0};
    Month month_ = Month{1};
    Day day_ = Day{1};

    static constexpr std::chrono::day last_day_in_month(Year year, Month month) noexcept {
        RDF4CPP_ASSERT(month.ok());
        constexpr unsigned char common[]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        auto m = static_cast<unsigned int>(month);
        return std::chrono::day{m != 2 || !year.is_leap() ? common[m - 1] : 29u};
    }

public:
    constexpr YearMonthDay() noexcept  = default;

    constexpr explicit YearMonthDay(std::chrono::year_month_day ymd) noexcept
        : year_(static_cast<int>(ymd.year())), month_(ymd.month()), day_(ymd.day()) {
    }
    constexpr YearMonthDay(Year const &y, Month m, std::chrono::day d) noexcept
        : year_(y), month_(m), day_(d) {
    }
    constexpr YearMonthDay(Year const &y, Month m, std::chrono::last_spec) noexcept
        : year_(y), month_(m), day_(last_day_in_month(y, m)) {
    }
    constexpr YearMonthDay(YearMonth const &ym, Day d) noexcept
        : year_(ym.year()), month_(ym.month()), day_(d) {
    }
    constexpr YearMonthDay(YearMonth const &ym, std::chrono::last_spec) noexcept
        : YearMonthDay(ym.year(), ym.month(), std::chrono::last) {
    }
    template<typename P>
    constexpr explicit YearMonthDay(time_point<P> sd) noexcept(noexcept(P{} + P{} * P{} - P{} / P{})) {
        static_assert(std::numeric_limits<unsigned>::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer");
        static_assert(std::numeric_limits<int64_t>::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer");
        static_assert(std::numeric_limits<P>::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer");
        P z = sd.time_since_epoch().count();
        z += 719468;
        P const era = (z >= 0 ? z : z - 146096) / 146097;
        auto const doe = static_cast<P>(z - era * 146097);                    // [0, 146096]
        auto const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;  // [0, 399]
        P const y = static_cast<P>(yoe) + era * 400;
        auto const doy = doe - (365 * yoe + yoe / 4 - yoe / 100);  // [0, 365]
        auto const mp = (5 * doy + 2) / 153;                       // [0, 11]
        auto const d = doy - (153 * mp + 2) / 5 + 1;               // [1, 31]
        auto const m = mp < 10 ? mp + 3 : mp - 9;                  // [1, 12]
        year_ = Year{static_cast<int64_t>(y + (m <= 2))};
        month_ = std::chrono::month{static_cast<unsigned>(m)};
        day_ = std::chrono::day{static_cast<unsigned>(d)};
    }
    template<typename P>
    constexpr explicit YearMonthDay(time_point_local<P> sd) noexcept(noexcept(P{} + P{} * P{} - P{} / P{}))
        : YearMonthDay(time_point<P>(sd.time_since_epoch())) {
    }

    [[nodiscard]] constexpr Year year() const noexcept {
        return year_;
    }

    [[nodiscard]] constexpr Month month() const noexcept {
        return month_;
    }

    [[nodiscard]] constexpr Day day() const noexcept {
        return day_;
    }

    [[nodiscard]] constexpr time_point<boost::multiprecision::checked_int128_t> to_time_point() const {
        static_assert(std::numeric_limits<unsigned>::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer");
        static_assert(std::numeric_limits<int64_t>::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer");
        static_assert(std::numeric_limits<boost::multiprecision::checked_int128_t>::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer");
        boost::multiprecision::checked_int128_t y = static_cast<int64_t>(year_);
        auto m = static_cast<unsigned int>(month_);
        auto d = static_cast<unsigned int>(day_);
        y -= m <= 2;
        boost::multiprecision::checked_int128_t const era = (y >= 0 ? y : y - 399) / 400;
        auto const yoe = static_cast<unsigned>(y - era * 400);                 // [0, 399]
        unsigned const doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1;  // [0, 365]
        unsigned const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;            // [0, 146096]
        // note that the epoch of system_clock is specified as 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970
        return time_point<boost::multiprecision::checked_int128_t>{typename time_point<boost::multiprecision::checked_int128_t>::duration{era * 146097 + static_cast<boost::multiprecision::checked_int128_t>(doe) - 719468}};
    }
    [[nodiscard]] constexpr time_point_local<boost::multiprecision::checked_int128_t> to_time_point_local() const{
        return time_point_local<boost::multiprecision::checked_int128_t>{to_time_point().time_since_epoch()};
    }

    [[nodiscard]] constexpr bool ok() const noexcept {
        return month_.ok() && day_.ok() && day_ <= last_day_in_month(year_, month_);
    }

    constexpr auto operator<=>(YearMonthDay const &) const noexcept = default;

    friend constexpr YearMonthDay operator+(YearMonthDay const &ym, std::chrono::years d) noexcept {
        return {ym.year_ + d, ym.month_, ym.day_};
    }
    friend constexpr YearMonthDay operator+(std::chrono::years d, YearMonthDay const &ym) noexcept {
        return {ym.year_ + d, ym.month_, ym.day_};
    }

    constexpr YearMonthDay & operator+=(std::chrono::years d) noexcept {
        *this = *this + d;
        return *this;
    }

    friend constexpr YearMonthDay operator+(YearMonthDay const &d, std::chrono::months m) noexcept {
        return YearMonthDay{YearMonth{d.year_, d.month_} + m, d.day_};
    }
    friend constexpr YearMonthDay operator+(std::chrono::months m, YearMonthDay const &d) noexcept {
        return YearMonthDay{YearMonth{d.year_, d.month_} + m, d.day_};
    }

    constexpr YearMonthDay & operator+=(std::chrono::months d) noexcept {
        *this = *this + d;
        return *this;
    }

    friend constexpr YearMonthDay operator-(YearMonthDay const &ym, std::chrono::years d) noexcept {
        return {ym.year_ - d, ym.month_, ym.day_};
    }
    friend constexpr YearMonthDay operator-(YearMonthDay const &d, std::chrono::months m) noexcept {
        return YearMonthDay{YearMonth{d.year_, d.month_} - m, d.day_};
    }

    constexpr YearMonthDay & operator-=(std::chrono::years d) noexcept {
        *this = *this - d;
        return *this;
    }
    constexpr YearMonthDay & operator-=(std::chrono::months d) noexcept {
        *this = *this - d;
        return *this;
    }
};

using DurationNano = std::chrono::duration<boost::multiprecision::checked_int128_t, std::chrono::nanoseconds::period>;
using TimePoint = std::chrono::time_point<std::chrono::local_t, DurationNano>;
// system_clock does not use leap seconds, as required by rdf (xsd)
using TimePointSys = std::chrono::time_point<std::chrono::system_clock, DurationNano>;
using ZonedTime = std::chrono::zoned_time<DurationNano, Timezone>;

namespace util {

// see https://www.w3.org/TR/xpath-functions/#comp.datetime
inline constexpr YearMonthDay time_point_replacement_date{Year(1972), std::chrono::January, std::chrono::day{1}};
inline constexpr DurationNano time_point_replacement_time_of_day{0};
// implementation defined, not from standard
inline constexpr Timezone time_point_replacement_timezone{std::chrono::minutes{0}};

constexpr TimePoint construct_timepoint(YearMonthDay const &date, const DurationNano& time_of_day) {
    auto sd = date.to_time_point_local();
    auto ms = static_cast<TimePoint>(sd);
    ms += time_of_day;
    return ms;
}

constexpr std::pair<YearMonthDay, DurationNano> deconstruct_timepoint(TimePoint const &tp) {
    auto days = std::chrono::floor<std::chrono::duration<DurationNano::rep, std::chrono::days::period>>(tp);
    return {YearMonthDay{days}, tp - days};
}

} // namespace util

}  // namespace rdf4cpp

namespace std::chrono {
    template<>
    struct zoned_traits<::rdf4cpp::Timezone> {
        static ::rdf4cpp::Timezone default_zone() noexcept {
            return ::rdf4cpp::Timezone{};
        }
    };
} // namespace std::chrono

#ifndef DOXYGEN_PARSER
template<typename Policy>
struct dice::hash::dice_hash_overload<Policy, rdf4cpp::Timezone> {
    static size_t dice_hash(rdf4cpp::Timezone const &x) noexcept {
        auto off = x.offset.count();
        return dice::hash::dice_hash_templates<Policy>::dice_hash(off);
    }
};
template<typename Policy>
struct dice::hash::dice_hash_overload<Policy, rdf4cpp::OptionalTimezone> {
    static size_t dice_hash(rdf4cpp::OptionalTimezone const &x) noexcept {
        auto off = x.has_value() ? x->offset.count() : std::chrono::minutes{std::chrono::hours{15}}.count();
        return dice::hash::dice_hash_templates<Policy>::dice_hash(off);
    }
};
template<typename Policy>
struct dice::hash::dice_hash_overload<Policy, ::boost::multiprecision::checked_int128_t> {
    static size_t dice_hash(::boost::multiprecision::cpp_int const &x) noexcept {
        return dice::hash::dice_hash_templates<Policy>::dice_hash(::boost::multiprecision::hash_value(x));
    }
};
template<typename Policy>
struct dice::hash::dice_hash_overload<Policy, rdf4cpp::Year> {
    static size_t dice_hash(rdf4cpp::Year const &x) noexcept {
        return dice::hash::dice_hash_templates<Policy>::dice_hash(static_cast<int64_t>(x));
    }
};
template<typename Policy>
struct dice::hash::dice_hash_overload<Policy, rdf4cpp::YearMonthDay> {
    static size_t dice_hash(rdf4cpp::YearMonthDay const &x) noexcept {
        return dice::hash::dice_hash_templates<Policy>::dice_hash(std::make_tuple(x.year(), x.month(), x.day()));
    }
};
template<typename Policy>
struct dice::hash::dice_hash_overload<Policy, rdf4cpp::YearMonth> {
    static size_t dice_hash(rdf4cpp::YearMonth const &x) noexcept {
        return dice::hash::dice_hash_templates<Policy>::dice_hash(std::make_tuple(x.year(), x.month()));
    }
};

template<>
struct std::formatter<rdf4cpp::Year> : std::formatter<std::string_view> {
    inline auto format(rdf4cpp::Year const &p, format_context &ctx) const {
        return std::format_to(ctx.out(), "{:0{}}", static_cast<int64_t>(p), static_cast<int64_t>(p) < 0 ? 5 : 4);
    }
};
template<>
struct std::formatter<rdf4cpp::YearMonthDay> : std::formatter<std::string_view> {
    inline auto format(rdf4cpp::YearMonthDay const &p, format_context &ctx) const {
        return std::format_to(ctx.out(), "{}-{:%m}-{:%d}", p.year(), p.month(), p.day());
    }
};
template<>
struct std::formatter<rdf4cpp::YearMonth> : std::formatter<std::string_view> {
    inline auto format(rdf4cpp::YearMonth const &p, format_context &ctx) const {
        return std::format_to(ctx.out(), "{}-{:%m}", p.year(), p.month());
    }
};
template<>
struct std::formatter<rdf4cpp::TimePoint> : std::formatter<std::string_view> {
    inline auto format(rdf4cpp::TimePoint const &p, format_context &ctx) const {
        auto [date, time] = rdf4cpp::util::deconstruct_timepoint(p);
        return std::format_to(ctx.out(), "{}T{:%H:%M:%S}", date, std::chrono::hh_mm_ss{std::chrono::duration_cast<std::chrono::nanoseconds>(time)});
    }
};
template<>
struct std::formatter<rdf4cpp::ZonedTime> : std::formatter<std::string_view> {
    inline auto format(rdf4cpp::ZonedTime const &p, format_context &ctx) const {
        return std::format_to(ctx.out(), "{}{}", p.get_local_time(), p.get_time_zone().to_canonical_string());
    }
};

namespace rdf4cpp {
    inline std::ostream &operator<<(std::ostream &os, rdf4cpp::Year const &value) {
        std::format_to(std::ostream_iterator<char, char>{os}, "{}", value);
        return os;
    }
    inline std::ostream &operator<<(std::ostream &os, rdf4cpp::YearMonthDay const &value) {
        std::format_to(std::ostream_iterator<char, char>{os}, "{}", value);
        return os;
    }
    inline std::ostream &operator<<(std::ostream &os, rdf4cpp::YearMonth const &value) {
        std::format_to(std::ostream_iterator<char, char>{os}, "{}", value);
        return os;
    }
}
#endif

#endif  //RDF4CPP_TIMEZONE_HPP