Skip to content

Commit

Permalink
Add more generic number parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
hasselmm committed Sep 19, 2024
1 parent 2e7ffa2 commit ce63687
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 0 deletions.
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
19 changes: 19 additions & 0 deletions qnc/qncparse.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,33 @@
#include <QString>

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

namespace qnc {

namespace detail {

template <class S, typename 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); }
Expand All @@ -27,6 +45,7 @@ template <class S> void parse(const S &text, qlonglong &value, bool &isValid,
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)
Expand Down
58 changes: 58 additions & 0 deletions tests/auto/tst_qncparse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,47 @@
// QtNetworkCrumbs headers
#include "qncliterals.h"
#include "qncparse.h"
#include "qnctestsupport.h"

// Qt headers
#include <QTest>

// STL headers
#include <cmath>

Q_DECLARE_METATYPE(std::function<void()>)

namespace qnc::core::tests {
namespace {

const auto s_initialized = qnc::tests::initialize();

template <typename T> constexpr bool hasNegativeNumbers = std::is_signed_v<T> || std::is_same_v<T, bool>;
template <typename T> constexpr bool hasCustomBase = std::is_integral_v<T> && !std::is_same_v<T, bool>;

template <typename T> constexpr QStringView maximumText;
template <> constexpr QStringView maximumText <qint8> = u"127";
template <> constexpr QStringView maximumText <qint16> = u"32767";
template <> constexpr QStringView maximumText <qint32> = u"2147483647";
template <> constexpr QStringView maximumText <qint64> = u"9223372036854775807";
template <> constexpr QStringView maximumText <quint8> = u"255";
template <> constexpr QStringView maximumText <quint16> = u"65535";
template <> constexpr QStringView maximumText <quint32> = u"4294967295";
template <> constexpr QStringView maximumText <quint64> = u"18446744073709551615";
template <> constexpr QStringView maximumText <bool> = u"true";
template <> constexpr QStringView maximumText <float> = u"3.40282e+38";
template <> constexpr QStringView maximumText <double> = u"1.7976931348623e+308";

template <typename T> constexpr QStringView minimumText;
template <> constexpr QStringView minimumText <qint8> = u"-128";
template <> constexpr QStringView minimumText <qint16> = u"-32768";
template <> constexpr QStringView minimumText <qint32> = u"-2147483648";
template <> constexpr QStringView minimumText <qint64> = u"-9223372036854775808";
template <> constexpr QStringView minimumText <quint8> = u"0";
template <> constexpr QStringView minimumText <quint16> = u"0";
template <> constexpr QStringView minimumText <quint32> = u"0";
template <> constexpr QStringView minimumText <quint64> = u"0";
template <> constexpr QStringView minimumText <bool> = u"false";
template <> constexpr QStringView minimumText <float> = u"-3.40282e+38";
template <> constexpr QStringView minimumText <double> = u"-1.7976931348623e+308";

Expand All @@ -58,6 +70,14 @@ template <> constexpr QStringView minimumText<long> = selectMinimumText<long,
template <> constexpr QStringView maximumText<ulong> = selectMaximumText<ulong, quint32, quint64>();
template <> constexpr QStringView minimumText<ulong> = selectMinimumText<ulong, quint32, quint64>();

template <typename T, typename A>
constexpr QStringView selectMinimumText(QStringView b) { return select<T, A, T>(minimumText<A>, std::move(b)); }
template <typename T, typename A>
constexpr QStringView selectMaximumText(QStringView b) { return select<T, A, T>(maximumText<A>, std::move(b)); }

template <> constexpr QStringView maximumText<long double> = selectMaximumText<long double, double>(u"1.1897314953572317649e+4932");
template <> constexpr QStringView minimumText<long double> = selectMinimumText<long double, double>(u"-1.1897314953572317649e+4932");

QString incrementLastChar(QStringView text)
{
if (text == u"0")
Expand Down Expand Up @@ -159,6 +179,9 @@ private slots:
{
QTest::addColumn<std::function<void()>>("testFunction");

QTest::newRow("bool") << makeTestParseNumbers<bool>();
QTest::newRow("qint8") << makeTestParseNumbers<qint8>();
QTest::newRow("quint8") << makeTestParseNumbers<quint8>();
QTest::newRow("short") << makeTestParseNumbers<short>();
QTest::newRow("ushort") << makeTestParseNumbers<ushort>();
QTest::newRow("int") << makeTestParseNumbers<int>();
Expand All @@ -169,13 +192,48 @@ private slots:
QTest::newRow("qulonglong") << makeTestParseNumbers<qulonglong>();
QTest::newRow("float") << makeTestParseNumbers<float>();
QTest::newRow("double") << makeTestParseNumbers<double>();
QTest::newRow("longdouble") << makeTestParseNumbers<long double>();
}

void testParseNumbers()
{
const QFETCH(std::function<void()>, testFunction);
testFunction();
}

void testParseBool_data()
{
QTest::addColumn<QString>("text");
QTest::addColumn<bool>("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<bool>(text).has_value());
QCOMPARE(parse<bool>(text).value(), value);

QVERIFY(parse<bool>(text.toLower()).has_value());
QCOMPARE(parse<bool>(text.toLower()).value(), value);

QVERIFY(parse<bool>(text.toUpper()).has_value());
QCOMPARE(parse<bool>(text.toUpper()).value(), value);
}
};

} // namespace qnc::core::tests
Expand Down

0 comments on commit ce63687

Please sign in to comment.