15.1.208. tablet_qt/lib/numericfunc.h

/*
    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/>.
*/

#pragma once

// #define NUMERICFUNC_DEBUG_VALIDATOR

#ifdef NUMERICFUNC_DEBUG_VALIDATOR
#include <QDebug>
#endif
#include <QLocale>
#include <QString>
#include <QValidator>

namespace numeric {

// Since there are many integer types, we templatize these:

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

// Converts a string containing a decimal integer to that integer.
// We offer this function for a variety of types, so our templatized functions
// can find what they want.
int strToNumber(const QString& str, int type_dummy);
qint64 strToNumber(const QString& str, qint64 type_dummy);
quint64 strToNumber(const QString& str, quint64 type_dummy);

// Similarly for locale-based strings containing integers, for different
// languages/conventions; see https://doc.qt.io/qt-6.5/qlocale.html
int localeStrToNumber(const QString& str, bool& ok,
                      const QLocale& locale, int type_dummy);
qint64 localeStrToNumber(const QString&, bool& ok,
                         const QLocale& locale, qint64 type_dummy);
quint64 localeStrToNumber(const QString&, bool& ok,
                          const QLocale& locale, quint64 type_dummy);

// ============================================================================
// Digit counting; first n digits
// ============================================================================

// Counts the number of digits in an integer type.
template<typename T>
int numDigitsInteger(const T& number, bool count_sign = false);

// Returns the first n_digits of an integer, as an integer.
template<typename T>
int firstDigitsInteger(const T& number, int n_digits);

// If you add extra digits to the number to make it as long as it could be,
// must it exceed the top value?
template<typename T>
bool extendedIntegerMustExceedTop(const T& number, const T& bottom,
                                  const T& top);

// If you add extra digits to the number to make it as long as it could be,
// must it be less than the bottom value?
template<typename T>
bool extendedIntegerMustBeLessThanBottom(const T& number, const T& bottom,
                                         const T& top);

// ============================================================================
// For integer validation
// ============================================================================

// Is number an integer that is a valid start to typing a number between
// min and max (inclusive)?
template<typename T>
bool isValidStartToInteger(const T& number, const T& bottom, const T& top);

// Validates an integer.
template<typename T>
QValidator::State validateInteger(const QString& s, const QLocale& locale,
                                  const T& bottom, const T& top,
                                  bool allow_empty);

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

// Counts the number of digits in a floating-point number.
// - ignores sign
// - includes decimal point
int numDigitsDouble(double number, int max_dp = 50);

// Returns the first n_digits of a floating point number, as a double.
// - sign is ignored (can't compare numbers without dropping it)
// - includes decimal point
double firstDigitsDouble(double number, int n_digits, int max_dp = 50);

// Is "number" something you could validly type to end up with a number in the
// range [bottom, top]?
bool isValidStartToDouble(double number, double bottom, double top);

// If you made "number" longer, would it necessarily exceed "top"?
bool extendedDoubleMustExceedTop(double number, double bottom, double top);

// If you made "number" longer, would it necessarily be below "bottom"?
bool extendedDoubleMustBeLessThanBottom(double number, double bottom, double top);

}  // namespace numeric


// ============================================================================
// Templated functions, first declared above
// ============================================================================

template<typename T>
int numeric::numDigitsInteger(const T& number, bool count_sign)
{
    // Counts the number of digits in an integer type.
    int digits = 0;
    if (number < 0 && count_sign) {
        digits = 1;
    }
    T working = number;
    while (working) {
        // don't use while (number); consider double number = 0.05...
        working /= 10;  // assumes base 10
        ++digits;
    }
    return digits;
}


template<typename T>
int numeric::firstDigitsInteger(const T& number, int n_digits)
{
    // Returns the first n_digits of an integer, as an integer.
    int current_digits = numDigitsInteger(number);
    T working = number;
    while (current_digits > n_digits) {
        working /= 10;  // assumes base 10
        --current_digits;
    }
    return working;
}


template<typename T>
bool numeric::extendedIntegerMustExceedTop(const T& number,
                                           const T& bottom,
                                           const T& top)
{
    // If you add extra digits to the number to make it as long as it could be,
    // must it exceed the top value?
    T type_dummy = 0;
    if (number < 0 && top > 0) {
        return false;
    }
    if (number > 0 && top < 0) {
        return true;
    }
    int nd_number = numDigitsInteger(number);
    QString str_number = QString("%1").arg(number);
    if (number > 0) {
        // Both positive. Extend with zeros, to length of nd_top
        int nd_top = numDigitsInteger(top);
        for (int i = 0; i < nd_top - nd_number; ++i) {
            str_number += "0";
            if (strToNumber(str_number, type_dummy) <= top) {
                return false;
            }
        }
        return true;
    } else {
        // Both negative. Extend with nines, to length of nd_bottom
        int nd_bottom = numDigitsInteger(bottom);
        for (int i = 0; i < nd_bottom - nd_number; ++i) {
            str_number += "9";
            if (strToNumber(str_number, type_dummy) <= top) {
                return false;
            }
        }
        return true;
    }
}


template<typename T>
bool numeric::extendedIntegerMustBeLessThanBottom(const T& number,
                                                  const T& bottom,
                                                  const T& top)
{
    // If you add extra digits to the number to make it as long as it could be,
    // must it be less than bottom?
    T type_dummy = 0;
    if (number < 0 && bottom > 0) {
        return true;
    }
    if (number > 0 && bottom < 0) {
        return false;
    }
    int nd_number = numDigitsInteger(number);
    QString str_number = QString("%1").arg(number);
    if (number > 0) {
        // Both positive. Extend with nines, to length of top
        int nd_top = numDigitsInteger(top);
        for (int i = 0; i < nd_top - nd_number; ++i) {
            str_number += "9";
            if (strToNumber(str_number, type_dummy) >= bottom) {
                return false;
            }
        }
        return true;
    } else {
        // Both negative. Extend with zeros, to length of bottom
        int nd_bottom = numDigitsInteger(bottom);
        for (int i = 0; i < nd_bottom - nd_number; ++i) {
            str_number += "0";
            if (strToNumber(str_number, type_dummy) >= bottom) {
                return false;
            }
        }
        return true;
    }
}


template<typename T>
bool numeric::isValidStartToInteger(const T& number, const T& bottom,
                                    const T& top)
{
    // Is number an integer that is a valid start to typing a number between
    // min and max (inclusive)?

    /*
    Tricky! No proper way to do it just by looking at the first n digits of
    the boundaries:

    bottom  top     bottom_start    top_start   possibilities   description

    10      30      1               3           1-3 yes         >= bottom_start && <= top_start
                                                4-9 no          > top_start (3)

    30      100     3               1           1 yes           >= bottom_start || <= top_start
                                                2 no            < bottom_start (3) && > top_start (1)
                                                3-9 yes         >= bottom_start || <= top_start

    20      30      2               3           1 no            < bottom_start (2)
                                                2-3 yes         >= bottom_start && <= top_start
                                                4-9 no          > top_start (3)

    30      100     30              10          3_: 0-9 yes     >= bottom_start (30) || <= top_start (10)
                                                1_: 0 yes       >= bottom_start (30) || <= top_start (10)
                                                    1-9 no      > top_start

    But then:

    100     30000   10              30          5_: 0-9 OK (e.g. 500-599)

    70      300     7               3           0-3, 7-9 OK
    */

    if (extendedIntegerMustBeLessThanBottom(number, bottom, top)) {
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << number
                 << "when extended must be less than bottom value of"
                 << bottom << "=> fail";
#endif
        return false;
    }
    if (extendedIntegerMustExceedTop(number, bottom, top)) {
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << number
                 << "when extended must be more than top value of"
                 << top << "=> fail";
#endif
        return false;
    }
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
    qDebug() << Q_FUNC_INFO << number << "is OK for bottom"
             << bottom << "top" << top;
#endif
    return true;
}

template<typename T>
QValidator::State numeric::validateInteger(
        const QString& s, const QLocale& locale,
        const T& bottom, const T& top,
        bool allow_empty)
{
    if (s.isEmpty()) {
        if (allow_empty) {
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
            qDebug() << Q_FUNC_INFO << "empty -> Acceptable (as allow_empty)";
#endif
            return QValidator::Acceptable;
        }
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "empty -> Intermediate";
#endif
        return QValidator::Intermediate;
    }

    const QString decimalPoint = locale.decimalPoint();
    if (s.indexOf(decimalPoint) != -1) {
        // Containing a decimal point: not OK
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "decimal point -> Invalid";
#endif
        return QValidator::Invalid;
    }

    if ((bottom < 0 || top < 0) && s == "-") {
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "plain -, negatives OK -> Intermediate";
#endif
        return QValidator::Intermediate;
    }
    if ((bottom > 0 || top > 0) && s == "+") {
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "plain +, positives OK -> Intermediate";
#endif
        return QValidator::Intermediate;
    }

    bool ok = true;
    const T type_dummy = 0;
    const T i = localeStrToNumber(s, ok, locale, type_dummy);  // NB: ok modified
    if (!ok) {  // Not an integer.
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "not an integer -> Invalid";
#endif
        return QValidator::Invalid;
    }

    if (i >= bottom && i <= top) {  // Perfect.
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "in range -> Acceptable";
#endif
        return QValidator::Acceptable;
    }

    // "Negative zero" is a special case -- a string starting with "-" that
    // evaluates to zero, like "-0" or "--0". The presence of the minus sign
    // can't be detected in the numeric version, so we have to handle it
    // specially.
    if (s.startsWith("-") && i == 0) {
        // If we get here, we already know that negative numbers are OK.
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO << "negative zero -> Intermediate";
#endif
        return QValidator::Intermediate;
    }

    // Is the number on its way to being something valid, or is it already
    // outside the permissible range?
    if (numeric::isValidStartToInteger(i, bottom, top)) {
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
        qDebug() << Q_FUNC_INFO
                 << "within range for number of digits -> Intermediate;"
                 << "s" << s;
#endif
        return QValidator::Intermediate;
    }
#ifdef NUMERICFUNC_DEBUG_VALIDATOR
    qDebug() << Q_FUNC_INFO << "end of function -> Invalid;"
             << "s" << s;
#endif
    return QValidator::Invalid;
}