From c7b0cc865863d0a2cf924a087ac9d06dd0ae95b1 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 10:22:01 +0200 Subject: [PATCH 1/7] Adjust wrong comment --- qnc/qncliterals.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qnc/qncliterals.h b/qnc/qncliterals.h index f5b6854..066364d 100644 --- a/qnc/qncliterals.h +++ b/qnc/qncliterals.h @@ -74,7 +74,7 @@ constexpr auto operator ""_L1(const char *str, std::size_t len) return QLatin1String{str, static_cast(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) { From f19ba00bbf8b4325705ec088cf0844409ffaf670 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 10:22:12 +0200 Subject: [PATCH 2/7] Add char16_t string literal for Qt5 --- qnc/qncliterals.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qnc/qncliterals.h b/qnc/qncliterals.h index 066364d..31287ab 100644 --- a/qnc/qncliterals.h +++ b/qnc/qncliterals.h @@ -91,6 +91,11 @@ inline auto operator ""_ba(const char *str, std::size_t len) return QByteArray{str, static_cast(len)}; } +inline auto operator ""_s(const char16_t *str, std::size_t len) +{ + return QString::fromUtf16(str, static_cast(len)); +} + #endif // QT_VERSION < QT_VERSION_CHECK(6,4,0) constexpr auto operator ""_baview(const char *str, std::size_t len) From 536ff86e7938b7523024a799985cbb8eb5701183 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 20:15:58 +0200 Subject: [PATCH 3/7] Allow parsing of numbers in raw string literals --- qnc/qncparse.h | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/qnc/qncparse.h b/qnc/qncparse.h index b34d2b8..c867618 100644 --- a/qnc/qncparse.h +++ b/qnc/qncparse.h @@ -14,27 +14,27 @@ namespace qnc { namespace detail { -template auto parseMethod(const T &) = delete; -template auto parseMethod(const short &) { return &S::toShort; } -template auto parseMethod(const ushort &) { return &S::toUShort; } -template auto parseMethod(const int &) { return &S::toInt; } -template auto parseMethod(const uint &) { return &S::toUInt; } -template auto parseMethod(const long &) { return &S::toLong; } -template auto parseMethod(const ulong &) { return &S::toULong; } -template auto parseMethod(const qlonglong &) { return &S::toLongLong; } -template auto parseMethod(const qulonglong &) { return &S::toULongLong; } -template auto parseMethod(const float &) { return &S::toFloat; } -template auto parseMethod(const double &) { return &S::toDouble; } - -template -using HasBase = std::is_same(T{}))>; +template +void parse(const S &, T &, bool &, Args...) = delete; + +template void parse(const S &text, short &value, bool &isValid, int base = 10) { value = text.toShort (&isValid, base); } +template void parse(const S &text, ushort &value, bool &isValid, int base = 10) { value = text.toUShort (&isValid, base); } +template void parse(const S &text, int &value, bool &isValid, int base = 10) { value = text.toInt (&isValid, base); } +template void parse(const S &text, uint &value, bool &isValid, int base = 10) { value = text.toUInt (&isValid, base); } +template void parse(const S &text, long &value, bool &isValid, int base = 10) { value = text.toLong (&isValid, base); } +template void parse(const S &text, ulong &value, bool &isValid, int base = 10) { value = text.toULong (&isValid, base); } +template void parse(const S &text, qlonglong &value, bool &isValid, int base = 10) { value = text.toLongLong (&isValid, base); } +template void parse(const S &text, qulonglong &value, bool &isValid, int base = 10) { value = text.toULongLong (&isValid, base); } +template void parse(const S &text, float &value, bool &isValid) { value = text.toFloat (&isValid); } +template void parse(const S &text, double &value, bool &isValid) { value = text.toDouble (&isValid); } template std::optional parse(const StringLike &text, Args &&...args) { + auto value = T{}; auto isValid = false; - const auto parser = detail::parseMethod(T{}); - const auto value = (text.*parser)(&isValid, std::forward(args)...); + + parse(text, value, isValid, std::forward(args)...); if (Q_LIKELY(isValid)) return value; @@ -42,14 +42,23 @@ std::optional parse(const StringLike &text, Args &&...args) return {}; } -} // namespace detail +template +std::optional parse(const Char (&str)[N], Args &&...args) +{ + const auto it = std::char_traits::find(str, N, Char(0)); + const auto end = it ? it : std::end(str); -template ::value>> -std::optional parse(const StringLike &text, int base = 10) { return detail::parse(text, base); } + return parse(QStringView{str, end}, std::forward(args)...); +} + +} // namespace detail -template ::value>> +template std::optional parse(const StringLike &text) { return detail::parse(text); } +template +std::optional parse(const StringLike &text, int base) { return detail::parse(text, base); } + } // namespace qnc #endif // QNC_QNCPARSE_H From fddbd023e8a0eed4a115e0b2455e010f5e56af98 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 20:23:33 +0200 Subject: [PATCH 4/7] Use strict typing for number parser tests --- tests/auto/tst_qncparse.cpp | 127 ++++++++++++++---------------------- 1 file changed, 49 insertions(+), 78 deletions(-) diff --git a/tests/auto/tst_qncparse.cpp b/tests/auto/tst_qncparse.cpp index 05a970b..89aaba2 100644 --- a/tests/auto/tst_qncparse.cpp +++ b/tests/auto/tst_qncparse.cpp @@ -1,77 +1,63 @@ /* QtNetworkCrumbs - Some networking toys for Qt - * Copyright (C) 2023 Mathias Hasselmann + * Copyright (C) 2019-2024 Mathias Hasselmann */ +// QtNetworkCrumbs headers #include "qncparse.h" +// Qt headers #include -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) -Q_DECLARE_METATYPE(std::optional) +Q_DECLARE_METATYPE(std::function) namespace qnc::core::tests { namespace { -struct NumberTypeInfo -{ - QVariant (* parse) (QStringView) = nullptr; - QVariant (* parseWithBase)(QStringView, int base) = nullptr; - bool (* hasValue) (const QVariant &boxedOptional) = nullptr; - QVariant (* value) (const QVariant &boxedOptional) = nullptr; - - bool hasSign = false; - bool isFloat = false; -}; +template constexpr bool hasNegativeNumbers = std::is_signed_v || std::is_same_v; +template constexpr bool hasCustomBase = std::is_integral_v && !std::is_same_v; template -NumberTypeInfo makeNumberTypeInfo() +std::function makeTestParseNumbers() { - static constexpr auto unbox = [](const QVariant &boxedOptional) { - return qvariant_cast>(boxedOptional); - }; + return [] { + const auto invalidNumber = parse(u"ABC"); + const auto positiveNumber = parse(u"+10"); + const auto negativeNumber = parse(u"-10"); - auto info = NumberTypeInfo{}; + QVERIFY (!invalidNumber.has_value()); + QVERIFY (positiveNumber.has_value()); - info.hasSign = std::is_signed_v; - info.isFloat = std::is_floating_point_v; + QCOMPARE(positiveNumber.value(), static_cast(+10)); - info.parse = [](QStringView text) { - return QVariant::fromValue(parse(text)); - }; + if constexpr (hasNegativeNumbers) { + QVERIFY(negativeNumber.has_value()); + QCOMPARE(negativeNumber.value(), static_cast(-10)); + } else { + QVERIFY(!negativeNumber.has_value()); + } - info.hasValue = [](const QVariant &boxedOptional) { - return unbox(boxedOptional).has_value(); - }; + if constexpr (hasCustomBase) { + QCOMPARE(parse(u"21", 8), 17); + QCOMPARE(parse(u"21", 10), 21); + QCOMPARE(parse(u"21", 16), 33); + } - info.value = [](const QVariant &boxedOptional) { - if (const auto optional = unbox(boxedOptional)) - return QVariant::fromValue(optional.value()); - else - return QVariant{}; - }; + if constexpr (std::is_floating_point_v) { + const auto positiveFloat = parse(u"1.23"); + const auto negativeFloat = parse(u"-5e-3"); - if constexpr (std::is_integral_v) { - info.parseWithBase = [](QStringView text, int base) { - return QVariant::fromValue(parse(text, base)); - }; - } + QVERIFY (positiveFloat.has_value()); + QCOMPARE(positiveFloat.value(), static_cast(1.23)); - return info; -}; + QVERIFY (negativeFloat.has_value()); + QCOMPARE(negativeFloat.value(), static_cast(-5e-3)); + } + }; +} } // namespace } // namespace qnc::core::tests -Q_DECLARE_METATYPE(qnc::core::tests::NumberTypeInfo) - namespace qnc::core::tests { class ParseTest : public QObject @@ -84,39 +70,24 @@ class ParseTest : public QObject private slots: void testParseNumbers_data() { - QTest::addColumn("number"); - - QTest::newRow("short") << makeNumberTypeInfo(); - QTest::newRow("ushort") << makeNumberTypeInfo(); - QTest::newRow("int") << makeNumberTypeInfo(); - QTest::newRow("uint") << makeNumberTypeInfo(); - QTest::newRow("long") << makeNumberTypeInfo(); - QTest::newRow("ulong") << makeNumberTypeInfo(); - QTest::newRow("qlonglong") << makeNumberTypeInfo(); - QTest::newRow("qulonglong") << makeNumberTypeInfo(); - QTest::newRow("float") << makeNumberTypeInfo(); - QTest::newRow("double") << makeNumberTypeInfo(); + QTest::addColumn>("testFunction"); + + QTest::newRow("short") << makeTestParseNumbers(); + QTest::newRow("ushort") << makeTestParseNumbers(); + QTest::newRow("int") << makeTestParseNumbers(); + QTest::newRow("uint") << makeTestParseNumbers(); + QTest::newRow("long") << makeTestParseNumbers(); + QTest::newRow("ulong") << makeTestParseNumbers(); + QTest::newRow("qlonglong") << makeTestParseNumbers(); + QTest::newRow("qulonglong") << makeTestParseNumbers(); + QTest::newRow("float") << makeTestParseNumbers(); + QTest::newRow("double") << makeTestParseNumbers(); } void testParseNumbers() { - const QFETCH(NumberTypeInfo, number); - - QCOMPARE(number.hasValue(number.parse(u"ABC")), false); - QCOMPARE(number.hasValue(number.parse(u"+10")), true); - QCOMPARE(number.hasValue(number.parse(u"-10")), number.hasSign); - - QCOMPARE(number.value(number.parse(u"+10")), +10); - - if (number.hasSign) - QCOMPARE(number.value(number.parse(u"-10")), -10); - if (number.parseWithBase) - QCOMPARE(number.value(number.parseWithBase(u"21", 16)), 33); - - if (number.isFloat) { - QCOMPARE(number.value(number.parse(u"1.2")).toFloat(), 1.2f); - QCOMPARE(number.value(number.parse(u"-5e-3")).toFloat(), -5e-3f); - } + const QFETCH(std::function, testFunction); + testFunction(); } }; From 13ba6b7d7959cf68579a8a374cac26d5d9944664 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 22:59:34 +0200 Subject: [PATCH 5/7] Add more sophisticated number parsing tests --- tests/auto/tst_qncparse.cpp | 87 +++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/auto/tst_qncparse.cpp b/tests/auto/tst_qncparse.cpp index 89aaba2..f6bb22a 100644 --- a/tests/auto/tst_qncparse.cpp +++ b/tests/auto/tst_qncparse.cpp @@ -3,6 +3,7 @@ */ // QtNetworkCrumbs headers +#include "qncliterals.h" #include "qncparse.h" // Qt headers @@ -16,19 +17,93 @@ namespace { template constexpr bool hasNegativeNumbers = std::is_signed_v || std::is_same_v; template constexpr bool hasCustomBase = std::is_integral_v && !std::is_same_v; +template constexpr QStringView maximumText; +template <> constexpr QStringView maximumText = u"32767"; +template <> constexpr QStringView maximumText = u"2147483647"; +template <> constexpr QStringView maximumText = u"9223372036854775807"; +template <> constexpr QStringView maximumText = u"65535"; +template <> constexpr QStringView maximumText = u"4294967295"; +template <> constexpr QStringView maximumText = u"18446744073709551615"; +template <> constexpr QStringView maximumText = u"3.40282e+38"; +template <> constexpr QStringView maximumText = u"1.7976931348623e+308"; + +template constexpr QStringView minimumText; +template <> constexpr QStringView minimumText = u"-32768"; +template <> constexpr QStringView minimumText = u"-2147483648"; +template <> constexpr QStringView minimumText = u"-9223372036854775808"; +template <> constexpr QStringView minimumText = u"0"; +template <> constexpr QStringView minimumText = u"0"; +template <> constexpr QStringView minimumText = u"0"; +template <> constexpr QStringView minimumText = u"-3.40282e+38"; +template <> constexpr QStringView minimumText = u"-1.7976931348623e+308"; + +template +constexpr QStringView select(QStringView a, QStringView b) +{ + if constexpr (sizeof(T) == sizeof(A)) { + return a; + } else { + static_assert(sizeof(T) == sizeof(B)); + return b; + } +} + +template +constexpr QStringView selectMinimumText() { return select(minimumText, minimumText); } +template +constexpr QStringView selectMaximumText() { return select(maximumText, maximumText); } + +template <> constexpr QStringView maximumText = selectMaximumText(); +template <> constexpr QStringView minimumText = selectMinimumText(); +template <> constexpr QStringView maximumText = selectMaximumText(); +template <> constexpr QStringView minimumText = selectMinimumText(); + +QString incrementLastChar(QStringView text) +{ + if (text == u"0") + return u"-1"_s; + + auto result = text.toString(); + const auto back = result.end() - 1; + + if (!back->isDigit()) + return {}; + + *back = QChar{back->unicode() + 1}; + + return result; +} + template std::function makeTestParseNumbers() { return [] { + QVERIFY(!maximumText.isEmpty()); + QVERIFY(!minimumText.isEmpty()); + + auto aboveMaximumText = incrementLastChar(maximumText); // max. numbers for all int types end with 7 or 5 + auto belowMinimumText = incrementLastChar(minimumText); // min. numbers or int types all are 0 or end with 8 + const auto invalidNumber = parse(u"ABC"); const auto positiveNumber = parse(u"+10"); const auto negativeNumber = parse(u"-10"); + const auto maximumNumber = parse(maximumText); + const auto minimumNumber = parse(minimumText); + const auto aboveNumber = parse(aboveMaximumText); + const auto belowNumber = parse(belowMinimumText); QVERIFY (!invalidNumber.has_value()); QVERIFY (positiveNumber.has_value()); + QVERIFY2( maximumNumber.has_value(), qPrintable(maximumText.toString())); + QVERIFY2( minimumNumber.has_value(), qPrintable(minimumText.toString())); + QVERIFY2(! aboveNumber.has_value(), qPrintable(aboveMaximumText)); + QVERIFY2(! belowNumber.has_value(), qPrintable(belowMinimumText)); QCOMPARE(positiveNumber.value(), static_cast(+10)); + QCOMPARE( maximumNumber.value(), std::numeric_limits::max()); + QCOMPARE( minimumNumber.value(), std::numeric_limits::lowest()); + if constexpr (hasNegativeNumbers) { QVERIFY(negativeNumber.has_value()); QCOMPARE(negativeNumber.value(), static_cast(-10)); @@ -44,13 +119,25 @@ std::function makeTestParseNumbers() if constexpr (std::is_floating_point_v) { const auto positiveFloat = parse(u"1.23"); + const auto positiveInfinity = parse(u"inf"); const auto negativeFloat = parse(u"-5e-3"); + const auto negativeInfinity = parse(u"-inf"); + const auto nan = parse(u"nan"); QVERIFY (positiveFloat.has_value()); QCOMPARE(positiveFloat.value(), static_cast(1.23)); QVERIFY (negativeFloat.has_value()); QCOMPARE(negativeFloat.value(), static_cast(-5e-3)); + + QVERIFY(positiveInfinity.has_value()); + QVERIFY(std::isinf(positiveInfinity.value())); + + QVERIFY(negativeInfinity.has_value()); + QVERIFY(std::isinf(negativeInfinity.value())); + + QVERIFY(nan.has_value()); + QVERIFY(std::isnan(nan.value())); } }; } From 8c04231cda08fde9efb29894d42aec3776146b12 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 21:09:04 +0200 Subject: [PATCH 6/7] Add tiny library with test helpers --- tests/auto/CMakeLists.txt | 10 ++++- tests/auto/qnctestsupport.cpp | 77 +++++++++++++++++++++++++++++++++++ tests/auto/qnctestsupport.h | 69 +++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/auto/qnctestsupport.cpp create mode 100644 tests/auto/qnctestsupport.h diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt index 1338aff..1371783 100644 --- a/tests/auto/CMakeLists.txt +++ b/tests/auto/CMakeLists.txt @@ -21,9 +21,17 @@ function(add_testcase SOURCE_FILENAME) # [SOURCES...] add_test(NAME "${TESTCASE_NAME}" COMMAND ${TEST_RUNNER} "$") 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) diff --git a/tests/auto/qnctestsupport.cpp b/tests/auto/qnctestsupport.cpp new file mode 100644 index 0000000..6c4ef07 --- /dev/null +++ b/tests/auto/qnctestsupport.cpp @@ -0,0 +1,77 @@ +/* QtNetworkCrumbs - Some networking toys for Qt + * Copyright (C) 2019-2024 Mathias Hasselmann + */ +#include "qnctestsupport.h" + +// Qt headers +#include + +// STL headers +#include +#include + +namespace qnc::tests { +namespace { + +[[nodiscard]] std::string toStdString(long double value) +{ + const auto p = std::numeric_limits::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(&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); +} diff --git a/tests/auto/qnctestsupport.h b/tests/auto/qnctestsupport.h new file mode 100644 index 0000000..99bc297 --- /dev/null +++ b/tests/auto/qnctestsupport.h @@ -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 + +// STL headers +#include + +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(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(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(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(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(p1), static_cast(p2)); + } +} + +QDebug operator<<(QDebug debug, long double value); + +namespace qnc::tests { +bool initialize(); +} // namespace qnc::tests + +#endif // QNC_QNCTESTSUPPORT_H From 8d6df7142b79861246e475d94d9d9d19a45de936 Mon Sep 17 00:00:00 2001 From: Mathias Hasselmann Date: Thu, 19 Sep 2024 23:01:56 +0200 Subject: [PATCH 7/7] Add more generic number parsers --- qnc/qncparse.cpp | 94 +++++++++++++++++++++++++++++++++++++ qnc/qncparse.h | 19 ++++++++ tests/auto/tst_qncparse.cpp | 58 +++++++++++++++++++++++ 3 files changed, 171 insertions(+) diff --git a/qnc/qncparse.cpp b/qnc/qncparse.cpp index 99e20f9..be3d208 100644 --- a/qnc/qncparse.cpp +++ b/qnc/qncparse.cpp @@ -2,3 +2,97 @@ * Copyright (C) 2023 Mathias Hasselmann */ #include "qncparse.h" + +// STL headers +#include + +#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(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(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 diff --git a/qnc/qncparse.h b/qnc/qncparse.h index c867618..aafbb43 100644 --- a/qnc/qncparse.h +++ b/qnc/qncparse.h @@ -8,15 +8,33 @@ #include // STL headers +#include #include namespace qnc { namespace detail { +template +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::min()) + && Q_LIKELY(genericValue <= std::numeric_limits::max())) + value = static_cast(genericValue); + else + isValid = false; + } +} + template void parse(const S &, T &, bool &, Args...) = delete; +template void parse(const S &text, bool &value, bool &isValid); +template void parse(const S &text, qint8 &value, bool &isValid, int base = 10) { parseIndirect(text, value, isValid, base); } +template void parse(const S &text, quint8 &value, bool &isValid, int base = 10) { parseIndirect(text, value, isValid, base); } template void parse(const S &text, short &value, bool &isValid, int base = 10) { value = text.toShort (&isValid, base); } template void parse(const S &text, ushort &value, bool &isValid, int base = 10) { value = text.toUShort (&isValid, base); } template void parse(const S &text, int &value, bool &isValid, int base = 10) { value = text.toInt (&isValid, base); } @@ -27,6 +45,7 @@ template void parse(const S &text, qlonglong &value, bool &isValid, template void parse(const S &text, qulonglong &value, bool &isValid, int base = 10) { value = text.toULongLong (&isValid, base); } template void parse(const S &text, float &value, bool &isValid) { value = text.toFloat (&isValid); } template void parse(const S &text, double &value, bool &isValid) { value = text.toDouble (&isValid); } +template void parse(const S &text, long double &value, bool &isValid); template std::optional parse(const StringLike &text, Args &&...args) diff --git a/tests/auto/tst_qncparse.cpp b/tests/auto/tst_qncparse.cpp index f6bb22a..ac0e33b 100644 --- a/tests/auto/tst_qncparse.cpp +++ b/tests/auto/tst_qncparse.cpp @@ -5,35 +5,47 @@ // QtNetworkCrumbs headers #include "qncliterals.h" #include "qncparse.h" +#include "qnctestsupport.h" // Qt headers #include +// STL headers +#include + Q_DECLARE_METATYPE(std::function) namespace qnc::core::tests { namespace { +const auto s_initialized = qnc::tests::initialize(); + template constexpr bool hasNegativeNumbers = std::is_signed_v || std::is_same_v; template constexpr bool hasCustomBase = std::is_integral_v && !std::is_same_v; template constexpr QStringView maximumText; +template <> constexpr QStringView maximumText = u"127"; template <> constexpr QStringView maximumText = u"32767"; template <> constexpr QStringView maximumText = u"2147483647"; template <> constexpr QStringView maximumText = u"9223372036854775807"; +template <> constexpr QStringView maximumText = u"255"; template <> constexpr QStringView maximumText = u"65535"; template <> constexpr QStringView maximumText = u"4294967295"; template <> constexpr QStringView maximumText = u"18446744073709551615"; +template <> constexpr QStringView maximumText = u"true"; template <> constexpr QStringView maximumText = u"3.40282e+38"; template <> constexpr QStringView maximumText = u"1.7976931348623e+308"; template constexpr QStringView minimumText; +template <> constexpr QStringView minimumText = u"-128"; template <> constexpr QStringView minimumText = u"-32768"; template <> constexpr QStringView minimumText = u"-2147483648"; template <> constexpr QStringView minimumText = u"-9223372036854775808"; +template <> constexpr QStringView minimumText = u"0"; template <> constexpr QStringView minimumText = u"0"; template <> constexpr QStringView minimumText = u"0"; template <> constexpr QStringView minimumText = u"0"; +template <> constexpr QStringView minimumText = u"false"; template <> constexpr QStringView minimumText = u"-3.40282e+38"; template <> constexpr QStringView minimumText = u"-1.7976931348623e+308"; @@ -58,6 +70,14 @@ template <> constexpr QStringView minimumText = selectMinimumText constexpr QStringView maximumText = selectMaximumText(); template <> constexpr QStringView minimumText = selectMinimumText(); +template +constexpr QStringView selectMinimumText(QStringView b) { return select(minimumText, std::move(b)); } +template +constexpr QStringView selectMaximumText(QStringView b) { return select(maximumText, std::move(b)); } + +template <> constexpr QStringView maximumText = selectMaximumText(u"1.1897314953572317649e+4932"); +template <> constexpr QStringView minimumText = selectMinimumText(u"-1.1897314953572317649e+4932"); + QString incrementLastChar(QStringView text) { if (text == u"0") @@ -159,6 +179,9 @@ private slots: { QTest::addColumn>("testFunction"); + QTest::newRow("bool") << makeTestParseNumbers(); + QTest::newRow("qint8") << makeTestParseNumbers(); + QTest::newRow("quint8") << makeTestParseNumbers(); QTest::newRow("short") << makeTestParseNumbers(); QTest::newRow("ushort") << makeTestParseNumbers(); QTest::newRow("int") << makeTestParseNumbers(); @@ -169,6 +192,7 @@ private slots: QTest::newRow("qulonglong") << makeTestParseNumbers(); QTest::newRow("float") << makeTestParseNumbers(); QTest::newRow("double") << makeTestParseNumbers(); + QTest::newRow("longdouble") << makeTestParseNumbers(); } void testParseNumbers() @@ -176,6 +200,40 @@ private slots: const QFETCH(std::function, testFunction); testFunction(); } + + void testParseBool_data() + { + QTest::addColumn("text"); + QTest::addColumn("value"); + + QTest::newRow("enabled") << "Enabled" << true; + QTest::newRow("true") << "True" << true; + QTest::newRow("yes") << "Yes" << true; + QTest::newRow("on") << "On" << true; + QTest::newRow("1") << "1" << true; + QTest::newRow("-1") << "-1" << true; + + QTest::newRow("disabled") << "Disabled" << false; + QTest::newRow("false") << "False" << false; + QTest::newRow("no") << "No" << false; + QTest::newRow("off") << "Off" << false; + QTest::newRow("0") << "0" << false; + } + + void testParseBool() + { + const QFETCH(QString, text); + const QFETCH(bool, value); + + QVERIFY(parse(text).has_value()); + QCOMPARE(parse(text).value(), value); + + QVERIFY(parse(text.toLower()).has_value()); + QCOMPARE(parse(text.toLower()).value(), value); + + QVERIFY(parse(text.toUpper()).has_value()); + QCOMPARE(parse(text.toUpper()).value(), value); + } }; } // namespace qnc::core::tests