14.1.544. tablet_qt/tasks/cesdr.cpp

#include "cesdr.h"
#include "core/camcopsapp.h"
#include "lib/version.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.h"
#include "maths/mathfunc.h"
#include "questionnairelib/namevaluepair.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/qumcqgrid.h"
#include "questionnairelib/qutext.h"
#include "questionnairelib/qutextedit.h"
#include "tasklib/taskfactory.h"
using mathfunc::countNull;
using mathfunc::countWhere;
using mathfunc::noneNull;
using mathfunc::sumInt;
using mathfunc::totalScorePhrase;
using stringfunc::standardResult;
using stringfunc::strnum;
using stringfunc::strseq;

const int FIRST_Q = 1;
const int N_QUESTIONS = 20;
const int MAX_SCORE = 60;

const int CAT_NONCLINICAL = 0;
const int CAT_SUB = 1;
const int CAT_POSS_MAJOR = 2;
const int CAT_PROB_MAJOR = 3;
const int CAT_MAJOR = 4;

const int DEPRESSION_RISK_THRESHOLD = 16;

const int FREQ_NOT_AT_ALL         = 0;
const int FREQ_1_2_DAYS_LAST_WEEK = 1;
const int FREQ_3_4_DAYS_LAST_WEEK = 2;
const int FREQ_5_7_DAYS_LAST_WEEK = 3;
const int FREQ_DAILY_2_WEEKS      = 4;

const int POSS_MAJOR_THRESH = 2;
const int PROB_MAJOR_THRESH = 3;
const int MAJOR_THRESH = 4;

const QString QPREFIX("q");
const QString Cesdr::CESDR_TABLENAME("cesdr");

void initializeCesdr(TaskFactory& factory)
{
    static TaskRegistrar<Cesdr> registered(factory);
}

Cesdr::Cesdr(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
            Task(app, db, CESDR_TABLENAME, false, false, false),
            m_questionnaire(nullptr)
{
    addFields(strseq(QPREFIX, FIRST_Q, N_QUESTIONS), QVariant::Int);

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

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

QString Cesdr::shortname() const
{
    return "CESD-R";
}


QString Cesdr::longname() const
{
    return tr("Center for Epidemiologic Studies Depression Scale (Revised)");
}


QString Cesdr::description() const
{
    return tr("20-item self-report depression scale.");
}


Version Cesdr::minimumServerVersion() const
{
    return Version(2, 2, 8);
}


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

bool Cesdr::isComplete() const
{
    return noneNull(values(strseq(QPREFIX, FIRST_Q, N_QUESTIONS)));
}


QStringList Cesdr::summary() const
{
    return QStringList{
        totalScorePhrase(totalScore(), MAX_SCORE),
        standardResult(xstring("depression_or_risk_of"),
                       uifunc::yesNoUnknown(totalScore() >= DEPRESSION_RISK_THRESHOLD)),
    };
}


QStringList Cesdr::detail() const
{
    QStringList lines = completenessInfo();
    lines += summary();

    const int cat = depressionCategory();
    lines.append("");
    lines.append(xstring("category_" + QString::number(cat)));

    return lines;
}


int Cesdr::depressionCategory() const
{
    /*
     * Determining CESD-R categories
     * See: https://cesd-r.com/cesdr/
     */

    if (totalScore() < DEPRESSION_RISK_THRESHOLD) {
        return CAT_SUB;
    }

    // DSM Categories
    const QVector<int> qs_dysphoria     = {2, 4, 6};
    const QVector<int> qs_anhedonia     = {8, 10};
    const QVector<int> qs_appetite      = {1, 18};
    const QVector<int> qs_sleep         = {5, 11, 19};
    const QVector<int> qs_thinking      = {3, 20};
    const QVector<int> qs_guilt         = {9, 17};
    const QVector<int> qs_tired         = {7, 16};
    const QVector<int> qs_movement      = {12, 13};
    const QVector<int> qs_suicidal      = {14, 15};
    const QVector<QVector<int>> non_anhedonia_groups{
        qs_appetite, qs_sleep, qs_movement, qs_tired, qs_guilt,
        qs_thinking, qs_suicidal
    };

    // Dysphoria or anhedonia must be present at frequency FREQ_DAILY_2_WEEKS
    const bool anhedonia_criterion = fulfilsGroupCriteria(qs_dysphoria, true) ||
                                     fulfilsGroupCriteria(qs_anhedonia, true);
    if (anhedonia_criterion) {
        int categoryCountHighFrequency = 0;
        int categoryCountLowerFrequency = 0;
        for (auto qgroup : non_anhedonia_groups) {
            if (fulfilsGroupCriteria(qgroup, true)) {
                // Category contains an answer == FREQ_DAILY_2_WEEKS
                categoryCountHighFrequency += 1;
            }
            if (fulfilsGroupCriteria(qgroup, false)) {
                // Category contains an answer == FREQ_DAILY_2_WEEKS or FREQ_5_7_DAYS_LAST_WEEK
                categoryCountLowerFrequency += 1;
            }
        }

        if (categoryCountHighFrequency >= MAJOR_THRESH) {
            // Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS)
            // plus 4 other symptom groups at FREQ_DAILY_2_WEEKS
            return CAT_MAJOR;
        } else if (categoryCountLowerFrequency >= PROB_MAJOR_THRESH) {
            // Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS)
            // plus 3 other symptom groups at FREQ_DAILY_2_WEEKS or FREQ_5_7_DAYS_LAST_WEEK
            return CAT_PROB_MAJOR;
        } else if (categoryCountLowerFrequency >= POSS_MAJOR_THRESH) {
            // Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS)
            // plus 2 other symptom groups at FREQ_DAILY_2_WEEKS or FREQ_5_7_DAYS_LAST_WEEK
            return CAT_POSS_MAJOR;
        }
    }

    const int cesd_score = totalScore();
    if (cesd_score >= DEPRESSION_RISK_THRESHOLD) {
        // Total CESD-style score >= 16 but doesn't meet other criteria.
        return CAT_SUB;
    }
    return CAT_NONCLINICAL;
}


OpenableWidget* Cesdr::editor(const bool read_only)
{
    const NameValueOptions options{
        {xstring("a0"), FREQ_NOT_AT_ALL},
        {xstring("a1"), FREQ_1_2_DAYS_LAST_WEEK},
        {xstring("a2"), FREQ_3_4_DAYS_LAST_WEEK},
        {xstring("a3"), FREQ_5_7_DAYS_LAST_WEEK},
        {xstring("a4"), FREQ_DAILY_2_WEEKS},
    };

    const int question_width = 50;
    const QVector<int> option_widths{10, 10, 10, 10, 10};

    QuPagePtr page((new QuPage{
        new QuText(xstring("instructions")),
        (new QuMcqGrid(
            {
                QuestionWithOneField(xstring("q1"), fieldRef("q1")),
                QuestionWithOneField(xstring("q2"), fieldRef("q2")),
                QuestionWithOneField(xstring("q3"), fieldRef("q3")),
                QuestionWithOneField(xstring("q4"), fieldRef("q4")),
                QuestionWithOneField(xstring("q5"), fieldRef("q5")),
                QuestionWithOneField(xstring("q6"), fieldRef("q6")),
                QuestionWithOneField(xstring("q7"), fieldRef("q7")),
                QuestionWithOneField(xstring("q8"), fieldRef("q8")),
                QuestionWithOneField(xstring("q9"), fieldRef("q9")),
                QuestionWithOneField(xstring("q10"), fieldRef("q10")),
                QuestionWithOneField(xstring("q11"), fieldRef("q11")),
                QuestionWithOneField(xstring("q12"), fieldRef("q12")),
                QuestionWithOneField(xstring("q13"), fieldRef("q13")),
                QuestionWithOneField(xstring("q14"), fieldRef("q14")),
                QuestionWithOneField(xstring("q15"), fieldRef("q15")),
                QuestionWithOneField(xstring("q16"), fieldRef("q16")),
                QuestionWithOneField(xstring("q17"), fieldRef("q17")),
                QuestionWithOneField(xstring("q18"), fieldRef("q18")),
                QuestionWithOneField(xstring("q19"), fieldRef("q19")),
                QuestionWithOneField(xstring("q20"), fieldRef("q20"))
            },
            options
        ))
        ->setTitle(xstring("stem"))
        ->setWidth(question_width, option_widths)
        ->setExpand(true)
        ->setQuestionsBold(false)
    })->setTitle(xstring("title")));

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


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

bool Cesdr::fulfilsGroupCriteria(const QVector<int>& qnums,
                                 bool nearly_every_day_2w) const
{
    for (const int qnum : qnums) {
        const int v = valueInt(stringfunc::strnum("q", qnum));
        if (v == FREQ_DAILY_2_WEEKS) {
            return true;
        }
        if (v == FREQ_5_7_DAYS_LAST_WEEK && !nearly_every_day_2w) {
            // A lower threshold for some symptoms, when nearly_every_day_2w
            // is false.
            return true;
        }
    }
    return false;
}


int Cesdr::totalScore() const
{
    // So that the CESD-R has the same range as the CESD (the "CESD-style
    // score"), the values for the top two responses are given the same value.
    // See: https://cesd-r.com/cesdr/

    QVector<QVariant> responses = values(strseq(QPREFIX, FIRST_Q, N_QUESTIONS));

    // Sum the response values, and subtract the count of answers marked as
    // occurring daily. Makes the 5-7 and daily responses value-quivalent, so
    // scoring is out of 60 and comparable to CESD.
    return sumInt(responses) - countWhere(responses,
                                          QVector<QVariant>{FREQ_DAILY_2_WEEKS});
}


int Cesdr::numNull(const int first, const int last) const
{
    return countNull(values(strseq(QPREFIX, first, last)));
}