15.1.209. tablet_qt/lib/numericfunc.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

#include "numericfunc.h"

#include <QString>

namespace numeric {


// ============================================================================
// Overloaded functions to convert to an integer type
// ============================================================================

int strToNumber(const QString& str, const int type_dummy)
{
    Q_UNUSED(type_dummy)
    return str.toInt();
}

qint64 strToNumber(const QString& str, const qint64 type_dummy)
{
    Q_UNUSED(type_dummy)
    return str.toLongLong();
}

quint64 strToNumber(const QString& str, const quint64 type_dummy)
{
    Q_UNUSED(type_dummy)
    return str.toULongLong();
}

int localeStrToNumber(
    const QString& str, bool& ok, const QLocale& locale, const int type_dummy
)
{
    Q_UNUSED(type_dummy)
    return locale.toInt(str, &ok);
}

qint64 localeStrToNumber(
    const QString& str,
    bool& ok,
    const QLocale& locale,
    const qint64 type_dummy
)
{
    Q_UNUSED(type_dummy)
    return locale.toLongLong(str, &ok);
}

quint64 localeStrToNumber(
    const QString& str,
    bool& ok,
    const QLocale& locale,
    const quint64 type_dummy
)
{
    Q_UNUSED(type_dummy)
    return locale.toULongLong(str, &ok);
}

// ============================================================================
// Numeric string representations
// ============================================================================

bool containsOnlySignOrZeros(const QString& number_string)
{
    if (number_string.isEmpty()) {
        return false;
    }
    const qsizetype length = number_string.length();
    for (qsizetype pos = 0; pos < length; ++pos) {
        const QChar c = number_string.at(pos);
        if (c != '0' && !(pos == 0 && (c == '-' || c == '+'))) {
            // Not a zero; not a leading sign.
            return false;
        }
    }
    return true;
}

// ============================================================================
// For double validation
// ============================================================================

QString getDefaultDecimalPoint()
{
    // https://doc.qt.io/qt-5/qlocale.html#QLocale
    // https://doc.qt.io/qt-5/qlocale.html#decimalPoint
    // ... changes from QChar to QString in Qt6
    return QLocale().decimalPoint();
}

bool isValidStartToDouble(
    const double number,
    const double bottom,
    const double top,
    const int max_dp,
    const QString& decimal_point
)
{
    // If you type more after "number", could you end up with a legitimate
    // value, in the range [bottom, top]?

    // 1. If "number" is negative and "bottom" is zero or positive, then
    //    "extended number" must always negative (because there must already be
    //    a minus sign at the start), and therefore always less than "bottom".
    if (number < 0 && bottom >= 0) {
#ifdef NUMERICFUNC_DEBUG_DETAIL
        qDebug() << Q_FUNC_INFO << number
                 << "invalid (negative and bottom >= 0)";
#endif
        return false;  // invalid
    }

    // 2. If "number" is positive and "top" is negative or zero, then "extended
    //    number" must always be positive (because there is no minus sign) and
    //    therefore always more than "top".
    if (number > 0 && top <= 0) {
#ifdef NUMERICFUNC_DEBUG_DETAIL
        qDebug() << Q_FUNC_INFO << number << "invalid (positive and top <= 0)";
#endif
        return false;  // invalid
    }

    // 3. Move into the positive domain to save brain ache.
    if (number >= 0) {
        // Number is already positive (or zero).
        // We already know that top > 0, and by definition bottom <= top.
#ifdef NUMERICFUNC_DEBUG_DETAIL
        qDebug() << Q_FUNC_INFO << number << "passing on positive/zero number";
#endif
        return isValidStartToPosDouble(
            number,  // already positive or zero
            std::max(0.0, bottom),  // makes it zero or positive
            top,  // already known to be positive
            max_dp,
            decimal_point
        );
    } else {
        // Number is negative.
        // We already know that bottom < 0, and by definition bottom <= top;
        // therefore, -top <= -bottom.
#ifdef NUMERICFUNC_DEBUG_DETAIL
        qDebug() << Q_FUNC_INFO << number << "passing on negative number";
#endif
        return isValidStartToPosDouble(
            -number,  // now positive
            std::max(0.0, -top),  // makes it zero or positive
            -bottom,
            max_dp,
            decimal_point
        );
    }
}

bool isValidStartToPosDouble(
    const double pos_number,
    const double pos_bottom,
    const double pos_top,
    const int max_dp,
    const QString& decimal_point
)
{
    // If you type more after "number", could you end up with a legitimate
    // value, in the range [bottom, top]?

    const int n_extra
        = maxExtraDigitsDouble(pos_number, pos_bottom, pos_top, max_dp);
    const QString str_number = QString("%1").arg(pos_number);
    const bool contains_dp = str_number.contains(decimal_point);

    // 1. If any extended version must be less than "bottom", it is invalid.
    // Check without adding a decimal point.
    const bool must_be_lt_bottom_noextradp
        = extendedPosDoubleMustBeLessThanBottom(
            pos_number, pos_bottom, n_extra, false, decimal_point
        );
    // Or with an extra decimal point, if applicable.
    const bool must_be_lt_bottom_extradp = contains_dp
        ? must_be_lt_bottom_noextradp
        : extendedPosDoubleMustBeLessThanBottom(
            pos_number, pos_bottom, n_extra, true, decimal_point
        );
    const bool must_be_lt_bottom
        = must_be_lt_bottom_noextradp && must_be_lt_bottom_noextradp;

    if (must_be_lt_bottom) {
#ifdef NUMERICFUNC_DEBUG_BASIC
        qDebug() << Q_FUNC_INFO << ": " << pos_number
                 << "when extended must be less than bottom value of"
                 << pos_bottom << "=> fail";
#endif
        return false;
    }

    // 2. If any extended version must be more than "top", it is invalid.
    // Check without adding a decimal point.
    const bool must_be_gt_top_noextradp = extendedPosDoubleMustExceedTop(
        pos_number, pos_top, n_extra, false, decimal_point
    );
    // Or with an extra decimal point, if applicable.
    const bool must_be_gt_top_extradp = contains_dp
        ? must_be_gt_top_noextradp
        : extendedPosDoubleMustExceedTop(
            pos_number, pos_top, n_extra, true, decimal_point
        );
    const bool must_be_gt_top
        = must_be_gt_top_noextradp && must_be_gt_top_extradp;

    if (must_be_gt_top) {
#ifdef NUMERICFUNC_DEBUG_BASIC
        qDebug() << Q_FUNC_INFO << ":" << pos_number
                 << "when extended must be more than top value of" << pos_top
                 << "=> fail";
#endif
        return false;
    }

    // 3. Check that we haven't allowed through obvious exclusionary
    // conditions.
    const bool no_extra_dp_ok
        = !must_be_lt_bottom_noextradp && !must_be_gt_top_noextradp;
    const bool extra_dp_ok
        = !must_be_lt_bottom_extradp && !must_be_gt_top_extradp;
    if (!no_extra_dp_ok && !extra_dp_ok) {
#ifdef NUMERICFUNC_DEBUG_BASIC
        qDebug().nospace() << Q_FUNC_INFO << ": " << pos_number
                           << "when extended must out of range [" << pos_bottom
                           << ", " << pos_top << "] => fail";
#endif
        return false;
    }

    // 4. By implication, there is a way of extending it that produces a number
    // that's >= bottom, and a way of extending that produces a number that's
    // <= top. It is not guaranteed that the same way of extenting satisfies
    // BOTH criteria. The only way to check that is recursion, which is very
    // slow.
#ifdef NUMERICFUNC_DEBUG_BASIC
    qDebug().nospace() << Q_FUNC_INFO << ": " << pos_number
                       << " is potentially OK for bottom " << pos_bottom
                       << ", top " << pos_top;
#endif
    return true;
}

bool extendedPosDoubleMustBeLessThanBottom(
    const double pos_number,
    const double pos_bottom,
    const int n_extra_digits,
    const bool add_dp,
    const QString& decimal_point
)
{
    // If you add extra digits to the number, must it be less than the bottom
    // value?
    // - All arguments are positive.

    // Try to extend, making the number as large as possible.
    // - Add a decimal point if our caller wants.
    //   That doesn't help us make it as large as possible, but our caller
    //   may have their reasons.
    QString str_number = QString("%1").arg(pos_number);
    if (add_dp && !str_number.contains(decimal_point)) {
        str_number += decimal_point;
    }
    const QString extension_digit = "9";  // make the largest possible number
#ifdef NUMERICFUNC_DEBUG_DETAIL
    qDebug().nospace() << Q_FUNC_INFO << "; pos_number = " << pos_number
                       << ", pos_bottom = " << pos_bottom
                       << ", n_extra = " << n_extra_digits;
#endif
    for (int i = 0; i < n_extra_digits; ++i) {
        str_number += extension_digit;
        if (str_number.toDouble() >= pos_bottom) {
            return false;
        }
    }
    return true;
}

bool extendedPosDoubleMustExceedTop(
    const double pos_number,
    const double pos_top,
    const int n_extra_digits,
    const bool add_dp,
    const QString& decimal_point
)
{
    // If you add extra digits to the number, must it exceed the top value?
    // - All arguments are positive.

    // 1. Adding digits to a positive integer can only make it larger.
    //    If "number" already exceeds "top", it will always do so.
    if (pos_number > pos_top) {
        return true;
    }

    // 2. Try to extend, making the number as small as possible.
    // - Add a decimal point if our caller wants.
    //   That helps us keep things as small as possible.
    QString str_number = QString("%1").arg(pos_number);
    if (add_dp && !str_number.contains(decimal_point)) {
        str_number += decimal_point;
    }
    const QString extension_digit = "0";  // make the smallest possible number
#ifdef NUMERICFUNC_DEBUG_DETAIL
    qDebug().nospace() << Q_FUNC_INFO << "; pos_number = " << pos_number
                       << ", pos_top = " << pos_top
                       << ", n_extra_digits = " << n_extra_digits;
#endif
    for (int i = 0; i < n_extra_digits; ++i) {
        str_number += extension_digit;
        if (str_number.toDouble() <= pos_top) {
            return false;  // an extended number does not exceed top
        }
    }
    return true;  // all extended versions exceed top
}

int numCharsDouble(const double number, const int max_dp, bool count_sign)
{
    const QString formatted = QString::number(number, 'f', max_dp);
    const bool sign_present = number < 0;
    const int length = formatted.length();
    const int nchars = (sign_present && !count_sign) ? length - 1 : length;
#ifdef NUMERICFUNC_DEBUG_DETAIL
    qDebug().nospace() << Q_FUNC_INFO << ": " << number << " formatted to "
                       << max_dp << " dp is " << formatted << "; nchars "
                       << nchars
                       << (count_sign ? " (inc. sign)" : " (exc. sign)");
#endif
    return nchars;
}

int maxExtraDigitsDouble(
    const double pos_number,
    const double pos_bottom,
    const double pos_top,
    const int max_dp
)
{
    // Follows logic of maxExtraDigits().
    int nd_number = numCharsDouble(pos_number, max_dp);
    if (pos_number == 0) {
        --nd_number;
    }
    const int max_nd_target = std::max(
        numCharsDouble(pos_bottom, max_dp), numCharsDouble(pos_top, max_dp)
    );
    return std::max(0, max_nd_target - nd_number);
}


}  // namespace numeric