/*
* Reads z-Tree questionnaires (.ztq), written from z-Tree's example (brand16.ztq, file version
* 11, and as z-Tree 5 saves it, version 46): questionnaires, each a page of items (texts, and questions with a variable, its range and
* its labels) and a button. As a treatment (see toTreatment), they run as any treatment does:
* one stage a questionnaire, which each subject takes at their own pace.
*/
const { tokenize, Reader, ZttError } = require('./ztt.js');
/** The questionnaires in a .ztq file: {version, questionnaires: [{name, title, items, buttons}]}. */
function readQuestionnaires(buf) {
// z-Tree's text export of them (see ztext.js).
const ztext = require('./ztext.js');
if (ztext.isText(buf)) return ztext.readTextQuestionnaires(buf);
const version = buf.readInt32LE(0);
const tokens = tokenize(buf);
if (!tokens.some((t) => t.cls === 'EEXQuesterQuestionnaire')) {
throw new ZttError('not a z-Tree questionnaire file (no EEXQuesterQuestionnaire)');
}
const out = { version, questionnaires: [], warnings: [] };
let current = null;
for (const t of tokens) {
const r = new Reader(buf, t.body, t.end);
try {
if (t.cls === 'EEXQuesterQuestionnaire') {
current = { name: r.str(), title: r.str(), items: [], buttons: [] };
out.questionnaires.push(current);
} else if (t.cls === 'EEXQuesterItem' && current != null) {
const item = { label: r.str(), type: r.i32(), variable: r.str() };
if (item.variable) {
r.skip(6);
// Its range and step: floats (doubles from file version 46, z-Tree 5).
const real = () => {
const v = version >= 46 ? r.buf.readDoubleLE(r.pos) : r.buf.readFloatLE(r.pos);
r.skip(version >= 46 ? 8 : 4);
return v;
};
item.min = real();
item.max = real();
item.step = real();
item.layout = r.i32();
r.skip(4);
// The labels of the range's ends, a line each.
item.labels = r.str().split('\n').map((l) => l.trim()).filter(Boolean);
}
current.items.push(item);
} else if (t.cls === 'EEXQuesterButton' && current != null) {
current.buttons.push({ name: r.str() });
} else if (/^EEXQuester/.test(t.cls)) {
out.warnings.push('an object of class ' + t.cls + ' (at byte ' + t.start + ') is not read');
}
} catch (err) {
if (!(err instanceof ZttError)) throw err;
out.warnings.push(t.cls + ' at byte ' + t.start + ': ' + err.message);
}
}
return out;
}
const round = (x) => Math.round(x * 1e6) / 1e6;
/**
* The questionnaires as a treatment (as ztt.js describes one): a stage for each, its items in a
* standard box: texts, and questions as a line of choices from min to max (labelled at its ends)
* or a number field; and its button, or OK.
*/
function toTreatment(q) {
const box = (questionnaire) => ({
type: 'standard', cls: 'EEXDialogWindow', name: questionnaire.title || questionnaire.name,
condition: '', left: '', right: '', width: '', top: '', bottom: '', height: '',
boxes: [], programs: [],
items: questionnaire.items.map((item) => {
if (!item.variable) return { kind: 'item', label: item.label, variable: '', input: false, min: '', max: '', layout: '' };
const steps = item.step > 0 ? Math.round((item.max - item.min) / item.step) + 1 : 0;
const [left, right] = [item.labels[0] || '', item.labels[item.labels.length - 1] || ''];
const layout = steps >= 2 && steps <= 21
? '!radioline: ' + round(item.min) + ' = "' + left.replace(/"/g, "'") + '"; ' + round(item.max) + ' = "' + right.replace(/"/g, "'") + '"; ' + steps
: String(item.step > 0 ? round(item.step) : 1);
return { kind: 'item', label: item.label, variable: item.variable, input: true, min: String(round(item.min)), max: String(round(item.max)), layout };
}),
buttons: questionnaire.items.length === 0 ? [] : (questionnaire.buttons.length ? questionnaire.buttons : [{ name: 'OK' }]).map((b) => ({ name: b.name, checkers: [], programs: [] })),
});
const stage = (questionnaire) => ({
name: questionnaire.title || questionnaire.name,
// Start if possible (bit 0): each subject at their own pace; no timeout.
programs: [], timeout: '', options: [1, 0, 0, 0, 0, 0, 0, 0], showHeader: false, startIfPossible: true, timeoutAlways: false,
active: [box(questionnaire)], waiting: [],
});
const waitingText = { type: 'standard', cls: 'EEXDialogWindow', name: 'Text', condition: '', left: '', right: '', width: '', top: '', bottom: '', height: '',
boxes: [], programs: [], buttons: [], items: [{ kind: 'item', label: 'Please wait.', variable: '', input: false, min: '', max: '', layout: '' }] };
return {
version: q.version,
questionnaire: true,
tables: [],
background: { name: 'Background', programs: [], timeout: '', options: [], showHeader: false, active: [], waiting: [waitingText] },
stages: q.questionnaires.map(stage),
periods: [{ name: '1', program: '' }],
subjects: [{ name: 'S 1', index: 0 }],
params: [{ label: '', group: 1, program: '' }],
warnings: q.warnings,
texts: [],
};
}
module.exports = { readQuestionnaires, toTreatment };