@longsightgroup/qti3-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +18 -3
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +191 -80
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +136 -3
- package/dist/validation.js.map +1 -1
- package/dist/value-format.d.ts +7 -0
- package/dist/value-format.d.ts.map +1 -0
- package/dist/value-format.js +46 -0
- package/dist/value-format.js.map +1 -0
- package/dist/xml.d.ts +2 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +2 -0
- package/dist/xml.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +7 -0
- package/src/parser.ts +19 -3
- package/src/session.ts +329 -270
- package/src/types.ts +3 -0
- package/src/validation.ts +151 -2
- package/src/value-format.ts +39 -0
- package/src/xml.ts +4 -0
package/src/session.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
QtiValue,
|
|
18
18
|
QtiVariableDeclaration,
|
|
19
19
|
} from "./types.js";
|
|
20
|
+
import { qtiScalarToString, qtiValueToString, qtiValueToStringList } from "./value-format.js";
|
|
20
21
|
|
|
21
22
|
export interface QtiCustomOperatorContext {
|
|
22
23
|
definition?: string | undefined;
|
|
@@ -64,8 +65,9 @@ export function visibleModalFeedback(
|
|
|
64
65
|
return item.modalFeedback.filter((feedback) => {
|
|
65
66
|
if (feedback.showHide === "hide") return false;
|
|
66
67
|
const outcome = outcomes[feedback.outcomeIdentifier];
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const outcomeValue: QtiValue = outcome === undefined ? null : outcome;
|
|
69
|
+
if (Array.isArray(outcomeValue)) return outcomeValue.includes(feedback.identifier);
|
|
70
|
+
return qtiValueToString(outcomeValue) === feedback.identifier;
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -84,6 +86,7 @@ export function createItemSession(
|
|
|
84
86
|
);
|
|
85
87
|
let validationMessages = cloneDiagnostics(priorState?.validationMessages ?? []);
|
|
86
88
|
const responses: Record<string, QtiValue> = {};
|
|
89
|
+
const responseDefaults: Record<string, QtiValue> = {};
|
|
87
90
|
const outcomes: Record<string, QtiValue> = {};
|
|
88
91
|
const templateValues: Record<string, QtiValue> = {};
|
|
89
92
|
const interactionStates: Record<string, QtiPortableCustomStateValue> = {};
|
|
@@ -100,8 +103,8 @@ export function createItemSession(
|
|
|
100
103
|
|
|
101
104
|
for (const declaration of document.item.responseDeclarations) {
|
|
102
105
|
correctResponses[declaration.identifier] = cloneValue(declaration.correctResponse);
|
|
103
|
-
if (declaration.defaultValue !== null
|
|
104
|
-
|
|
106
|
+
if (declaration.defaultValue !== null) {
|
|
107
|
+
responseDefaults[declaration.identifier] = cloneValue(declaration.defaultValue);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
for (const declaration of document.item.templateDeclarations) {
|
|
@@ -112,18 +115,21 @@ export function createItemSession(
|
|
|
112
115
|
}
|
|
113
116
|
outcomes[COMPLETION_STATUS] = COMPLETION_NOT_ATTEMPTED;
|
|
114
117
|
const baseResponses = cloneValueRecord(responses);
|
|
118
|
+
const baseResponseDefaults = cloneValueRecord(responseDefaults);
|
|
115
119
|
const baseOutcomes = cloneValueRecord(outcomes);
|
|
116
120
|
|
|
117
121
|
applyTemplateProcessing(
|
|
118
122
|
document,
|
|
119
123
|
templateValues,
|
|
120
124
|
responses,
|
|
125
|
+
responseDefaults,
|
|
121
126
|
outcomes,
|
|
122
127
|
correctResponses,
|
|
123
128
|
random,
|
|
124
129
|
customOperators,
|
|
125
130
|
new Set(),
|
|
126
131
|
baseResponses,
|
|
132
|
+
baseResponseDefaults,
|
|
127
133
|
baseOutcomes,
|
|
128
134
|
);
|
|
129
135
|
if (priorState) {
|
|
@@ -133,12 +139,14 @@ export function createItemSession(
|
|
|
133
139
|
document,
|
|
134
140
|
templateValues,
|
|
135
141
|
responses,
|
|
142
|
+
responseDefaults,
|
|
136
143
|
outcomes,
|
|
137
144
|
correctResponses,
|
|
138
145
|
random,
|
|
139
146
|
customOperators,
|
|
140
147
|
new Set(Object.keys(priorTemplateValues)),
|
|
141
148
|
baseResponses,
|
|
149
|
+
baseResponseDefaults,
|
|
142
150
|
baseOutcomes,
|
|
143
151
|
);
|
|
144
152
|
}
|
|
@@ -174,7 +182,7 @@ export function createItemSession(
|
|
|
174
182
|
},
|
|
175
183
|
score() {
|
|
176
184
|
const diagnostics: QtiDiagnostic[] = [];
|
|
177
|
-
if (document.item.adaptive || status !== "initialized"
|
|
185
|
+
if (document.item.adaptive || status !== "initialized") {
|
|
178
186
|
startAttempt();
|
|
179
187
|
}
|
|
180
188
|
const completionStatus = outcomes[COMPLETION_STATUS] ?? COMPLETION_NOT_ATTEMPTED;
|
|
@@ -218,6 +226,9 @@ export function createItemSession(
|
|
|
218
226
|
};
|
|
219
227
|
|
|
220
228
|
function startAttempt(): void {
|
|
229
|
+
for (const [identifier, value] of Object.entries(responseDefaults)) {
|
|
230
|
+
if (responses[identifier] === undefined) responses[identifier] = cloneValue(value);
|
|
231
|
+
}
|
|
221
232
|
if (status === "initialized" || status === "suspended") status = "interacting";
|
|
222
233
|
if (outcomes[COMPLETION_STATUS] === COMPLETION_NOT_ATTEMPTED) {
|
|
223
234
|
outcomes[COMPLETION_STATUS] = COMPLETION_UNKNOWN;
|
|
@@ -297,7 +308,9 @@ function assertCompatiblePriorState(
|
|
|
297
308
|
completionStatus !== COMPLETION_COMPLETED &&
|
|
298
309
|
completionStatus !== "incomplete"
|
|
299
310
|
) {
|
|
300
|
-
throw new Error(
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Cannot restore unsupported completionStatus ${qtiValueToString(completionStatus)}.`,
|
|
313
|
+
);
|
|
301
314
|
}
|
|
302
315
|
}
|
|
303
316
|
|
|
@@ -431,12 +444,14 @@ function applyTemplateProcessing(
|
|
|
431
444
|
document: QtiDocument,
|
|
432
445
|
templateValues: Record<string, QtiValue>,
|
|
433
446
|
responses: Record<string, QtiValue>,
|
|
447
|
+
responseDefaults: Record<string, QtiValue>,
|
|
434
448
|
outcomes: Record<string, QtiValue>,
|
|
435
449
|
correctResponses: Record<string, QtiValue>,
|
|
436
450
|
random: () => number,
|
|
437
451
|
customOperators: QtiCustomOperatorRegistry,
|
|
438
452
|
preservedTemplateIdentifiers = new Set<string>(),
|
|
439
453
|
baseResponses: Record<string, QtiValue> = cloneValueRecord(responses),
|
|
454
|
+
baseResponseDefaults: Record<string, QtiValue> = cloneValueRecord(responseDefaults),
|
|
440
455
|
baseOutcomes: Record<string, QtiValue> = cloneValueRecord(outcomes),
|
|
441
456
|
): void {
|
|
442
457
|
const rules = document.item.templateProcessing?.rules ?? [];
|
|
@@ -448,6 +463,7 @@ function applyTemplateProcessing(
|
|
|
448
463
|
document,
|
|
449
464
|
templateValues,
|
|
450
465
|
responses,
|
|
466
|
+
responseDefaults,
|
|
451
467
|
outcomes,
|
|
452
468
|
correctResponses,
|
|
453
469
|
random,
|
|
@@ -469,6 +485,7 @@ function applyTemplateProcessing(
|
|
|
469
485
|
if (!satisfied) {
|
|
470
486
|
resetTemplateValues(document, templateValues);
|
|
471
487
|
resetRecord(responses, cloneValueRecord(baseResponses));
|
|
488
|
+
resetRecord(responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
472
489
|
resetRecord(outcomes, cloneValueRecord(baseOutcomes));
|
|
473
490
|
resetCorrectResponses(document, correctResponses);
|
|
474
491
|
restarts += 1;
|
|
@@ -504,6 +521,7 @@ function applyTemplateRule(
|
|
|
504
521
|
document: QtiDocument,
|
|
505
522
|
templateValues: Record<string, QtiValue>,
|
|
506
523
|
responses: Record<string, QtiValue>,
|
|
524
|
+
responseDefaults: Record<string, QtiValue>,
|
|
507
525
|
outcomes: Record<string, QtiValue>,
|
|
508
526
|
correctResponses: Record<string, QtiValue>,
|
|
509
527
|
random: () => number,
|
|
@@ -552,6 +570,7 @@ function applyTemplateRule(
|
|
|
552
570
|
document,
|
|
553
571
|
templateValues,
|
|
554
572
|
responses,
|
|
573
|
+
responseDefaults,
|
|
555
574
|
outcomes,
|
|
556
575
|
correctResponses,
|
|
557
576
|
random,
|
|
@@ -583,7 +602,11 @@ function applyTemplateRule(
|
|
|
583
602
|
const responseDeclaration = getResponseDeclaration(document, rule.identifier);
|
|
584
603
|
if (responseDeclaration) {
|
|
585
604
|
const normalized = normalizeValueForCardinality(value, responseDeclaration.cardinality);
|
|
586
|
-
|
|
605
|
+
if (normalized === null) {
|
|
606
|
+
delete responseDefaults[rule.identifier];
|
|
607
|
+
} else {
|
|
608
|
+
responseDefaults[rule.identifier] = normalized;
|
|
609
|
+
}
|
|
587
610
|
return false;
|
|
588
611
|
}
|
|
589
612
|
const outcomeDeclaration = document.item.outcomeDeclarations.find(
|
|
@@ -628,33 +651,45 @@ function applyResponseProcessing(
|
|
|
628
651
|
return;
|
|
629
652
|
}
|
|
630
653
|
|
|
631
|
-
const
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
654
|
+
const templateKind = responseProcessingTemplateKind(processing?.template);
|
|
655
|
+
if (templateKind === "unsupported") {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (templateKind === "mapResponse" || templateKind === "mapResponsePoint") {
|
|
659
|
+
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
660
|
+
outcomes.SCORE = declaration
|
|
661
|
+
? mapOrMatchResponse(
|
|
662
|
+
declaration,
|
|
663
|
+
responses.RESPONSE ?? null,
|
|
664
|
+
correctResponses.RESPONSE ?? null,
|
|
665
|
+
)
|
|
666
|
+
: 0;
|
|
642
667
|
return;
|
|
643
668
|
}
|
|
644
669
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
670
|
+
if (templateKind === "matchCorrect") {
|
|
671
|
+
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
672
|
+
const matches = declaration
|
|
673
|
+
? qtiMatchValues(
|
|
674
|
+
responses.RESPONSE ?? null,
|
|
675
|
+
correctResponses.RESPONSE ?? null,
|
|
676
|
+
declaration.cardinality === "ordered",
|
|
677
|
+
)
|
|
678
|
+
: null;
|
|
679
|
+
outcomes.SCORE = matches === true ? 1 : 0;
|
|
656
680
|
}
|
|
657
|
-
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function responseProcessingTemplateKind(
|
|
684
|
+
template: string | undefined,
|
|
685
|
+
): "matchCorrect" | "mapResponse" | "mapResponsePoint" | "unsupported" | undefined {
|
|
686
|
+
if (!template) return undefined;
|
|
687
|
+
const path = template.split(/[?#]/, 1)[0] ?? "";
|
|
688
|
+
const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
|
|
689
|
+
if (name === "match_correct") return "matchCorrect";
|
|
690
|
+
if (name === "map_response") return "mapResponse";
|
|
691
|
+
if (name === "map_response_point") return "mapResponsePoint";
|
|
692
|
+
return "unsupported";
|
|
658
693
|
}
|
|
659
694
|
|
|
660
695
|
function applyResponseRules(
|
|
@@ -823,7 +858,7 @@ function evaluateValue(
|
|
|
823
858
|
if (expression.type === "matchCorrect") {
|
|
824
859
|
const declaration = getResponseDeclaration(document, expression.correctIdentifier);
|
|
825
860
|
return declaration
|
|
826
|
-
?
|
|
861
|
+
? qtiMatchValues(
|
|
827
862
|
responses[expression.identifier] ?? null,
|
|
828
863
|
correctResponses[expression.correctIdentifier] ?? null,
|
|
829
864
|
declaration.cardinality === "ordered",
|
|
@@ -831,27 +866,29 @@ function evaluateValue(
|
|
|
831
866
|
: false;
|
|
832
867
|
}
|
|
833
868
|
if (expression.type === "match") {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
869
|
+
const left = evaluateValue(
|
|
870
|
+
expression.left,
|
|
871
|
+
document,
|
|
872
|
+
responses,
|
|
873
|
+
outcomes,
|
|
874
|
+
templateValues,
|
|
875
|
+
correctResponses,
|
|
876
|
+
random,
|
|
877
|
+
customOperators,
|
|
878
|
+
);
|
|
879
|
+
const right = evaluateValue(
|
|
880
|
+
expression.right,
|
|
881
|
+
document,
|
|
882
|
+
responses,
|
|
883
|
+
outcomes,
|
|
884
|
+
templateValues,
|
|
885
|
+
correctResponses,
|
|
886
|
+
random,
|
|
887
|
+
customOperators,
|
|
888
|
+
);
|
|
889
|
+
return qtiMatchValues(
|
|
890
|
+
left,
|
|
891
|
+
right,
|
|
855
892
|
expressionIsOrdered(expression.left, document) ||
|
|
856
893
|
expressionIsOrdered(expression.right, document),
|
|
857
894
|
);
|
|
@@ -957,89 +994,57 @@ function evaluateValue(
|
|
|
957
994
|
).length;
|
|
958
995
|
}
|
|
959
996
|
if (expression.type === "sum") {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
templateValues,
|
|
970
|
-
correctResponses,
|
|
971
|
-
random,
|
|
972
|
-
customOperators,
|
|
973
|
-
),
|
|
974
|
-
),
|
|
975
|
-
0,
|
|
997
|
+
const values = evaluateNumericOperands(
|
|
998
|
+
expression.expressions,
|
|
999
|
+
document,
|
|
1000
|
+
responses,
|
|
1001
|
+
outcomes,
|
|
1002
|
+
templateValues,
|
|
1003
|
+
correctResponses,
|
|
1004
|
+
random,
|
|
1005
|
+
customOperators,
|
|
976
1006
|
);
|
|
1007
|
+
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
977
1008
|
}
|
|
978
1009
|
if (expression.type === "product") {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
templateValues,
|
|
989
|
-
correctResponses,
|
|
990
|
-
random,
|
|
991
|
-
customOperators,
|
|
992
|
-
),
|
|
993
|
-
),
|
|
994
|
-
1,
|
|
1010
|
+
const values = evaluateNumericOperands(
|
|
1011
|
+
expression.expressions,
|
|
1012
|
+
document,
|
|
1013
|
+
responses,
|
|
1014
|
+
outcomes,
|
|
1015
|
+
templateValues,
|
|
1016
|
+
correctResponses,
|
|
1017
|
+
random,
|
|
1018
|
+
customOperators,
|
|
995
1019
|
);
|
|
1020
|
+
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
996
1021
|
}
|
|
997
1022
|
if (expression.type === "min" || expression.type === "max") {
|
|
998
|
-
const values =
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
random,
|
|
1008
|
-
customOperators,
|
|
1009
|
-
),
|
|
1010
|
-
),
|
|
1023
|
+
const values = evaluateNumericOperands(
|
|
1024
|
+
expression.expressions,
|
|
1025
|
+
document,
|
|
1026
|
+
responses,
|
|
1027
|
+
outcomes,
|
|
1028
|
+
templateValues,
|
|
1029
|
+
correctResponses,
|
|
1030
|
+
random,
|
|
1031
|
+
customOperators,
|
|
1011
1032
|
);
|
|
1012
|
-
if (values.length === 0) return null;
|
|
1013
|
-
|
|
1014
|
-
return expression.type === "min" ? Math.min(...numericValues) : Math.max(...numericValues);
|
|
1033
|
+
if (!values || values.length === 0) return null;
|
|
1034
|
+
return expression.type === "min" ? Math.min(...values) : Math.max(...values);
|
|
1015
1035
|
}
|
|
1016
1036
|
if (expression.type === "subtract") {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
random,
|
|
1027
|
-
customOperators,
|
|
1028
|
-
),
|
|
1029
|
-
) -
|
|
1030
|
-
numericValue(
|
|
1031
|
-
evaluateValue(
|
|
1032
|
-
expression.right,
|
|
1033
|
-
document,
|
|
1034
|
-
responses,
|
|
1035
|
-
outcomes,
|
|
1036
|
-
templateValues,
|
|
1037
|
-
correctResponses,
|
|
1038
|
-
random,
|
|
1039
|
-
customOperators,
|
|
1040
|
-
),
|
|
1041
|
-
)
|
|
1037
|
+
const values = evaluateNumericOperands(
|
|
1038
|
+
[expression.left, expression.right],
|
|
1039
|
+
document,
|
|
1040
|
+
responses,
|
|
1041
|
+
outcomes,
|
|
1042
|
+
templateValues,
|
|
1043
|
+
correctResponses,
|
|
1044
|
+
random,
|
|
1045
|
+
customOperators,
|
|
1042
1046
|
);
|
|
1047
|
+
return values && values.length === 2 ? values[0]! - values[1]! : null;
|
|
1043
1048
|
}
|
|
1044
1049
|
if (expression.type === "divide") {
|
|
1045
1050
|
const dividendValue = evaluateValue(
|
|
@@ -1063,38 +1068,25 @@ function evaluateValue(
|
|
|
1063
1068
|
customOperators,
|
|
1064
1069
|
);
|
|
1065
1070
|
if (dividendValue === null || divisorValue === null) return null;
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1071
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
1072
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
1073
|
+
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1074
|
+
const quotient = dividend / divisor;
|
|
1069
1075
|
return Number.isFinite(quotient) ? quotient : null;
|
|
1070
1076
|
}
|
|
1071
1077
|
if (expression.type === "power") {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
random,
|
|
1082
|
-
customOperators,
|
|
1083
|
-
),
|
|
1084
|
-
),
|
|
1085
|
-
numericValue(
|
|
1086
|
-
evaluateValue(
|
|
1087
|
-
expression.right,
|
|
1088
|
-
document,
|
|
1089
|
-
responses,
|
|
1090
|
-
outcomes,
|
|
1091
|
-
templateValues,
|
|
1092
|
-
correctResponses,
|
|
1093
|
-
random,
|
|
1094
|
-
customOperators,
|
|
1095
|
-
),
|
|
1096
|
-
),
|
|
1078
|
+
const values = evaluateNumericOperands(
|
|
1079
|
+
[expression.left, expression.right],
|
|
1080
|
+
document,
|
|
1081
|
+
responses,
|
|
1082
|
+
outcomes,
|
|
1083
|
+
templateValues,
|
|
1084
|
+
correctResponses,
|
|
1085
|
+
random,
|
|
1086
|
+
customOperators,
|
|
1097
1087
|
);
|
|
1088
|
+
if (!values || values.length !== 2) return null;
|
|
1089
|
+
const value = Math.pow(values[0]!, values[1]!);
|
|
1098
1090
|
return Number.isFinite(value) ? value : null;
|
|
1099
1091
|
}
|
|
1100
1092
|
if (expression.type === "integerDivide") {
|
|
@@ -1119,9 +1111,10 @@ function evaluateValue(
|
|
|
1119
1111
|
customOperators,
|
|
1120
1112
|
);
|
|
1121
1113
|
if (dividendValue === null || divisorValue === null) return null;
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1114
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
1115
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
1116
|
+
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1117
|
+
return Math.floor(dividend / divisor);
|
|
1125
1118
|
}
|
|
1126
1119
|
if (expression.type === "integerModulus") {
|
|
1127
1120
|
const dividendValue = evaluateValue(
|
|
@@ -1145,29 +1138,28 @@ function evaluateValue(
|
|
|
1145
1138
|
customOperators,
|
|
1146
1139
|
);
|
|
1147
1140
|
if (dividendValue === null || divisorValue === null) return null;
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1141
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
1142
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
1143
|
+
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1151
1144
|
return dividend - Math.floor(dividend / divisor) * divisor;
|
|
1152
1145
|
}
|
|
1153
1146
|
if (expression.type === "round") {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
customOperators,
|
|
1165
|
-
),
|
|
1147
|
+
const value = numericValueOrNull(
|
|
1148
|
+
evaluateValue(
|
|
1149
|
+
expression.expression,
|
|
1150
|
+
document,
|
|
1151
|
+
responses,
|
|
1152
|
+
outcomes,
|
|
1153
|
+
templateValues,
|
|
1154
|
+
correctResponses,
|
|
1155
|
+
random,
|
|
1156
|
+
customOperators,
|
|
1166
1157
|
),
|
|
1167
1158
|
);
|
|
1159
|
+
return value === null ? null : Math.round(value);
|
|
1168
1160
|
}
|
|
1169
1161
|
if (expression.type === "roundTo") {
|
|
1170
|
-
const value =
|
|
1162
|
+
const value = numericValueOrNull(
|
|
1171
1163
|
evaluateValue(
|
|
1172
1164
|
expression.expression,
|
|
1173
1165
|
document,
|
|
@@ -1179,25 +1171,25 @@ function evaluateValue(
|
|
|
1179
1171
|
customOperators,
|
|
1180
1172
|
),
|
|
1181
1173
|
);
|
|
1174
|
+
if (value === null) return null;
|
|
1182
1175
|
return expression.roundingMode === "decimalPlaces"
|
|
1183
1176
|
? roundToDecimalPlaces(value, expression.figures)
|
|
1184
1177
|
: roundToSignificantFigures(value, expression.figures);
|
|
1185
1178
|
}
|
|
1186
1179
|
if (expression.type === "truncate") {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
customOperators,
|
|
1198
|
-
),
|
|
1180
|
+
const value = numericValueOrNull(
|
|
1181
|
+
evaluateValue(
|
|
1182
|
+
expression.expression,
|
|
1183
|
+
document,
|
|
1184
|
+
responses,
|
|
1185
|
+
outcomes,
|
|
1186
|
+
templateValues,
|
|
1187
|
+
correctResponses,
|
|
1188
|
+
random,
|
|
1189
|
+
customOperators,
|
|
1199
1190
|
),
|
|
1200
1191
|
);
|
|
1192
|
+
return value === null ? null : Math.trunc(value);
|
|
1201
1193
|
}
|
|
1202
1194
|
if (expression.type === "integerToFloat") {
|
|
1203
1195
|
const value = evaluateValue(
|
|
@@ -1210,11 +1202,12 @@ function evaluateValue(
|
|
|
1210
1202
|
random,
|
|
1211
1203
|
customOperators,
|
|
1212
1204
|
);
|
|
1213
|
-
return
|
|
1205
|
+
return numericValueOrNull(value);
|
|
1214
1206
|
}
|
|
1215
1207
|
if (expression.type === "and") {
|
|
1216
|
-
|
|
1217
|
-
|
|
1208
|
+
let sawNull = false;
|
|
1209
|
+
for (const item of expression.expressions) {
|
|
1210
|
+
const value = booleanValueOrNull(
|
|
1218
1211
|
evaluateValue(
|
|
1219
1212
|
item,
|
|
1220
1213
|
document,
|
|
@@ -1225,8 +1218,11 @@ function evaluateValue(
|
|
|
1225
1218
|
random,
|
|
1226
1219
|
customOperators,
|
|
1227
1220
|
),
|
|
1228
|
-
)
|
|
1229
|
-
|
|
1221
|
+
);
|
|
1222
|
+
if (value === false) return false;
|
|
1223
|
+
if (value === null) sawNull = true;
|
|
1224
|
+
}
|
|
1225
|
+
return sawNull ? null : true;
|
|
1230
1226
|
}
|
|
1231
1227
|
if (expression.type === "anyN") {
|
|
1232
1228
|
const min = indexValue(expression.min, outcomes, templateValues) ?? 0;
|
|
@@ -1250,8 +1246,9 @@ function evaluateValue(
|
|
|
1250
1246
|
return null;
|
|
1251
1247
|
}
|
|
1252
1248
|
if (expression.type === "or") {
|
|
1253
|
-
|
|
1254
|
-
|
|
1249
|
+
let sawNull = false;
|
|
1250
|
+
for (const item of expression.expressions) {
|
|
1251
|
+
const value = booleanValueOrNull(
|
|
1255
1252
|
evaluateValue(
|
|
1256
1253
|
item,
|
|
1257
1254
|
document,
|
|
@@ -1262,11 +1259,14 @@ function evaluateValue(
|
|
|
1262
1259
|
random,
|
|
1263
1260
|
customOperators,
|
|
1264
1261
|
),
|
|
1265
|
-
)
|
|
1266
|
-
|
|
1262
|
+
);
|
|
1263
|
+
if (value === true) return true;
|
|
1264
|
+
if (value === null) sawNull = true;
|
|
1265
|
+
}
|
|
1266
|
+
return sawNull ? null : false;
|
|
1267
1267
|
}
|
|
1268
1268
|
if (expression.type === "not") {
|
|
1269
|
-
|
|
1269
|
+
const value = booleanValueOrNull(
|
|
1270
1270
|
evaluateValue(
|
|
1271
1271
|
expression.expression,
|
|
1272
1272
|
document,
|
|
@@ -1278,30 +1278,30 @@ function evaluateValue(
|
|
|
1278
1278
|
customOperators,
|
|
1279
1279
|
),
|
|
1280
1280
|
);
|
|
1281
|
+
return value === null ? null : !value;
|
|
1281
1282
|
}
|
|
1282
1283
|
if (expression.type === "equal") {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
customOperators,
|
|
1293
|
-
),
|
|
1294
|
-
evaluateValue(
|
|
1295
|
-
expression.right,
|
|
1296
|
-
document,
|
|
1297
|
-
responses,
|
|
1298
|
-
outcomes,
|
|
1299
|
-
templateValues,
|
|
1300
|
-
correctResponses,
|
|
1301
|
-
random,
|
|
1302
|
-
customOperators,
|
|
1303
|
-
),
|
|
1284
|
+
const left = evaluateValue(
|
|
1285
|
+
expression.left,
|
|
1286
|
+
document,
|
|
1287
|
+
responses,
|
|
1288
|
+
outcomes,
|
|
1289
|
+
templateValues,
|
|
1290
|
+
correctResponses,
|
|
1291
|
+
random,
|
|
1292
|
+
customOperators,
|
|
1304
1293
|
);
|
|
1294
|
+
const right = evaluateValue(
|
|
1295
|
+
expression.right,
|
|
1296
|
+
document,
|
|
1297
|
+
responses,
|
|
1298
|
+
outcomes,
|
|
1299
|
+
templateValues,
|
|
1300
|
+
correctResponses,
|
|
1301
|
+
random,
|
|
1302
|
+
customOperators,
|
|
1303
|
+
);
|
|
1304
|
+
return left === null || right === null ? null : valuesEqual(left, right);
|
|
1305
1305
|
}
|
|
1306
1306
|
if (expression.type === "equalRounded") {
|
|
1307
1307
|
const left = evaluateValue(
|
|
@@ -1325,16 +1325,11 @@ function evaluateValue(
|
|
|
1325
1325
|
customOperators,
|
|
1326
1326
|
);
|
|
1327
1327
|
if (left === null || right === null) return null;
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
);
|
|
1333
|
-
const roundedRight = roundWithMode(
|
|
1334
|
-
numericValue(right),
|
|
1335
|
-
expression.roundingMode,
|
|
1336
|
-
expression.figures,
|
|
1337
|
-
);
|
|
1328
|
+
const leftNumber = numericValueOrNull(left);
|
|
1329
|
+
const rightNumber = numericValueOrNull(right);
|
|
1330
|
+
if (leftNumber === null || rightNumber === null) return null;
|
|
1331
|
+
const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
|
|
1332
|
+
const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
|
|
1338
1333
|
return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
|
|
1339
1334
|
}
|
|
1340
1335
|
if (expression.type === "numericCompare") {
|
|
@@ -1359,8 +1354,9 @@ function evaluateValue(
|
|
|
1359
1354
|
customOperators,
|
|
1360
1355
|
);
|
|
1361
1356
|
if (leftValue === null || rightValue === null) return null;
|
|
1362
|
-
const left =
|
|
1363
|
-
const right =
|
|
1357
|
+
const left = numericValueOrNull(leftValue);
|
|
1358
|
+
const right = numericValueOrNull(rightValue);
|
|
1359
|
+
if (left === null || right === null) return null;
|
|
1364
1360
|
if (expression.operator === "lt") return left < right;
|
|
1365
1361
|
if (expression.operator === "lte") return left <= right;
|
|
1366
1362
|
if (expression.operator === "gt") return left > right;
|
|
@@ -1462,7 +1458,9 @@ function evaluateValue(
|
|
|
1462
1458
|
templateValues[expression.pattern] ??
|
|
1463
1459
|
expression.pattern;
|
|
1464
1460
|
try {
|
|
1465
|
-
return new RegExp(
|
|
1461
|
+
return new RegExp(
|
|
1462
|
+
typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue),
|
|
1463
|
+
).test(qtiValueToString(value));
|
|
1466
1464
|
} catch {
|
|
1467
1465
|
return null;
|
|
1468
1466
|
}
|
|
@@ -1501,8 +1499,9 @@ function evaluateValue(
|
|
|
1501
1499
|
random,
|
|
1502
1500
|
customOperators,
|
|
1503
1501
|
);
|
|
1502
|
+
if (value === null || collection === null) return null;
|
|
1504
1503
|
const values = valueContainer(collection);
|
|
1505
|
-
return
|
|
1504
|
+
return values.some((item) => valuesEqual(item, value));
|
|
1506
1505
|
}
|
|
1507
1506
|
if (expression.type === "delete") {
|
|
1508
1507
|
const value = evaluateValue(
|
|
@@ -1515,47 +1514,46 @@ function evaluateValue(
|
|
|
1515
1514
|
random,
|
|
1516
1515
|
customOperators,
|
|
1517
1516
|
);
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
customOperators,
|
|
1528
|
-
),
|
|
1517
|
+
const collectionValue = evaluateValue(
|
|
1518
|
+
expression.collection,
|
|
1519
|
+
document,
|
|
1520
|
+
responses,
|
|
1521
|
+
outcomes,
|
|
1522
|
+
templateValues,
|
|
1523
|
+
correctResponses,
|
|
1524
|
+
random,
|
|
1525
|
+
customOperators,
|
|
1529
1526
|
);
|
|
1530
|
-
if (value === null ||
|
|
1527
|
+
if (value === null || collectionValue === null) return null;
|
|
1528
|
+
const collection = valueContainer(collectionValue);
|
|
1529
|
+
if (collection.length === 0) return null;
|
|
1531
1530
|
const filtered = collection.filter((item) => !valuesEqual(item, value));
|
|
1532
1531
|
return filtered.length > 0 ? filtered : null;
|
|
1533
1532
|
}
|
|
1534
1533
|
if (expression.type === "contains") {
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
customOperators,
|
|
1545
|
-
),
|
|
1534
|
+
const collectionValue = evaluateValue(
|
|
1535
|
+
expression.collection,
|
|
1536
|
+
document,
|
|
1537
|
+
responses,
|
|
1538
|
+
outcomes,
|
|
1539
|
+
templateValues,
|
|
1540
|
+
correctResponses,
|
|
1541
|
+
random,
|
|
1542
|
+
customOperators,
|
|
1546
1543
|
);
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
customOperators,
|
|
1557
|
-
),
|
|
1544
|
+
const valuesValue = evaluateValue(
|
|
1545
|
+
expression.values,
|
|
1546
|
+
document,
|
|
1547
|
+
responses,
|
|
1548
|
+
outcomes,
|
|
1549
|
+
templateValues,
|
|
1550
|
+
correctResponses,
|
|
1551
|
+
random,
|
|
1552
|
+
customOperators,
|
|
1558
1553
|
);
|
|
1554
|
+
if (collectionValue === null || valuesValue === null) return null;
|
|
1555
|
+
const collection = valueContainer(collectionValue);
|
|
1556
|
+
const values = valueContainer(valuesValue);
|
|
1559
1557
|
if (collection.length === 0 || values.length === 0) return null;
|
|
1560
1558
|
return containsValues(collection, values);
|
|
1561
1559
|
}
|
|
@@ -1769,7 +1767,11 @@ function scoreAreaMapping(
|
|
|
1769
1767
|
response: QtiValue,
|
|
1770
1768
|
areaMapping: NonNullable<QtiResponseDeclaration["areaMapping"]>,
|
|
1771
1769
|
): number {
|
|
1772
|
-
const points = Array.isArray(response)
|
|
1770
|
+
const points = Array.isArray(response)
|
|
1771
|
+
? response.map(qtiScalarToString)
|
|
1772
|
+
: response === null
|
|
1773
|
+
? []
|
|
1774
|
+
: qtiValueToStringList(response);
|
|
1773
1775
|
let score = 0;
|
|
1774
1776
|
for (const point of points) {
|
|
1775
1777
|
const parsed = parsePoint(String(point));
|
|
@@ -1907,7 +1909,10 @@ function scoreMapping(
|
|
|
1907
1909
|
);
|
|
1908
1910
|
return clampMappedScore(score, mapping.attributes);
|
|
1909
1911
|
}
|
|
1910
|
-
const score =
|
|
1912
|
+
const score =
|
|
1913
|
+
response === null || isRecordValue(response)
|
|
1914
|
+
? 0
|
|
1915
|
+
: (values[String(response)] ?? mapping.defaultValue);
|
|
1911
1916
|
return clampMappedScore(score, mapping.attributes);
|
|
1912
1917
|
}
|
|
1913
1918
|
|
|
@@ -1950,6 +1955,11 @@ function valuesEqual(actual: QtiValue, expected: QtiValue, ordered = false): boo
|
|
|
1950
1955
|
return scalarValuesEqual(actual, expected);
|
|
1951
1956
|
}
|
|
1952
1957
|
|
|
1958
|
+
function qtiMatchValues(actual: QtiValue, expected: QtiValue, ordered = false): boolean | null {
|
|
1959
|
+
if (actual === null || expected === null) return null;
|
|
1960
|
+
return valuesEqual(actual, expected, ordered);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1953
1963
|
function scalarValuesEqual(actual: QtiValue, expected: QtiValue): boolean {
|
|
1954
1964
|
if (typeof actual === "boolean" && typeof expected === "string") {
|
|
1955
1965
|
return String(actual) === expected;
|
|
@@ -2052,8 +2062,41 @@ function indexValue(
|
|
|
2052
2062
|
const parsed = Number(n);
|
|
2053
2063
|
if (Number.isInteger(parsed)) return parsed;
|
|
2054
2064
|
const value = outcomes[n] ?? templateValues[n] ?? null;
|
|
2055
|
-
const numeric =
|
|
2056
|
-
return Number.isInteger(numeric) ? numeric : undefined;
|
|
2065
|
+
const numeric = numericValueOrNull(value);
|
|
2066
|
+
return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function evaluateNumericOperands(
|
|
2070
|
+
expressions: QtiProcessingExpression[],
|
|
2071
|
+
document: QtiDocument,
|
|
2072
|
+
responses: Record<string, QtiValue>,
|
|
2073
|
+
outcomes: Record<string, QtiValue>,
|
|
2074
|
+
templateValues: Record<string, QtiValue>,
|
|
2075
|
+
correctResponses: Record<string, QtiValue>,
|
|
2076
|
+
random: () => number,
|
|
2077
|
+
customOperators: QtiCustomOperatorRegistry,
|
|
2078
|
+
): number[] | null {
|
|
2079
|
+
const numericValues: number[] = [];
|
|
2080
|
+
for (const expression of expressions) {
|
|
2081
|
+
const value = evaluateValue(
|
|
2082
|
+
expression,
|
|
2083
|
+
document,
|
|
2084
|
+
responses,
|
|
2085
|
+
outcomes,
|
|
2086
|
+
templateValues,
|
|
2087
|
+
correctResponses,
|
|
2088
|
+
random,
|
|
2089
|
+
customOperators,
|
|
2090
|
+
);
|
|
2091
|
+
if (value === null || isRecordValue(value)) return null;
|
|
2092
|
+
const values = Array.isArray(value) ? value : [value];
|
|
2093
|
+
for (const item of values) {
|
|
2094
|
+
const numeric = numericValueOrNull(item);
|
|
2095
|
+
if (numeric === null) return null;
|
|
2096
|
+
numericValues.push(numeric);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return numericValues;
|
|
2057
2100
|
}
|
|
2058
2101
|
|
|
2059
2102
|
function containsValues(collection: QtiScalarValue[], values: QtiScalarValue[]): boolean {
|
|
@@ -2077,6 +2120,16 @@ function numericValue(value: QtiValue): number {
|
|
|
2077
2120
|
return 0;
|
|
2078
2121
|
}
|
|
2079
2122
|
|
|
2123
|
+
function numericValueOrNull(value: QtiValue): number | null {
|
|
2124
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
2125
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
2126
|
+
if (typeof value === "string") {
|
|
2127
|
+
const parsed = Number(value);
|
|
2128
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2129
|
+
}
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2080
2133
|
function durationSeconds(value: QtiValue): number | null {
|
|
2081
2134
|
if (value === null || Array.isArray(value) || isRecordValue(value)) return null;
|
|
2082
2135
|
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
@@ -2107,6 +2160,11 @@ function booleanValue(value: QtiValue): boolean {
|
|
|
2107
2160
|
return false;
|
|
2108
2161
|
}
|
|
2109
2162
|
|
|
2163
|
+
function booleanValueOrNull(value: QtiValue): boolean | null {
|
|
2164
|
+
if (value === null) return null;
|
|
2165
|
+
return booleanValue(value);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2110
2168
|
function roundToDecimalPlaces(value: number, figures: number): number {
|
|
2111
2169
|
const factor = 10 ** figures;
|
|
2112
2170
|
return Math.round(value * factor) / factor;
|
|
@@ -2256,9 +2314,10 @@ function stringMatch(
|
|
|
2256
2314
|
right: QtiValue,
|
|
2257
2315
|
caseSensitive: boolean,
|
|
2258
2316
|
substring: boolean,
|
|
2259
|
-
): boolean {
|
|
2260
|
-
|
|
2261
|
-
let
|
|
2317
|
+
): boolean | null {
|
|
2318
|
+
if (left === null || right === null) return null;
|
|
2319
|
+
let actual = qtiValueToString(left);
|
|
2320
|
+
let expected = qtiValueToString(right);
|
|
2262
2321
|
if (!caseSensitive) {
|
|
2263
2322
|
actual = actual.toLocaleLowerCase();
|
|
2264
2323
|
expected = expected.toLocaleLowerCase();
|