15.1.794. tablet_qt/tasks/pcl5.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 "pcl5.h"
#include "core/camcopsapp.h"
#include "lib/version.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.h"
#include "maths/mathfunc.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/qumcqgrid.h"
#include "questionnairelib/qutext.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"

using mathfunc::countNull;
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_QUESTION_SCORE = 80;

const QString QPREFIX("q");
const QString Pcl5::PCL5_TABLENAME("pcl5");


void initializePcl5(TaskFactory& factory)
{
    static TaskRegistrar<Pcl5> registered(factory);
}


Pcl5::Pcl5(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
            Task(app, db, PCL5_TABLENAME, false, false, false),  // ... anon, clin, resp
            m_questionnaire(nullptr)
{
    addFields(strseq(QPREFIX, FIRST_Q, N_QUESTIONS), QMetaType::fromType<int>());

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


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

QString Pcl5::shortname() const
{
    return "PCL-5";
}


QString Pcl5::longname() const
{
    return tr("PTSD Checklist for DSM-5");
}


QString Pcl5::description() const
{
    return tr("20-item self-report scale, based on DSM-5 criteria.");
}


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


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

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


QStringList Pcl5::summary() const
{
    return QStringList{
        totalScorePhrase(totalScore(), MAX_QUESTION_SCORE),
        standardResult(xstring("dsm_criteria_met"),
                       uifunc::yesNoUnknown(hasPtsd()))
    };
}


QStringList Pcl5::detail() const
{
    QStringList lines = completenessInfo();
    lines += summary();
    return lines;
}


OpenableWidget* Pcl5::editor(const bool read_only)
{
    const NameValueOptions options{
        {xstring("a0"), 0},
        {xstring("a1"), 1},
        {xstring("a2"), 2},
        {xstring("a3"), 3},
        {xstring("a4"), 4},
    };

    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")),
    })->setTitle(xstring("title")));

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


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

int Pcl5::totalScore() const
{
    return sumInt(values(strseq(QPREFIX, FIRST_Q, N_QUESTIONS)));
}


QVariant Pcl5::hasPtsd() const
{
    // PTSD = at least one "B" item (questions 1-5)
    //        and at least one "C" item (questions 6-7)
    //        and at least two "D" items (questions 8-14)
    //        and at least two "E" items (questions 15-20)
    // https://www.ptsd.va.gov/professional/assessment/adult-sr/ptsd-checklist.asp

    const int first_b = 1;
    const int last_b = 5;
    const int first_c = 6;
    const int last_c = 7;
    const int first_d = 8;
    const int last_d = 14;
    const int first_e = 15;
    const int last_e = 20;

    const int criterion_b = 1;
    const int criterion_c = 1;
    const int criterion_d = 2;
    const int criterion_e = 2;

    const int symptomatic_b = numSymptomatic(first_b, last_b);
    const int symptomatic_c = numSymptomatic(first_c, last_c);
    const int symptomatic_d = numSymptomatic(first_d, last_d);
    const int symptomatic_e = numSymptomatic(first_e, last_e);

    const int null_b = numNull(first_b, last_b);
    const int null_c = numNull(first_c, last_c);
    const int null_d = numNull(first_d, last_d);
    const int null_e = numNull(first_e, last_e);

    if (symptomatic_b >= criterion_b &&
            symptomatic_c >= criterion_c &&
            symptomatic_d >= criterion_d &&
            symptomatic_e >= criterion_e) {
        return true;  // has PTSD
    }
    if (symptomatic_b + null_b >= criterion_b &&
            symptomatic_c + null_c >= criterion_c &&
            symptomatic_d + null_d >= criterion_d &&
            symptomatic_e + null_e >= criterion_e) {
        return QVariant();  // might have PTSD, depending on more info
    }
    return false;  // not PTSD
}


int Pcl5::numSymptomatic(const int first, const int last) const
{
    int total = 0;
    for (int i = first; i <= last; ++i) {
        if (valueInt(strnum(QPREFIX, i)) >= 2) {
            // 2 and above scores as "symptomatic":
            // https://www.ptsd.va.gov/professional/assessment/adult-sr/ptsd-checklist.asp
            ++total;
        }
    }
    return total;
}


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