15.1.378. tablet_qt/qobjects/proquintvalidator.cpp

/*
    Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).

    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 <http://www.gnu.org/licenses/>.
*/

#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QString>
#include <QUrl>
#include <QValidator>

#include "proquintvalidator.h"


ProquintValidator::ProquintValidator(QObject* parent) : QValidator(parent)
{
}

QValidator::State ProquintValidator::validate(QString& input, int&) const
{
    const int max_len = 8 * 6 + 1;  // 8 groups of 5-and-dash, then check
    if (input.length() > max_len) {
        // Even though that is invalid, don't return "Invalid" -- Invalid is
        // treated very harshly and you can't (for example) copy/paste in
        // things that are too long (the paste operation simply does nothing)
        // which is more confusing for users than showing the "bad colour".
        return QValidator::Intermediate;
    }
    const QString consonant = "[bdfghjklmnprstvz]";
    const QString vowel = "[aiou]";
    const QString quint = QString("%1%2%3%4%5").arg(
        consonant, vowel, consonant, vowel, consonant
    );
    const QString check_character = consonant;
    const QRegularExpression proquint_regex(
        QString("%1-%2-%3-%4-%5-%6-%7-%8-%9").arg(
            quint,quint,quint,quint,quint,quint,quint,quint,check_character
        )
    );

    // Note: a proquint group (5 letters: consonant/vowel/con/vo/con).
    // represents 16 bits: https://arxiv.org/html/0901.4016
    // ("Proquint" = "pronoucable quintuplet".)
    // The 8-proquint version here represents 128 bits (plus a checksum).

    const QRegularExpressionMatch match = proquint_regex.match(input);

    if (!match.hasMatch()) {
        return QValidator::Intermediate;
    }

    if (!validateLuhnMod16(input)) {
        return QValidator::Intermediate;
    }

    return QValidator::Acceptable;
}

bool ProquintValidator::validateLuhnMod16(const QString& input) const
{
    const QMap<QChar, int> lookup_table {
        {'b', 0x0},
        {'d', 0x1},
        {'f', 0x2},
        {'g', 0x3},
        {'h', 0x4},
        {'j', 0x5},
        {'k', 0x6},
        {'l', 0x7},
        {'m', 0x8},
        {'n', 0x9},
        {'p', 0xa},
        {'r', 0xb},
        {'s', 0xc},
        {'t', 0xd},
        {'v', 0xe},
        {'z', 0xf},
        {'a', 0x0},
        {'i', 0x1},
        {'o', 0x2},
        {'u', 0x3},
    };

    // https://en.wikipedia.org/wiki/Luhn_mod_N_algorithm
    const QString proquint = input.trimmed().replace("-", "");

    int factor = 1;
    int sum = 0;

    for (QString::const_reverse_iterator it = proquint.rbegin();
            it != proquint.rend(); ++it) {
        const int value = lookup_table.value(*it) * factor;
        sum += (value / 16 + value % 16);

        factor = (factor == 2) ? 1 : 2;
    }

    const int remainder = sum % 16;

    return remainder == 0;
}