Skip to content

Commit

Permalink
Merge pull request #27 from hasselmm/improvement/better-number-parsing
Browse files Browse the repository at this point in the history
Improve the number parsing
  • Loading branch information
hasselmm authored Sep 19, 2024
2 parents 9e5220c + 8d6df71 commit 1294bb8
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 93 deletions.
7 changes: 6 additions & 1 deletion qnc/qncliterals.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ constexpr auto operator ""_L1(const char *str, std::size_t len)
return QLatin1String{str, static_cast<qsizetype>(len)};
}

#else // QT_VERSION < QT_VERSION_CHECK(6,4,0)
#else // QT_VERSION < QT_VERSION_CHECK(6,0,0)

constexpr auto operator ""_L1(const char ch)
{
Expand All @@ -91,6 +91,11 @@ inline auto operator ""_ba(const char *str, std::size_t len)
return QByteArray{str, static_cast<compat::lentype>(len)};
}

inline auto operator ""_s(const char16_t *str, std::size_t len)
{
return QString::fromUtf16(str, static_cast<compat::lentype>(len));
}

#endif // QT_VERSION < QT_VERSION_CHECK(6,4,0)

constexpr auto operator ""_baview(const char *str, std::size_t len)
Expand Down
94 changes: 94 additions & 0 deletions qnc/qncparse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,97 @@
* Copyright (C) 2023 Mathias Hasselmann
*/
#include "qncparse.h"

// STL headers
#include <cmath>

#if QT_VERSION_MAJOR < 6
using QByteArrayView = QByteArray;
#endif // QT_VERSION_MAJOR < 6

namespace qnc::detail {

template <>
void parse(const QByteArrayView &text, bool &value, bool &isValid)
{
const auto &trimmedText = text.trimmed();

if (trimmedText.compare("true", Qt::CaseInsensitive) == 0
|| trimmedText.compare("yes", Qt::CaseInsensitive) == 0
|| trimmedText.compare("on", Qt::CaseInsensitive) == 0
|| trimmedText.compare("enabled", Qt::CaseInsensitive) == 0) {
value = true;
isValid = true;
} else if (trimmedText.compare("false", Qt::CaseInsensitive) == 0
|| trimmedText.compare("no", Qt::CaseInsensitive) == 0
|| trimmedText.compare("off", Qt::CaseInsensitive) == 0
|| trimmedText.compare("disabled", Qt::CaseInsensitive) == 0) {
value = false;
isValid = true;
} else if (const auto &number = qnc::parse<int>(trimmedText)) {
value = (number != 0);
isValid = true;
} else {
isValid = false;
}
}

template <>
void parse(const QByteArrayView &text, long double &value, bool &isValid)
{
if (Q_UNLIKELY(text.compare("+nan", Qt::CaseInsensitive) == 0)
|| Q_UNLIKELY(text.compare("-nan", Qt::CaseInsensitive) == 0)) {
isValid = false; // reject nan with sign to align with QString::toFloat() and ::toDouble()
} else if (Q_UNLIKELY(text.compare("+inf", Qt::CaseInsensitive) == 0)
|| Q_UNLIKELY(text.compare("inf", Qt::CaseInsensitive) == 0)) {
value = qInf(); // parse infinitity early so that it can be distinguished from out-of-range error
isValid = true;
} else if (Q_UNLIKELY(text.compare("-inf", Qt::CaseInsensitive) == 0)) {
value = -qInf(); // parse infinitity early so that it can be distinguished from out-of-range error
isValid = true;
} else {
const auto first = text.constData();
auto last = static_cast<char *>(nullptr);
const auto result = value = std::strtold(first, &last);

if (Q_UNLIKELY(last == nullptr)
|| Q_UNLIKELY(last == first)
|| Q_UNLIKELY(last[0] != '\0')
|| Q_UNLIKELY(std::isinf(result))) {
isValid = false;
} else {
value = result;
isValid = true;
}
}
}

#if QT_VERSION_MAJOR < 6

template <> void parse(const QLatin1String &text, long double &value, bool &isValid) { parse(QByteArray{text.data(), text.size()}, value, isValid); }
template <> void parse(const QLatin1String &text, bool &value, bool &isValid) { parse(QByteArray{text.data(), text.size()}, value, isValid); }

#else // QT_VERSION_MAJOR >= 6

template <> void parse(const QByteArray &text, long double &value, bool &isValid) { parse(QByteArrayView{text}, value, isValid); }
template <> void parse(const QByteArray &text, bool &value, bool &isValid) { parse(QByteArrayView{text}, value, isValid); }
template <> void parse(const QLatin1String &text, long double &value, bool &isValid) { parse(QByteArrayView{text}, value, isValid); }
template <> void parse(const QLatin1String &text, bool &value, bool &isValid) { parse(QByteArrayView{text}, value, isValid); }

#endif // QT_VERSION_MAJOR >= 6

template <> void parse(const QStringView &text, long double &value, bool &isValid) { parse(text.toLatin1(), value, isValid); }
template <> void parse(const QStringView &text, bool &value, bool &isValid) { parse(text.toLatin1(), value, isValid); }
template <> void parse(const QString &text, long double &value, bool &isValid) { parse(text.toLatin1(), value, isValid); }
template <> void parse(const QString &text, bool &value, bool &isValid) { parse(text.toLatin1(), value, isValid); }

#if QT_VERSION_MAJOR >= 6

template <> void parse(const QAnyStringView &text, long double &value, bool &isValid) { parse(text.toString(), value, isValid); }
template <> void parse(const QAnyStringView &text, bool &value, bool &isValid) { parse(text.toString(), value, isValid); }
template <> void parse(const QUtf8StringView &text, long double &value, bool &isValid) { parse(text.toString(), value, isValid); }
template <> void parse(const QUtf8StringView &text, bool &value, bool &isValid) { parse(text.toString(), value, isValid); }

#endif // QT_VERSION_MAJOR >= 6

} // namespace qnc::detail
66 changes: 47 additions & 19 deletions qnc/qncparse.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,76 @@
#include <QString>

// STL headers
#include <limits>
#include <optional>

namespace qnc {

namespace detail {

template <class S, typename T> auto parseMethod(const T &) = delete;
template <class S> auto parseMethod(const short &) { return &S::toShort; }
template <class S> auto parseMethod(const ushort &) { return &S::toUShort; }
template <class S> auto parseMethod(const int &) { return &S::toInt; }
template <class S> auto parseMethod(const uint &) { return &S::toUInt; }
template <class S> auto parseMethod(const long &) { return &S::toLong; }
template <class S> auto parseMethod(const ulong &) { return &S::toULong; }
template <class S> auto parseMethod(const qlonglong &) { return &S::toLongLong; }
template <class S> auto parseMethod(const qulonglong &) { return &S::toULongLong; }
template <class S> auto parseMethod(const float &) { return &S::toFloat; }
template <class S> auto parseMethod(const double &) { return &S::toDouble; }

template <class S, typename T>
using HasBase = std::is_same<T (S::* )(bool *, int) const, decltype(detail::parseMethod<S>(T{}))>;
void parseIndirect(const S &text, T &value, bool &isValid, int base)
{
auto genericValue = text.toInt(&isValid, base);

if (Q_LIKELY(isValid)) {
if (Q_LIKELY(genericValue >= std::numeric_limits<T>::min())
&& Q_LIKELY(genericValue <= std::numeric_limits<T>::max()))
value = static_cast<T>(genericValue);
else
isValid = false;
}
}

template <class S, typename T, typename ...Args>
void parse(const S &, T &, bool &, Args...) = delete;

template <class S> void parse(const S &text, bool &value, bool &isValid);
template <class S> void parse(const S &text, qint8 &value, bool &isValid, int base = 10) { parseIndirect(text, value, isValid, base); }
template <class S> void parse(const S &text, quint8 &value, bool &isValid, int base = 10) { parseIndirect(text, value, isValid, base); }
template <class S> void parse(const S &text, short &value, bool &isValid, int base = 10) { value = text.toShort (&isValid, base); }
template <class S> void parse(const S &text, ushort &value, bool &isValid, int base = 10) { value = text.toUShort (&isValid, base); }
template <class S> void parse(const S &text, int &value, bool &isValid, int base = 10) { value = text.toInt (&isValid, base); }
template <class S> void parse(const S &text, uint &value, bool &isValid, int base = 10) { value = text.toUInt (&isValid, base); }
template <class S> void parse(const S &text, long &value, bool &isValid, int base = 10) { value = text.toLong (&isValid, base); }
template <class S> void parse(const S &text, ulong &value, bool &isValid, int base = 10) { value = text.toULong (&isValid, base); }
template <class S> void parse(const S &text, qlonglong &value, bool &isValid, int base = 10) { value = text.toLongLong (&isValid, base); }
template <class S> void parse(const S &text, qulonglong &value, bool &isValid, int base = 10) { value = text.toULongLong (&isValid, base); }
template <class S> void parse(const S &text, float &value, bool &isValid) { value = text.toFloat (&isValid); }
template <class S> void parse(const S &text, double &value, bool &isValid) { value = text.toDouble (&isValid); }
template <class S> void parse(const S &text, long double &value, bool &isValid);

template <typename T, class StringLike, typename ...Args>
std::optional<T> parse(const StringLike &text, Args &&...args)
{
auto value = T{};
auto isValid = false;
const auto parser = detail::parseMethod<StringLike>(T{});
const auto value = (text.*parser)(&isValid, std::forward<Args>(args)...);

parse(text, value, isValid, std::forward<Args>(args)...);

if (Q_LIKELY(isValid))
return value;

return {};
}

} // namespace detail
template <typename T, typename Char, std::size_t N, typename ...Args>
std::optional<T> parse(const Char (&str)[N], Args &&...args)
{
const auto it = std::char_traits<Char>::find(str, N, Char(0));
const auto end = it ? it : std::end(str);

return parse<T>(QStringView{str, end}, std::forward<Args>(args)...);
}

template <typename T, class StringLike, typename = std::enable_if_t<detail::HasBase<StringLike, T>::value>>
std::optional<T> parse(const StringLike &text, int base = 10) { return detail::parse<T>(text, base); }
} // namespace detail

template <typename T, class StringLike, typename = std::enable_if_t<!detail::HasBase<StringLike, T>::value>>
template <typename T, class StringLike>
std::optional<T> parse(const StringLike &text) { return detail::parse<T>(text); }

template <typename T, class StringLike>
std::optional<T> parse(const StringLike &text, int base) { return detail::parse<T>(text, base); }

} // namespace qnc

#endif // QNC_QNCPARSE_H
10 changes: 9 additions & 1 deletion tests/auto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,17 @@ function(add_testcase SOURCE_FILENAME) # [SOURCES...]
add_test(NAME "${TESTCASE_NAME}" COMMAND ${TEST_RUNNER} "$<TARGET_FILE:${TESTCASE_NAME}>")
endfunction()

qnc_add_library(
QncTestSuport STATIC
qnctestsupport.cpp
qnctestsupport.h
)

target_link_libraries(QncTestSuport PUBLIC Qt::Test)

add_testcase(tst_httpparser.cpp LIBRARIES QncHttp)
add_testcase(tst_mdnsmessages.cpp LIBRARIES QncMdns)
add_testcase(tst_mdnsresolver.cpp LIBRARIES QncMdns)
add_testcase(tst_qncparse.cpp LIBRARIES QncCore)
add_testcase(tst_qncparse.cpp LIBRARIES QncCore QncTestSuport)
add_testcase(tst_ssdpresolver.cpp LIBRARIES QncSsdp)
add_testcase(tst_xmlparser.cpp LIBRARIES QncXml)
77 changes: 77 additions & 0 deletions tests/auto/qnctestsupport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* QtNetworkCrumbs - Some networking toys for Qt
* Copyright (C) 2019-2024 Mathias Hasselmann
*/
#include "qnctestsupport.h"

// Qt headers
#include <QTest>

// STL headers
#include <iomanip>
#include <sstream>

namespace qnc::tests {
namespace {

[[nodiscard]] std::string toStdString(long double value)
{
const auto p = std::numeric_limits<long double>::digits;
return (std::ostringstream{} << std::setprecision(p) << value).str();
}

[[nodiscard]] QString toString(long double value)
{
return QString::fromStdString(toStdString(value));
}

} // namespace

bool initialize()
{
return QMetaType::registerConverter<long double, QString>(&toString);
}

} // namespace qnc::tests

[[nodiscard]] char *QTest::toString(const long double &t)
{
return qstrdup(qnc::tests::toStdString(t).c_str());
}

[[nodiscard]] bool QTest::qCompare(long double const &t1, long double const &t2,
const char *actual, const char *expected,
const char *file, int line)
{
auto success = false;

if (std::isnan(t1)) {
success = std::isnan(t2);
} else if (std::isinf(t1) && t1 > 0) {
success = std::isinf(t2) && t2 > 0;
} else if (std::isinf(t1) && t1 < 0) {
success = std::isinf(t2) && t2 < 0;
} else {
success = qFuzzyCompare(t1, t2);
}

#if QT_VERSION_MAJOR < 6

return compare_helper(success, "Compared numbers are not the same",
toString(t1), toString(t2), actual, expected, file, line);

#else // QT_VERSION_MAJOR >= 6

return reportResult(success,
[t1] { return toString(t1); },
[t2] { return toString(t2); },
actual, expected, ComparisonOperation::Equal,
file, line);

#endif // QT_VERSION_MAJOR >= 6
}

QDebug operator<<(QDebug debug, long double value)
{
const auto _ = QDebugStateSaver{debug};
return debug.noquote() << qnc::tests::toString(value);
}
69 changes: 69 additions & 0 deletions tests/auto/qnctestsupport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* QtNetworkCrumbs - Some networking toys for Qt
* Copyright (C) 2019-2024 Mathias Hasselmann
*/
#ifndef QNC_QNCTESTSUPPORT_H
#define QNC_QNCTESTSUPPORT_H

// Qt headers
#include <QMetaType>

// STL headers
#include <cmath>

class QDebug;

Q_DECLARE_METATYPE(long double)

namespace QTest {

[[nodiscard]] char *toString(const long double &t);
[[nodiscard]] bool qCompare(long double const &t1, long double const &t2,
const char *actual, const char *expected,
const char *file, int line);

[[nodiscard]] inline bool qCompare(long double const &t1, const double &t2,
const char *actual, const char *expected,
const char *file, int line)
{
return qCompare(t1, static_cast<long double>(t2), actual, expected, file, line);
}

[[nodiscard]] inline bool qCompare(long double const &t1, const float &t2,
const char *actual, const char *expected,
const char *file, int line)
{
return qCompare(t1, static_cast<long double>(t2), actual, expected, file, line);
}

[[nodiscard]] inline bool qCompare(const double &t1, const long double &t2,
const char *actual, const char *expected,
const char *file, int line)
{
return qCompare(static_cast<long double>(t1), t2, actual, expected, file, line);
}

[[nodiscard]] inline bool qCompare(float const &t1, const long double &t2,
const char *actual, const char *expected,
const char *file, int line)
{
return qCompare(static_cast<long double>(t1), t2, actual, expected, file, line);
}

} // namespace QTest

[[nodiscard]] inline bool qFuzzyCompare(long double p1, long double p2)
{
if constexpr (sizeof(long double) != sizeof(double)) {
return (std::abs(p1 - p2) * 10000000000000000. <= std::min(std::abs(p1), std::abs(p2)));
} else {
return qFuzzyCompare(static_cast<double>(p1), static_cast<double>(p2));
}
}

QDebug operator<<(QDebug debug, long double value);

namespace qnc::tests {
bool initialize();
} // namespace qnc::tests

#endif // QNC_QNCTESTSUPPORT_H
Loading

0 comments on commit 1294bb8

Please sign in to comment.