|
| 1 | +#ifndef RFL_PARSING_PARSER_TIME_POINT_HPP_ |
| 2 | +#define RFL_PARSING_PARSER_TIME_POINT_HPP_ |
| 3 | + |
| 4 | +#include <chrono> |
| 5 | +#include <cstring> |
| 6 | +#include <ctime> |
| 7 | +#include <map> |
| 8 | +#include <optional> |
| 9 | +#include <sstream> |
| 10 | +#include <string> |
| 11 | + |
| 12 | +#include "../Result.hpp" |
| 13 | +#include "Parent.hpp" |
| 14 | +#include "Parser_base.hpp" |
| 15 | +#include "schema/Type.hpp" |
| 16 | + |
| 17 | +namespace rfl::parsing { |
| 18 | + |
| 19 | +template <class R, class W, class Duration, class ProcessorsType> |
| 20 | + requires AreReaderAndWriter< |
| 21 | + R, W, std::chrono::time_point<std::chrono::system_clock, Duration>> |
| 22 | +struct Parser<R, W, |
| 23 | + std::chrono::time_point<std::chrono::system_clock, Duration>, |
| 24 | + ProcessorsType> { |
| 25 | + public: |
| 26 | + using InputVarType = typename R::InputVarType; |
| 27 | + |
| 28 | + using ParentType = Parent<W>; |
| 29 | + |
| 30 | + using TimePointType = |
| 31 | + std::chrono::time_point<std::chrono::system_clock, Duration>; |
| 32 | + |
| 33 | + static Result<TimePointType> read(const R& _r, |
| 34 | + const InputVarType& _var) noexcept { |
| 35 | + return Parser<R, W, std::string, ProcessorsType>::read(_r, _var).and_then( |
| 36 | + from_string); |
| 37 | + } |
| 38 | + |
| 39 | + template <class P> |
| 40 | + static void write(const W& _w, const TimePointType& _tp, const P& _parent) { |
| 41 | + Parser<R, W, std::string, ProcessorsType>::write(_w, to_string(_tp), |
| 42 | + _parent); |
| 43 | + } |
| 44 | + |
| 45 | + static schema::Type to_schema( |
| 46 | + std::map<std::string, schema::Type>* _definitions) { |
| 47 | + return Parser<R, W, std::string, ProcessorsType>::to_schema(_definitions); |
| 48 | + } |
| 49 | + |
| 50 | + private: |
| 51 | + static std::string to_string(const TimePointType& _tp) { |
| 52 | + const auto sys_time = |
| 53 | + std::chrono::time_point_cast<std::chrono::nanoseconds>(_tp); |
| 54 | + const auto epoch = sys_time.time_since_epoch(); |
| 55 | + const auto secs = std::chrono::duration_cast<std::chrono::seconds>(epoch); |
| 56 | + const auto nsecs = |
| 57 | + std::chrono::duration_cast<std::chrono::nanoseconds>(epoch - secs); |
| 58 | + |
| 59 | + auto t = static_cast<std::time_t>(secs.count()); |
| 60 | + std::tm tm{}; |
| 61 | +#if defined(_MSC_VER) || defined(__MINGW32__) |
| 62 | + gmtime_s(&tm, &t); |
| 63 | +#else |
| 64 | + gmtime_r(&t, &tm); |
| 65 | +#endif |
| 66 | + |
| 67 | + char buf[32]; |
| 68 | + strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tm); |
| 69 | + |
| 70 | + const auto ns = nsecs.count(); |
| 71 | + if (ns != 0) { |
| 72 | + char frac[16]; |
| 73 | + // Write nanoseconds, then strip trailing zeros. |
| 74 | + snprintf(frac, sizeof(frac), ".%09lld", |
| 75 | + static_cast<long long>(ns < 0 ? -ns : ns)); |
| 76 | + auto len = strlen(frac); |
| 77 | + while (len > 1 && frac[len - 1] == '0') { |
| 78 | + --len; |
| 79 | + } |
| 80 | + frac[len] = '\0'; |
| 81 | + return std::string(buf) + frac + "Z"; |
| 82 | + } |
| 83 | + return std::string(buf) + "Z"; |
| 84 | + } |
| 85 | + |
| 86 | + static Result<TimePointType> from_string(const std::string& _str) noexcept { |
| 87 | + try { |
| 88 | + std::tm tm{}; |
| 89 | + const char* str = _str.c_str(); |
| 90 | + const char* rest = parse_datetime(str, &tm); |
| 91 | + if (!rest) { |
| 92 | + return error("Could not parse time point from '" + _str + "'."); |
| 93 | + } |
| 94 | + |
| 95 | + auto t = to_time_t(tm); |
| 96 | + auto tp = std::chrono::system_clock::from_time_t(t); |
| 97 | + |
| 98 | + // Parse fractional seconds if present. |
| 99 | + if (*rest == '.') { |
| 100 | + ++rest; |
| 101 | + long long frac = 0; |
| 102 | + int digits = 0; |
| 103 | + while (*rest >= '0' && *rest <= '9' && digits < 9) { |
| 104 | + frac = frac * 10 + (*rest - '0'); |
| 105 | + ++rest; |
| 106 | + ++digits; |
| 107 | + } |
| 108 | + // Pad to nanoseconds (9 digits). |
| 109 | + while (digits < 9) { |
| 110 | + frac *= 10; |
| 111 | + ++digits; |
| 112 | + } |
| 113 | + // Truncate beyond nanoseconds. |
| 114 | + while (digits > 9) { |
| 115 | + frac /= 10; |
| 116 | + --digits; |
| 117 | + } |
| 118 | + tp += std::chrono::duration_cast<std::chrono::system_clock::duration>( |
| 119 | + std::chrono::nanoseconds(frac)); |
| 120 | + } |
| 121 | + |
| 122 | + // Parse timezone: 'Z', '+HH:MM', '-HH:MM', or end of string. |
| 123 | + if (*rest == '+' || *rest == '-') { |
| 124 | + const auto offset = parse_tz_offset(rest); |
| 125 | + if (!offset) { |
| 126 | + return error("Could not parse timezone offset from '" + _str + "'."); |
| 127 | + } |
| 128 | + tp -= *offset; |
| 129 | + } else if (*rest != 'Z' && *rest != '\0') { |
| 130 | + return error("Could not parse time point from '" + _str + |
| 131 | + "': expected 'Z', timezone offset, or end of string."); |
| 132 | + } |
| 133 | + |
| 134 | + return std::chrono::time_point_cast<Duration>(tp); |
| 135 | + } catch (std::exception& e) { |
| 136 | + return error(e.what()); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + static bool is_digit(char c) { return c >= '0' && c <= '9'; } |
| 141 | + |
| 142 | + static int two_digits(const char* s) { |
| 143 | + return (s[0] - '0') * 10 + (s[1] - '0'); |
| 144 | + } |
| 145 | + |
| 146 | + /// Parses a timezone offset like "+05:30" or "-08:00". |
| 147 | + /// Returns the offset as a chrono duration, or std::nullopt on failure. |
| 148 | + static std::optional<std::chrono::minutes> parse_tz_offset(const char* _str) { |
| 149 | + if (*_str != '+' && *_str != '-') { |
| 150 | + return std::nullopt; |
| 151 | + } |
| 152 | + const int sign = (*_str == '+') ? 1 : -1; |
| 153 | + ++_str; |
| 154 | + // Expect HH:MM or HHMM. |
| 155 | + if (!is_digit(_str[0]) || !is_digit(_str[1])) { |
| 156 | + return std::nullopt; |
| 157 | + } |
| 158 | + const int hours = two_digits(_str); |
| 159 | + _str += 2; |
| 160 | + if (*_str == ':') { |
| 161 | + ++_str; |
| 162 | + } |
| 163 | + if (!is_digit(_str[0]) || !is_digit(_str[1])) { |
| 164 | + return std::nullopt; |
| 165 | + } |
| 166 | + const int minutes = two_digits(_str); |
| 167 | + return std::chrono::minutes(sign * (hours * 60 + minutes)); |
| 168 | + } |
| 169 | + |
| 170 | + static const char* parse_datetime(const char* _str, std::tm* _tm) { |
| 171 | +#if defined(_MSC_VER) || defined(__MINGW32__) |
| 172 | + std::istringstream input(_str); |
| 173 | + input.imbue(std::locale::classic()); |
| 174 | + input >> std::get_time(_tm, "%Y-%m-%dT%H:%M:%S"); |
| 175 | + if (input.fail()) { |
| 176 | + return nullptr; |
| 177 | + } |
| 178 | + const auto pos = input.tellg(); |
| 179 | + if (pos == std::streampos(-1)) { |
| 180 | + // Stream reached EOF after parsing — all input was consumed. |
| 181 | + return _str + std::strlen(_str); |
| 182 | + } |
| 183 | + return _str + static_cast<std::ptrdiff_t>(pos); |
| 184 | +#else |
| 185 | + return strptime(_str, "%Y-%m-%dT%H:%M:%S", _tm); |
| 186 | +#endif |
| 187 | + } |
| 188 | + |
| 189 | + static std::time_t to_time_t(std::tm& _tm) { |
| 190 | +#if defined(_MSC_VER) || defined(__MINGW32__) |
| 191 | + return _mkgmtime(&_tm); |
| 192 | +#else |
| 193 | + return timegm(&_tm); |
| 194 | +#endif |
| 195 | + } |
| 196 | +}; |
| 197 | + |
| 198 | +} // namespace rfl::parsing |
| 199 | + |
| 200 | +#endif |
0 commit comments