mountcontrol/mcc/mcc_utils.h
Timur A. Fatkhullin 27dccfe7c0 ...
2025-10-07 23:51:58 +03:00

702 lines
21 KiB
C++

#pragma once
#include <charconv>
#include <cmath>
#include <cstring>
#include <expected>
#include <format>
#include <numbers>
#include <ranges>
#include <regex>
#include "mcc_traits.h"
namespace mcc::utils
{
static const std::regex decimalNumberRx{" *[-+]?([0-9]*[.])?[0-9]+([eE][-+]?[0-9]+)? *"};
static const std::regex sexagesimalReprRx{" *[-+]?[0-9]{1,2}:[0-9]{1,2}:([0-9]{0,2}[.])?[0-9]+ *"};
static bool isEqual(std::floating_point auto const& v1, std::floating_point auto const& v2)
{
constexpr auto eps = std::numeric_limits<std::common_type_t<decltype(v1), decltype(v2)>>::epsilon();
return std::fabs(v1 - v2) <= eps * std::fmax(std::fabs(v1), std::fabs(v2));
}
enum class TrimType { TRIM_LEFT, TRIM_RIGHT, TRIM_BOTH };
template <std::ranges::contiguous_range R>
static std::string_view trimSpaces(R&& r, TrimType type = TrimType::TRIM_BOTH)
requires std::same_as<char, std::remove_cvref_t<std::ranges::range_value_t<R>>>
{
auto is_space = [](const auto& ch) { return ch == ' '; };
auto end = std::forward<R>(r).end();
auto f1 = std::forward<R>(r).begin();
if (type != TrimType::TRIM_RIGHT) {
// look for the first non-space symbol
f1 = std::ranges::find_if_not(std::forward<R>(r), is_space);
if (f1 == end) { // all are spaces!
return std::string_view();
}
}
auto f2 = end;
if (type != TrimType::TRIM_LEFT) {
auto f3 = f1;
do {
f2 = std::ranges::find_if(++f3, end, is_space);
if (f2 == end)
break;
f3 = std::ranges::find_if_not(f2 + 1, end, is_space);
} while (f3 != end);
}
return std::string_view(f1, f2);
}
static std::string_view trimSpaces(const char* r, TrimType type = TrimType::TRIM_BOTH)
{
return trimSpaces(std::string_view(r), type);
}
template <typename T, std::ranges::contiguous_range R>
std::optional<T> numFromStr(R&& r)
requires((std::integral<T> || std::floating_point<T>) &&
std::same_as<char, std::remove_cvref_t<std::ranges::range_value_t<R>>>)
{
T val;
const char* end_ptr = &*r.end();
if constexpr (std::integral<T>) {
auto cvt_res = std::from_chars(&*r.begin(), &*r.end(), val);
if (cvt_res.ec != std::errc()) {
return std::nullopt;
} else if (cvt_res.ptr != end_ptr) {
return std::nullopt;
}
} else {
#ifdef _LIBCPP_VERSION // clang's libc++ does not have floating-point overloads for std::from_chars
std::string s{str.begin(), str.end()};
size_t pos;
try {
if constexpr (std::same_as<T, float>) {
val = std::stof(s, &pos);
} else if constexpr (std::same_as<T, double>) {
val = std::stod(s, &pos);
} else {
val = std::stold(s, &pos);
}
} catch (...) {
return std::nullopt;
}
if (pos != s.size()) {
return std::nullopt;
}
#else
auto cvt_res = std::from_chars(&*r.begin(), &*r.end(), val);
if (cvt_res.ec != std::errc()) {
return std::nullopt;
} else if (cvt_res.ptr != end_ptr) {
return std::nullopt;
}
#endif
}
return val;
}
template <std::ranges::contiguous_range R>
static std::optional<double> parsAngleString(R&& r, bool hms = false)
requires std::same_as<char, std::remove_cvref_t<std::ranges::range_value_t<R>>>
{
auto str = trimSpaces(std::forward<R>(r));
bool ok = std::regex_match(str.begin(), str.end(), decimalNumberRx);
if (ok) {
return numFromStr<double>(str);
}
ok = std::regex_match(str.begin(), str.end(), sexagesimalReprRx);
if (ok) {
auto str = trimSpaces(std::forward<R>(r));
auto parts = std::views::split(str, std::string_view(":"));
double val;
double p1 = numFromStr<double>(*parts.begin()).value();
val = std::abs(p1);
double dv = 60.0;
for (auto const& v : parts | std::views::drop(1)) {
val += numFromStr<double>(v).value() / dv;
dv *= 60.0;
}
// val += numFromStr<double>(parts[1]) / 60.0;
// val += numFromStr<double>(parts[2]) / 3600.0;
if (p1 < 0) {
val = -val;
}
if (hms) {
val *= 15.0;
}
return val;
}
return std::nullopt;
}
static std::optional<double> parsAngleString(const char* s, bool hms = false)
{
return parsAngleString(std::span{s, std::strlen(s)}, hms);
}
static constexpr auto deg2radCoeff = std::numbers::pi / 180.0;
// radians to degrees
template <bool NORM = false>
static double rad2deg(double ang)
{
auto r = ang / deg2radCoeff;
if constexpr (NORM) {
return std::fmod(r, 360.0);
} else {
return r;
}
}
template <std::ranges::output_range<char> R, bool NORM = false>
static R rad2deg_str(double ang, int prec = 6)
{
R res;
std::string fmt = "{:0." + std::to_string(prec) + "f}";
auto degs = rad2deg<NORM>(ang);
std::vformat_to(std::back_inserter(res), {fmt.begin(), fmt.end()}, std::make_format_args(degs));
return res;
}
template <bool NORM = false>
static std::string rad2deg_str(double ang, int prec = 6)
{
return rad2deg_str<std::string, NORM>(ang, prec);
}
template <std::ranges::output_range<char> R, bool NORM = false>
static R rad2deg_str(double ang1, double ang2, std::string_view delim = ",", int prec1 = 6, int prec2 = 6)
{
R res = rad2deg_str<R, NORM>(ang1, prec1);
R r = rad2deg_str<R, NORM>(ang2, prec2);
std::ranges::copy(delim, std::back_inserter(res));
std::ranges::copy(r, std::back_inserter(res));
return res;
}
template <bool NORM = false>
static std::string rad2deg_str(double ang1, double ang2, std::string_view delim = ",", int prec1 = 6, int prec2 = 6)
{
return rad2deg_str<std::string, NORM>(ang1, ang2, delim, prec1, prec2);
}
// radians to sexagesimal
template <std::ranges::output_range<char> R, bool NORM = false>
static R rad2sxg(double ang, bool hms = false, int prec = 2)
{
R res;
if (!std::isfinite(ang)) {
std::vformat_to(std::back_inserter(res), "{}", std::make_format_args(ang));
return res;
}
std::string fmt = "{:02.0f}:{:02.0f}:{:0" + std::to_string(prec + 3) + "." + std::to_string(prec) + "f}";
// std::string fmt = "{:02.0f}:{:02.0f}:{:02." + std::to_string(prec) + "f}";
auto degs = rad2deg<NORM>(std::abs(ang));
if (hms) {
degs /= 15.0;
}
auto term = 10.0;
for (int i = 1; i < prec; ++i) {
term *= 10.0;
}
auto d = std::trunc(degs);
auto s = (degs - d) * 60.0;
auto m = std::trunc(s);
s = (s - m) * 60.0;
// round to given precision
s = std::round(s * term) / term;
if (isEqual(s, 60.0)) {
m += 1.0;
s = 0.0;
if (isEqual(m, 60.0)) {
d += 1.0;
m = 0.0;
}
}
if (ang < 0) {
std::ranges::copy(std::string_view("-"), std::back_inserter(res));
}
std::vformat_to(std::back_inserter(res), std::string_view{fmt.begin(), fmt.end()}, std::make_format_args(d, m, s));
return res;
}
template <bool NORM = false>
static std::string rad2sxg(double ang, bool hms = false, int prec = 2)
{
return rad2sxg<std::string, NORM>(ang, hms, prec);
}
template <std::ranges::output_range<char> R, bool NORM = false>
static R RADEC_rad2sxg(double ra, double dec, std::string_view delim = ",", int ra_prec = 2, int dec_prec = 1)
{
R res = rad2sxg<R, NORM>(ra, true, ra_prec);
R r = rad2sxg(dec, false, dec_prec);
std::ranges::copy(delim, std::back_inserter(res));
std::ranges::copy(r, std::back_inserter(res));
return res;
}
template <bool NORM = false>
static std::string RADEC_rad2sxg(double ra, double dec, std::string_view delim = ",", int ra_prec = 2, int dec_prec = 1)
{
return RADEC_rad2sxg<std::string, NORM>(ra, dec, delim, ra_prec, dec_prec);
}
template <std::ranges::output_range<char> R, bool NORM = false>
static R AZZD_rad2sxg(double az, double zd, std::string_view delim = ",", int az_prec = 2, int zd_prec = 1)
{
R res = rad2sxg<R, NORM>(az, false, az_prec);
R r = rad2sxg(zd, false, zd_prec);
std::ranges::copy(delim, std::back_inserter(res));
std::ranges::copy(r, std::back_inserter(res));
return res;
}
template <bool NORM = false>
static std::string AZZD_rad2sxg(double az, double zd, std::string_view delim = ",", int az_prec = 2, int zd_prec = 1)
{
return AZZD_rad2sxg<std::string, NORM>(az, zd, delim, az_prec, zd_prec);
}
// string to pair of angles
// " 12.3453467,102.4345346"
// "12:43:23.423, 102:43:12.124"
// " 12.3453467, 102:43:12.124 "
template <mcc::traits::mcc_input_char_range R>
std::pair<double, double> parseAnglePair(R&& str, bool hms1 = false, bool hms2 = false, std::string_view delim = ",")
{
std::pair<double, double> res{std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()};
auto found1 = std::ranges::search(std::forward<R>(str), delim);
if (found1.empty()) {
return res;
}
std::string s1, s2;
std::ranges::copy(str.begin(), found1.begin(), std::back_inserter(s1));
auto rem_len = std::distance(found1.end(), str.end());
if (!rem_len) {
return res;
}
auto found2 = std::ranges::search(found1.end(), str.end(), delim.begin(), delim.end());
if (found2.empty()) {
std::ranges::copy(found1.end(), str.end(), std::back_inserter(s2));
} else {
std::ranges::copy(found1.end(), found2.end(), std::back_inserter(s2));
}
auto vo1 = parsAngleString(s1, hms1);
if (vo1) {
auto vo2 = parsAngleString(s2, hms2);
if (vo2) {
res = {vo1.value(), vo2.value()};
}
}
return res;
}
template <traits::mcc_input_char_range R>
static constexpr size_t FNV1aHash(const R& r)
{
static_assert(sizeof(size_t) == 8 || sizeof(size_t) == 4, "ONLY FOR 32 or 64-bit size_t!!!");
size_t hash = 0, prime = 0;
if constexpr (sizeof(size_t) == 8) { // 64-bit
prime = 1099511628211UL;
hash = 14695981039346656037UL;
} else if constexpr (sizeof(size_t) == 4) { // 32-bit
prime = 16777619;
hash = 2166136261;
}
for (const char& ch : r) {
hash ^= ch;
hash *= prime;
}
return hash;
}
static constexpr size_t FNV1aHash(std::forward_iterator auto begin, std::sentinel_for<decltype(begin)> auto end)
requires std::same_as<std::remove_cv_t<std::iter_value_t<decltype(begin)>>, char>
{
static_assert(sizeof(size_t) == 8 || sizeof(size_t) == 4, "ONLY FOR 32 or 64-bit size_t!!!");
size_t hash = 0, prime = 0;
if constexpr (sizeof(size_t) == 8) { // 64-bit
prime = 1099511628211UL;
hash = 14695981039346656037UL;
} else if constexpr (sizeof(size_t) == 4) { // 32-bit
prime = 16777619;
hash = 2166136261;
}
for (auto it = begin; it != end; ++it) {
hash ^= *it;
hash *= prime;
}
return hash;
}
class MccSimpleDeserializer
{
public:
static constexpr std::string RANGE_DELIM_SEQ = ",";
template <traits::mcc_input_char_range R>
MccSimpleDeserializer& setRangeDelim(R&& r)
{
if (std::ranges::size(std::forward<R>(r))) {
_rangeDelim.clear();
std::ranges::copy(std::forward<R>(r), std::back_inserter(_rangeDelim));
}
return *this;
}
template <traits::mcc_output_char_range R>
R getRangeDelim() const
{
R r;
std::ranges::copy(_rangeDelim, std::back_inserter(r));
return r;
}
std::string getRangeDelim() const
{
return getRangeDelim<std::string>();
}
template <traits::mcc_input_char_range IR, typename VT>
std::error_code operator()(IR&& bytes, VT& value)
{
std::error_code ret{};
if constexpr (std::is_arithmetic_v<VT>) {
auto v = mcc::utils::numFromStr<VT>(trimSpaces(bytes));
if (!v.has_value()) {
return std::make_error_code(std::errc::invalid_argument);
}
value = v.value();
} else if constexpr (mcc::traits::mcc_output_char_range<VT>) {
VT r;
std::ranges::copy(bytes, std::back_inserter(r));
value = r;
} else if constexpr (std::ranges::range<VT>) {
using el_t = std::ranges::range_value_t<VT>;
if constexpr (std::is_reference_v<el_t> || std::is_const_v<el_t>) { // no reference or constants allowed
return std::make_error_code(std::errc::invalid_argument);
}
VT r;
el_t elem;
auto els = std::views::split(bytes, _rangeDelim);
for (auto const& el : els) {
ret = (*this)(std::string_view(el), elem);
if (!ret) {
std::back_inserter(r) = elem;
} else {
return std::make_error_code(std::errc::invalid_argument);
}
}
value = r;
} else if constexpr (mcc::traits::mcc_time_duration_c<VT>) {
typename VT::rep vd;
ret = (*this)(trimSpaces(bytes), vd);
if (!ret) {
value = VT{vd};
}
} else {
ret = std::make_error_code(std::errc::invalid_argument);
}
return ret;
}
protected:
std::string _rangeDelim{RANGE_DELIM_SEQ};
};
/* key-value pair holder */
// to follow std::variant requirements (not references, not array, not void)
template <typename T>
concept variant_valid_type_c = requires { !std::is_array_v<T> && !std::is_void_v<T> && !std::is_reference_v<T>; };
template <typename T>
concept keyvalue_record_c = requires(T t) {
requires std::same_as<decltype(t.key), std::string_view>;
requires variant_valid_type_c<decltype(t.value)>;
};
template <typename T>
concept keyvalue_desc_c = requires(T t) { []<keyvalue_record_c... Ts>(std::tuple<Ts...>) {}(t); };
template <keyvalue_desc_c DESCR_T>
class KeyValueHolder
{
public:
static constexpr std::string_view COMMENT_SEQ{"#"};
static constexpr std::string_view KEY_VALUE_DELIM{"="};
static constexpr std::string_view VALUE_ARRAY_DELIM{","};
inline static auto defaultDeserializeFunc = []<typename VT>(this auto&& self, std::string_view str, VT& value) {
std::error_code ret{};
if constexpr (std::is_arithmetic_v<VT>) {
auto v = mcc::utils::numFromStr<VT>(trimSpaces(str));
if (!v.has_value()) {
return std::make_error_code(std::errc::invalid_argument);
}
value = v.value();
} else if constexpr (mcc::traits::mcc_output_char_range<VT>) {
VT r;
std::ranges::copy(str, std::back_inserter(r));
value = r;
} else if constexpr (std::ranges::range<VT>) {
using el_t = std::ranges::range_value_t<VT>;
if constexpr (std::is_reference_v<el_t> || std::is_const_v<el_t>) { // no reference or constants allowed
return std::make_error_code(std::errc::invalid_argument);
}
VT r;
el_t elem;
auto els = std::views::split(str, VALUE_ARRAY_DELIM);
for (auto const& el : els) {
ret = std::forward<decltype(self)>(self)(std::string_view(el), elem);
if (!ret) {
std::back_inserter(r) = elem;
} else {
return std::make_error_code(std::errc::invalid_argument);
}
}
value = r;
} else if constexpr (mcc::traits::mcc_time_duration_c<VT>) {
typename VT::rep vd;
ret = std::forward<decltype(self)>(self)(trimSpaces(str), vd);
if (!ret) {
value = VT{vd};
}
} else {
ret = std::make_error_code(std::errc::invalid_argument);
}
return ret;
};
KeyValueHolder(DESCR_T desc) : _keyValue(desc)
{
[this]<size_t... I>(std::index_sequence<I...>) {
((_hashes[I] = FNV1aHash(std::get<I>(_keyValue).key)), ...);
}(std::make_index_sequence<std::tuple_size_v<DESCR_T>>());
}
template <typename T>
std::expected<T, std::error_code> getValue(std::string_view key)
{
T v;
auto err = forKey(key, [&v]<typename VT>(const VT& val) {
if constexpr (std::convertible_to<VT, T>) {
v = val;
return std::error_code();
} else {
return std::make_error_code(std::errc::invalid_argument);
}
});
if (err) {
return std::unexpected(err);
} else {
return v;
}
}
template <typename T>
std::error_code setValue(std::string_view key, const T& value)
{
return forKey(key, [value]<typename VT>(VT& val) {
if constexpr (std::convertible_to<T, VT>) {
val = value;
return std::error_code();
} else {
return std::make_error_code(std::errc::invalid_argument);
}
});
}
template <std::ranges::contiguous_range R, std::ranges::input_range RecDelimT = std::string_view>
std::error_code fromCharRange(const R& buffer, RecDelimT rec_delim = std::string_view("\n"))
{
// return fromCharRange(buffer, KeyValueHolder::defaultDeserializeFunc, std::move(rec_delim));
return fromCharRange(buffer, MccSimpleDeserializer{}.setRangeDelim(VALUE_ARRAY_DELIM), std::move(rec_delim));
}
template <std::ranges::contiguous_range R,
typename DeserFuncT,
std::ranges::input_range RecDelimT = std::string_view>
std::error_code fromCharRange(const R& buffer,
DeserFuncT&& deser_func,
RecDelimT rec_delim = std::string_view("\n"))
{
// static_assert(mcc::traits::mcc_callable_c<std::decay_t<DeserFuncT>>, "!!!!!!!");
if constexpr (std::is_array_v<std::decay_t<R>>) { // char*, const char*
if constexpr (std::is_array_v<std::decay_t<RecDelimT>>) {
return fromCharRange(std::string_view{buffer}, std::forward<DeserFuncT>(deser_func),
std::string_view(rec_delim));
} else {
return fromCharRange(std::string_view{buffer}, std::forward<DeserFuncT>(deser_func),
std::move(rec_delim));
}
} else {
if constexpr (std::is_array_v<std::decay_t<RecDelimT>>) {
return fromCharRange(buffer, std::forward<DeserFuncT>(deser_func), std::string_view(rec_delim));
}
}
std::error_code ec{};
std::string_view rec, key, value;
auto recs = std::views::split(buffer, std::move(rec_delim));
for (auto const& el : recs) {
rec = mcc::utils::trimSpaces(el, TrimType::TRIM_LEFT);
if (rec.size()) {
auto found = std::ranges::search(rec, COMMENT_SEQ);
if (found.begin() != rec.end()) { // there was the comment sequence in record
rec = std::string_view(rec.begin(), found.begin());
}
if (rec.size()) {
found = std::ranges::search(rec, KEY_VALUE_DELIM);
if (found.begin() != rec.begin()) { // ignore an empty key
key = trimSpaces(std::string_view(rec.begin(), found.begin()), TrimType::TRIM_RIGHT);
value = std::string_view(found.end(), rec.end());
ec = forKey(key, [value, &deser_func]<typename VT>(VT& v) { return deser_func(value, v); });
}
} // just comment string starting from the beginning, just skip it (no error)
} // empty record, just skip it (no error)
if (ec) {
break;
}
}
return ec;
}
protected:
DESCR_T _keyValue;
std::array<size_t, std::tuple_size_v<DESCR_T>> _hashes;
std::error_code forKey(std::string_view key, auto&& func)
{
return forHash<0>(FNV1aHash(key), std::forward<decltype(func)>(func));
}
template <size_t I = 0>
std::error_code forHash(size_t hash, auto&& func)
{
if constexpr (I < std::tuple_size_v<DESCR_T>) {
if (hash == _hashes[I]) {
return std::forward<decltype(func)>(func)(std::get<I>(_keyValue).value);
} else {
return forHash<I + 1>(hash, std::forward<decltype(func)>(func));
}
}
return std::make_error_code(std::errc::argument_out_of_domain);
}
};
} // namespace mcc::utils