15.1.789. tablet_qt/tasks/nart.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/>.
*/

/*
National Adult Reading Test (NART).
Copyright © Hazel E. Nelson. Used with permission; see documentation.
*/

#include "nart.h"

#include "lib/convert.h"
#include "lib/stringfunc.h"
#include "maths/mathfunc.h"
#include "questionnairelib/commonoptions.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/qugridcontainer.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/qutext.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"

const QString Nart::NART_TABLENAME("nart");

// Most of the NART is hard-coded as it is language-specific by its nature
const QStringList WORDLIST{
    "chord",     "ache",        "depot",     "aisle",     "bouquet",
    "psalm",     "capon",
    "deny",  // NB reserved word in SQL (auto-handled)
    "nausea",    "debt",        "courteous", "rarefy",    "equivocal",
    "naive",  // accent required
    "catacomb",  "gaoled",      "thyme",     "heir",      "radix",
    "assignate", "hiatus",      "subtle",    "procreate", "gist",
    "gouge",     "superfluous", "simile",    "banal",     "quadruped",
    "cellist",
    "facade",  // accent required
    "zealot",    "drachm",      "aeon",      "placebo",   "abstemious",
    "detente",  // accent required
    "idyll",     "puerperal",   "aver",      "gauche",    "topiary",
    "leviathan", "beatify",     "prelate",   "sidereal",  "demesne",
    "syncope",   "labile",      "campanile",
};
static QStringList ACCENTED_WORDLIST;
const int DP = 1;
const QString NART_INSTRUCTION(
    "Give the subject a piece of paper with the NART word list "
    "on. Follow the instructions in the Task Information. Use "
    "the list below to score. You may find it quickest to mark "
    "errors as the subject reads, then fill in correct answers "
    "at the end."
);

void initializeNart(TaskFactory& factory)
{
    static TaskRegistrar<Nart> registered(factory);

    ACCENTED_WORDLIST = WORDLIST;
    ACCENTED_WORDLIST.replace(WORDLIST.indexOf("naive"), "naïve");
    ACCENTED_WORDLIST.replace(WORDLIST.indexOf("facade"), "façade");
    ACCENTED_WORDLIST.replace(WORDLIST.indexOf("detente"), "détente");
}

Nart::Nart(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
    Task(app, db, NART_TABLENAME, false, true, false)  // ... anon, clin, resp
{
    addFields(WORDLIST, QMetaType::fromType<bool>());

    load(load_pk);  // MUST ALWAYS CALL from derived Task constructor.
}

// ============================================================================
// Class info
// ============================================================================

QString Nart::shortname() const
{
    return "NART";
}

QString Nart::longname() const
{
    return tr("National Adult Reading Test");
}

QString Nart::description() const
{
    return tr("Estimation of premorbid IQ by reading irregular words.");
}

// ============================================================================
// Instance info
// ============================================================================

bool Nart::isComplete() const
{
    return mathfunc::noneNull(values(WORDLIST));
}

QStringList Nart::summary() const
{
    const bool complete = isCompleteCached();
    const int errors = numErrors();
    return QStringList{
        result(nelsonFullScaleIQ(complete, errors), false),
        result(nelsonWillisonFullScaleIQ(complete, errors), false),
        result(brightFullScaleIQ(complete, errors), false),
    };
}

QStringList Nart::detail() const
{
    const bool complete = isCompleteCached();
    const int errors = numErrors();
    QStringList wordresults{"Words correct?"};
    for (int i = 0; i < ACCENTED_WORDLIST.length(); ++i) {
        const QString& fieldname = WORDLIST.at(i);
        const QString& word = ACCENTED_WORDLIST.at(i);
        wordresults.append(
            stringfunc::standardResult(word, prettyValue(fieldname))
        );
    }
    return completenessInfo() + wordresults
        + QStringList{
            stringfunc::standardResult(
                "Number of errors", QString::number(errors)
            ),
            "",
            result(nelsonFullScaleIQ(complete, errors)),
            result(nelsonVerbalIQ(complete, errors)),
            result(nelsonPerformanceIQ(complete, errors)),
            "",
            result(nelsonWillisonFullScaleIQ(complete, errors)),
            "",
            result(brightFullScaleIQ(complete, errors)),
            result(brightGeneralAbility(complete, errors)),
            result(brightVerbalComprehension(complete, errors)),
            result(brightPerceptualReasoning(complete, errors)),
            result(brightWorkingMemory(complete, errors)),
            result(brightPerceptualSpeed(complete, errors)),
        };
}

OpenableWidget* Nart::editor(const bool read_only)
{
    const NameValueOptions options = CommonOptions::incorrectCorrectBoolean();

    QVector<QuGridCell> cells;
    const int row_span = 1;
    const int col_span = 1;
    const Qt::Alignment align = Qt::AlignLeft | Qt::AlignVCenter;
    int row = 0;
    for (int i = 0; i < ACCENTED_WORDLIST.length(); ++i) {
        const QString& fieldname = WORDLIST.at(i);
        const QString& word = ACCENTED_WORDLIST.at(i).toUpper();
        auto el_word = new QuText(word);
        el_word->setBold();
        auto el_mcq = new QuMcq(fieldRef(fieldname), options);
        el_mcq->setHorizontal(true);
        cells.append(QuGridCell(el_word, row, 0, row_span, col_span, align));
        cells.append(QuGridCell(el_mcq, row, 1, row_span, col_span, align));
        ++row;
    }

    QuPagePtr page(new QuPage{
        getClinicianQuestionnaireBlockRawPointer(),
        new QuText(NART_INSTRUCTION),
        (new QuGridContainer(cells))
            ->setExpandHorizontally(false)
            ->setFixedGrid(false),
    });
    page->setTitle(longname());

    auto questionnaire = new Questionnaire(m_app, {page});
    questionnaire->setType(QuPage::PageType::Clinician);
    questionnaire->setReadOnly(read_only);
    return questionnaire;
}

// ============================================================================
// Task-specific calculations
// ============================================================================

int Nart::numErrors() const
{
    return mathfunc::countFalse(values(WORDLIST));
}

const QString NELSON_1982("Nelson 1982");
const QString NELSON_WILLISON_1991("Nelson & Willison 1991");
const QString BRIGHT_2016("Bright 2016, PMID 27624393");
#define IF_COMPLETE(complete, x) ((complete) ? (x) : QVariant())

Nart::NartIQ
    Nart::nelsonFullScaleIQ(const bool complete, const int errors) const
{
    // Figures from partial PDF of Nelson 1982
    return NartIQ(
        "Predicted WAIS full-scale IQ",
        NELSON_1982,
        "127.7 – 0.826 × errors",
        IF_COMPLETE(complete, 127.7 - 0.826 * errors)
    );
}

Nart::NartIQ Nart::nelsonVerbalIQ(const bool complete, const int errors) const
{
    // Figures from partial PDF of Nelson 1982
    return NartIQ(
        "Predicted WAIS verbal IQ",
        NELSON_1982,
        "129.0 – 0.919 × errors",
        IF_COMPLETE(complete, 129.0 - 0.919 * errors)
    );
}

Nart::NartIQ
    Nart::nelsonPerformanceIQ(const bool complete, const int errors) const
{
    // Figures from partial PDF of Nelson 1982
    return NartIQ(
        "Predicted WAIS performance IQ",
        NELSON_1982,
        "123.5 – 0.645 × errors",
        IF_COMPLETE(complete, 123.5 - 0.645 * errors)
    );
}

Nart::NartIQ Nart::nelsonWillisonFullScaleIQ(
    const bool complete, const int errors
) const
{
    // Figures from Bright 2016
    return NartIQ(
        "Predicted WAIS-R full-scale IQ",
        NELSON_WILLISON_1991,
        "130.6 – 1.24 × errors",
        IF_COMPLETE(complete, 130.6 - 1.24 * errors)
    );
}

Nart::NartIQ
    Nart::brightFullScaleIQ(const bool complete, const int errors) const
{
    return NartIQ(
        "Predicted WAIS-IV full-scale IQ",
        BRIGHT_2016,
        "126.41 – 0.9775 × errors",
        IF_COMPLETE(complete, 126.41 - 0.9775 * errors)
    );
}

Nart::NartIQ
    Nart::brightGeneralAbility(const bool complete, const int errors) const
{
    return NartIQ(
        "Predicted WAIS-IV General Ability Index",
        BRIGHT_2016,
        "126.5 – 0.9656 × errors",
        IF_COMPLETE(complete, 126.5 - 0.9656 * errors)
    );
}

Nart::NartIQ Nart::brightVerbalComprehension(
    const bool complete, const int errors
) const
{
    return NartIQ(
        "Predicted WAIS-IV Verbal Comprehension Index",
        BRIGHT_2016,
        "126.81 – 1.0745 × errors",
        IF_COMPLETE(complete, 126.81 - 1.0745 * errors)
    );
}

Nart::NartIQ Nart::brightPerceptualReasoning(
    const bool complete, const int errors
) const
{
    return NartIQ(
        "Predicted WAIS-IV Perceptual Reasoning Index",
        BRIGHT_2016,
        "120.18 – 0.6242 × errors",
        IF_COMPLETE(complete, 120.18 - 0.6242 * errors)
    );
}

Nart::NartIQ
    Nart::brightWorkingMemory(const bool complete, const int errors) const
{
    return NartIQ(
        "Predicted WAIS-IV Working Memory Index",
        BRIGHT_2016,
        "120.53 – 0.7901 × errors",
        IF_COMPLETE(complete, 120.53 - 0.7901 * errors)
    );
}

Nart::NartIQ
    Nart::brightPerceptualSpeed(const bool complete, const int errors) const
{
    return NartIQ(
        "Predicted WAIS-IV Perceptual Speed Index",
        BRIGHT_2016,
        "114.53 – 0.5285 × errors",
        IF_COMPLETE(complete, 114.53 - 0.5285 * errors)
    );
}

QString Nart::result(const NartIQ& iq, const bool full) const
{
    const QString name = full
        ? QString("%1 (%2; %3)").arg(iq.quantity, iq.reference, iq.formula)
        : QString("%1").arg(iq.quantity);
    const QString value = convert::prettyValue(iq.iq, DP);
    return stringfunc::standardResult(name, value);
}