@longsightgroup/qti3-core 0.9.2 → 0.9.4
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/README.md +108 -11
- package/dist/adaptive-turn-materializer.d.ts +2 -0
- package/dist/adaptive-turn-materializer.d.ts.map +1 -1
- package/dist/adaptive-turn-materializer.js +25 -19
- package/dist/adaptive-turn-materializer.js.map +1 -1
- package/dist/adaptive-turn.d.ts +8 -1
- package/dist/adaptive-turn.d.ts.map +1 -1
- package/dist/adaptive-turn.js +15 -5
- package/dist/adaptive-turn.js.map +1 -1
- package/dist/assert-never.d.ts +3 -0
- package/dist/assert-never.d.ts.map +1 -0
- package/dist/assert-never.js +5 -0
- package/dist/assert-never.js.map +1 -0
- package/dist/attempt-state-constants.d.ts +6 -0
- package/dist/attempt-state-constants.d.ts.map +1 -0
- package/dist/attempt-state-constants.js +6 -0
- package/dist/attempt-state-constants.js.map +1 -0
- package/dist/attempt-state.d.ts +5 -0
- package/dist/attempt-state.d.ts.map +1 -0
- package/dist/attempt-state.js +190 -0
- package/dist/attempt-state.js.map +1 -0
- package/dist/catalog.js +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +3 -0
- package/dist/content-text.js.map +1 -1
- package/dist/custom-operators.d.ts +13 -0
- package/dist/custom-operators.d.ts.map +1 -0
- package/dist/custom-operators.js +2 -0
- package/dist/custom-operators.js.map +1 -0
- package/dist/delivery-preparation.d.ts +20 -0
- package/dist/delivery-preparation.d.ts.map +1 -0
- package/dist/delivery-preparation.js +71 -0
- package/dist/delivery-preparation.js.map +1 -0
- package/dist/delivery-redaction.d.ts +5 -0
- package/dist/delivery-redaction.d.ts.map +1 -1
- package/dist/delivery-redaction.js +27 -6
- package/dist/delivery-redaction.js.map +1 -1
- package/dist/delivery-security.js +2 -2
- package/dist/delivery-security.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/item-scoring-expectation.d.ts +4 -0
- package/dist/item-scoring-expectation.d.ts.map +1 -0
- package/dist/item-scoring-expectation.js +6 -0
- package/dist/item-scoring-expectation.js.map +1 -0
- package/dist/item-submission-materialization.d.ts +25 -0
- package/dist/item-submission-materialization.d.ts.map +1 -0
- package/dist/item-submission-materialization.js +68 -0
- package/dist/item-submission-materialization.js.map +1 -0
- package/dist/parser-custom-interactions.js +1 -1
- package/dist/parser-custom-interactions.js.map +1 -1
- package/dist/parser-declarations.d.ts.map +1 -1
- package/dist/parser-declarations.js +4 -4
- package/dist/parser-declarations.js.map +1 -1
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +8 -7
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/parser-processing.js +2 -2
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts +5 -1
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +33 -0
- package/dist/parser-values.js.map +1 -1
- package/dist/processing-evaluator-boolean.d.ts +8 -0
- package/dist/processing-evaluator-boolean.d.ts.map +1 -0
- package/dist/processing-evaluator-boolean.js +47 -0
- package/dist/processing-evaluator-boolean.js.map +1 -0
- package/dist/processing-evaluator-collection.d.ts +12 -0
- package/dist/processing-evaluator-collection.d.ts.map +1 -0
- package/dist/processing-evaluator-collection.js +35 -0
- package/dist/processing-evaluator-collection.js.map +1 -0
- package/dist/processing-evaluator-comparison.d.ts +8 -0
- package/dist/processing-evaluator-comparison.d.ts.map +1 -0
- package/dist/processing-evaluator-comparison.js +88 -0
- package/dist/processing-evaluator-comparison.js.map +1 -0
- package/dist/processing-evaluator-custom.d.ts +8 -0
- package/dist/processing-evaluator-custom.d.ts.map +1 -0
- package/dist/processing-evaluator-custom.js +14 -0
- package/dist/processing-evaluator-custom.js.map +1 -0
- package/dist/processing-evaluator-geometry.d.ts +8 -0
- package/dist/processing-evaluator-geometry.d.ts.map +1 -0
- package/dist/processing-evaluator-geometry.js +21 -0
- package/dist/processing-evaluator-geometry.js.map +1 -0
- package/dist/processing-evaluator-numeric.d.ts +8 -0
- package/dist/processing-evaluator-numeric.d.ts.map +1 -0
- package/dist/processing-evaluator-numeric.js +112 -0
- package/dist/processing-evaluator-numeric.js.map +1 -0
- package/dist/processing-evaluator-string.d.ts +8 -0
- package/dist/processing-evaluator-string.d.ts.map +1 -0
- package/dist/processing-evaluator-string.js +33 -0
- package/dist/processing-evaluator-string.js.map +1 -0
- package/dist/processing-evaluator-variable.d.ts +8 -0
- package/dist/processing-evaluator-variable.d.ts.map +1 -0
- package/dist/processing-evaluator-variable.js +39 -0
- package/dist/processing-evaluator-variable.js.map +1 -0
- package/dist/processing-evaluator.d.ts +22 -0
- package/dist/processing-evaluator.d.ts.map +1 -0
- package/dist/processing-evaluator.js +137 -0
- package/dist/processing-evaluator.js.map +1 -0
- package/dist/processing-expression-children.d.ts +3 -0
- package/dist/processing-expression-children.d.ts.map +1 -0
- package/dist/processing-expression-children.js +68 -0
- package/dist/processing-expression-children.js.map +1 -0
- package/dist/processing-mapping.d.ts +13 -0
- package/dist/processing-mapping.d.ts.map +1 -0
- package/dist/processing-mapping.js +120 -0
- package/dist/processing-mapping.js.map +1 -0
- package/dist/processing-operators.d.ts +11 -0
- package/dist/processing-operators.d.ts.map +1 -0
- package/dist/processing-operators.js +178 -0
- package/dist/processing-operators.js.map +1 -0
- package/dist/processing-random.d.ts +2 -0
- package/dist/processing-random.d.ts.map +1 -0
- package/dist/processing-random.js +16 -0
- package/dist/processing-random.js.map +1 -0
- package/dist/processing-state.d.ts +8 -0
- package/dist/processing-state.d.ts.map +1 -0
- package/dist/processing-state.js +44 -0
- package/dist/processing-state.js.map +1 -0
- package/dist/processing-templates.d.ts +3 -0
- package/dist/processing-templates.d.ts.map +1 -0
- package/dist/processing-templates.js +12 -0
- package/dist/processing-templates.js.map +1 -0
- package/dist/processing-values.d.ts +15 -0
- package/dist/processing-values.d.ts.map +1 -0
- package/dist/processing-values.js +150 -0
- package/dist/processing-values.js.map +1 -0
- package/dist/processing-variables.d.ts +7 -0
- package/dist/processing-variables.d.ts.map +1 -0
- package/dist/processing-variables.js +40 -0
- package/dist/processing-variables.js.map +1 -0
- package/dist/qti-package-assets.d.ts +8 -0
- package/dist/qti-package-assets.d.ts.map +1 -0
- package/dist/qti-package-assets.js +173 -0
- package/dist/qti-package-assets.js.map +1 -0
- package/dist/qti-package-items.d.ts +14 -0
- package/dist/qti-package-items.d.ts.map +1 -0
- package/dist/qti-package-items.js +138 -0
- package/dist/qti-package-items.js.map +1 -0
- package/dist/qti-package-manifest.d.ts +13 -0
- package/dist/qti-package-manifest.d.ts.map +1 -0
- package/dist/qti-package-manifest.js +109 -0
- package/dist/qti-package-manifest.js.map +1 -0
- package/dist/qti-package-metadata.d.ts +8 -0
- package/dist/qti-package-metadata.d.ts.map +1 -0
- package/dist/qti-package-metadata.js +115 -0
- package/dist/qti-package-metadata.js.map +1 -0
- package/dist/qti-package-paths.d.ts +6 -0
- package/dist/qti-package-paths.d.ts.map +1 -0
- package/dist/qti-package-paths.js +32 -0
- package/dist/qti-package-paths.js.map +1 -0
- package/dist/qti-package-types.d.ts +98 -0
- package/dist/qti-package-types.d.ts.map +1 -0
- package/dist/qti-package-types.js +2 -0
- package/dist/qti-package-types.js.map +1 -0
- package/dist/qti-package-xml.d.ts +21 -0
- package/dist/qti-package-xml.d.ts.map +1 -0
- package/dist/qti-package-xml.js +68 -0
- package/dist/qti-package-xml.js.map +1 -0
- package/dist/qti-package-zip.d.ts +24 -0
- package/dist/qti-package-zip.d.ts.map +1 -0
- package/dist/qti-package-zip.js +185 -0
- package/dist/qti-package-zip.js.map +1 -0
- package/dist/qti-package.d.ts +7 -0
- package/dist/qti-package.d.ts.map +1 -0
- package/dist/qti-package.js +76 -0
- package/dist/qti-package.js.map +1 -0
- package/dist/response-input.d.ts +9 -0
- package/dist/response-input.d.ts.map +1 -0
- package/dist/response-input.js +9 -0
- package/dist/response-input.js.map +1 -0
- package/dist/response-validation-policy.d.ts +31 -0
- package/dist/response-validation-policy.d.ts.map +1 -0
- package/dist/response-validation-policy.js +158 -0
- package/dist/response-validation-policy.js.map +1 -0
- package/dist/response-validation.d.ts +23 -0
- package/dist/response-validation.d.ts.map +1 -0
- package/dist/response-validation.js +157 -0
- package/dist/response-validation.js.map +1 -0
- package/dist/scoring-disposition-policy.d.ts +8 -0
- package/dist/scoring-disposition-policy.d.ts.map +1 -0
- package/dist/scoring-disposition-policy.js +24 -0
- package/dist/scoring-disposition-policy.js.map +1 -0
- package/dist/serializer-processing-expressions.d.ts.map +1 -1
- package/dist/serializer-processing-expressions.js +2 -8
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/serializer-processing-xml.js +2 -2
- package/dist/serializer-processing-xml.js.map +1 -1
- package/dist/serializer-processing.d.ts.map +1 -1
- package/dist/serializer-processing.js +3 -0
- package/dist/serializer-processing.js.map +1 -1
- package/dist/session.d.ts +5 -14
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +76 -1158
- package/dist/session.js.map +1 -1
- package/dist/shared-vocabulary-coverage-policy.js +1 -1
- package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
- package/dist/shared-vocabulary-generated-families.js +1 -1
- package/dist/shared-vocabulary-generated-families.js.map +1 -1
- package/dist/shared-vocabulary-support.js +3 -3
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +4 -3
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support-evidence.d.ts +22 -0
- package/dist/support-evidence.d.ts.map +1 -0
- package/dist/support-evidence.js +93 -0
- package/dist/support-evidence.js.map +1 -0
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +30 -43
- package/dist/support.js.map +1 -1
- package/dist/template-presentation-materializer.d.ts +14 -0
- package/dist/template-presentation-materializer.d.ts.map +1 -0
- package/dist/template-presentation-materializer.js +143 -0
- package/dist/template-presentation-materializer.js.map +1 -0
- package/dist/trusted-item-session.d.ts +25 -8
- package/dist/trusted-item-session.d.ts.map +1 -1
- package/dist/trusted-item-session.js +57 -51
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/trusted-session-materialization.d.ts +33 -0
- package/dist/trusted-session-materialization.d.ts.map +1 -0
- package/dist/trusted-session-materialization.js +26 -0
- package/dist/trusted-session-materialization.js.map +1 -0
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +16 -2
- package/dist/tts.js.map +1 -1
- package/dist/validation-geometry.d.ts +8 -0
- package/dist/validation-geometry.d.ts.map +1 -0
- package/dist/validation-geometry.js +187 -0
- package/dist/validation-geometry.js.map +1 -0
- package/dist/validation-interactions.d.ts +3 -0
- package/dist/validation-interactions.d.ts.map +1 -0
- package/dist/validation-interactions.js +644 -0
- package/dist/validation-interactions.js.map +1 -0
- package/dist/validation-primitives.d.ts +12 -0
- package/dist/validation-primitives.d.ts.map +1 -0
- package/dist/validation-primitives.js +53 -0
- package/dist/validation-primitives.js.map +1 -0
- package/dist/validation-processing.d.ts +4 -0
- package/dist/validation-processing.d.ts.map +1 -0
- package/dist/validation-processing.js +569 -0
- package/dist/validation-processing.js.map +1 -0
- package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
- package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
- package/dist/validation-shared-vocabulary-layout.js +102 -0
- package/dist/validation-shared-vocabulary-layout.js.map +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +27 -1641
- package/dist/validation.js.map +1 -1
- package/dist/variable-snapshot.d.ts +9 -0
- package/dist/variable-snapshot.d.ts.map +1 -0
- package/dist/variable-snapshot.js +9 -0
- package/dist/variable-snapshot.js.map +1 -0
- package/dist/xml.js +1 -1
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
- package/src/adaptive-turn-materializer.ts +29 -16
- package/src/adaptive-turn.ts +29 -6
- package/src/assert-never.ts +4 -0
- package/src/attempt-state-constants.ts +5 -0
- package/src/attempt-state.ts +245 -0
- package/src/catalog.ts +1 -1
- package/src/content-text.ts +3 -0
- package/src/custom-operators.ts +12 -0
- package/src/delivery-preparation.ts +121 -0
- package/src/delivery-redaction.ts +37 -6
- package/src/delivery-security.ts +2 -2
- package/src/index.ts +67 -0
- package/src/item-scoring-expectation.ts +9 -0
- package/src/item-submission-materialization.ts +120 -0
- package/src/parser-custom-interactions.ts +1 -1
- package/src/parser-declarations.ts +4 -3
- package/src/parser-item-metadata.ts +8 -7
- package/src/parser-processing.ts +2 -2
- package/src/parser-values.ts +36 -0
- package/src/processing-evaluator-boolean.ts +48 -0
- package/src/processing-evaluator-collection.ts +50 -0
- package/src/processing-evaluator-comparison.ts +106 -0
- package/src/processing-evaluator-custom.ts +20 -0
- package/src/processing-evaluator-geometry.ts +26 -0
- package/src/processing-evaluator-numeric.ts +155 -0
- package/src/processing-evaluator-string.ts +60 -0
- package/src/processing-evaluator-variable.ts +81 -0
- package/src/processing-evaluator.ts +173 -0
- package/src/processing-expression-children.ts +69 -0
- package/src/processing-mapping.ts +153 -0
- package/src/processing-operators.ts +184 -0
- package/src/processing-random.ts +16 -0
- package/src/processing-state.ts +69 -0
- package/src/processing-templates.ts +12 -0
- package/src/processing-values.ts +162 -0
- package/src/processing-variables.ts +83 -0
- package/src/qti-package-assets.ts +256 -0
- package/src/qti-package-items.ts +240 -0
- package/src/qti-package-manifest.ts +210 -0
- package/src/qti-package-metadata.ts +143 -0
- package/src/qti-package-paths.ts +56 -0
- package/src/qti-package-types.ts +117 -0
- package/src/qti-package-xml.ts +97 -0
- package/src/qti-package-zip.ts +325 -0
- package/src/qti-package.ts +176 -0
- package/src/response-input.ts +19 -0
- package/src/response-validation-policy.ts +218 -0
- package/src/response-validation.ts +282 -0
- package/src/scoring-disposition-policy.ts +37 -0
- package/src/serializer-processing-expressions.ts +2 -12
- package/src/serializer-processing-xml.ts +2 -2
- package/src/serializer-processing.ts +3 -0
- package/src/session.ts +128 -2006
- package/src/shared-vocabulary-coverage-policy.ts +1 -1
- package/src/shared-vocabulary-generated-families.ts +1 -1
- package/src/shared-vocabulary-support.ts +3 -3
- package/src/shared-vocabulary.ts +3 -2
- package/src/support-evidence.ts +107 -0
- package/src/support.ts +59 -50
- package/src/template-presentation-materializer.ts +219 -0
- package/src/trusted-item-session.ts +86 -78
- package/src/trusted-session-materialization.ts +67 -0
- package/src/tts.ts +29 -15
- package/src/validation-geometry.ts +204 -0
- package/src/validation-interactions.ts +831 -0
- package/src/validation-primitives.ts +68 -0
- package/src/validation-processing.ts +785 -0
- package/src/validation-shared-vocabulary-layout.ts +117 -0
- package/src/validation.ts +44 -2077
- package/src/variable-snapshot.ts +20 -0
- package/src/xml.ts +1 -1
package/src/session.ts
CHANGED
|
@@ -7,50 +7,61 @@ import type {
|
|
|
7
7
|
QtiModalFeedback,
|
|
8
8
|
QtiPortableCustomStateValue,
|
|
9
9
|
QtiProcessingExpression,
|
|
10
|
-
QtiRecordValue,
|
|
11
10
|
QtiResponseCondition,
|
|
12
|
-
QtiResponseDeclaration,
|
|
13
11
|
QtiResponseRule,
|
|
14
|
-
QtiScalarValue,
|
|
15
12
|
QtiScoreResult,
|
|
16
13
|
QtiTemplateRule,
|
|
17
14
|
QtiValue,
|
|
18
|
-
QtiVariableDeclaration,
|
|
19
15
|
} from "./types.js";
|
|
16
|
+
import { qtiValueToString } from "./value-format.js";
|
|
17
|
+
import type { QtiCustomOperatorRegistry } from "./custom-operators.js";
|
|
18
|
+
export type {
|
|
19
|
+
QtiCustomOperatorContext,
|
|
20
|
+
QtiCustomOperatorHandler,
|
|
21
|
+
QtiCustomOperatorRegistry,
|
|
22
|
+
} from "./custom-operators.js";
|
|
23
|
+
import { createEvaluationContext, type EvaluationContext } from "./processing-evaluator.js";
|
|
24
|
+
import { seededRandom } from "./processing-random.js";
|
|
25
|
+
import { getResponseDeclaration } from "./processing-variables.js";
|
|
26
|
+
import { lookupOutcomeValue, mapOrMatchResponse } from "./processing-mapping.js";
|
|
27
|
+
import { responseProcessingTemplateKind } from "./processing-templates.js";
|
|
28
|
+
import { assertCompatiblePriorState } from "./attempt-state.js";
|
|
29
|
+
export { assertQtiAttemptStateV1, isQtiAttemptStateV1 } from "./attempt-state.js";
|
|
20
30
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
COMPLETION_COMPLETED,
|
|
32
|
+
COMPLETION_NOT_ATTEMPTED,
|
|
33
|
+
COMPLETION_STATUS,
|
|
34
|
+
COMPLETION_UNKNOWN,
|
|
35
|
+
} from "./attempt-state-constants.js";
|
|
36
|
+
import {
|
|
37
|
+
cloneDiagnostics,
|
|
38
|
+
clonePortableCustomState,
|
|
39
|
+
clonePortableCustomStateRecord,
|
|
40
|
+
cloneValue,
|
|
41
|
+
cloneValueRecord,
|
|
42
|
+
serialize,
|
|
43
|
+
} from "./processing-state.js";
|
|
44
|
+
import { booleanValue, normalizeValueForCardinality, qtiMatchValues } from "./processing-values.js";
|
|
45
|
+
|
|
46
|
+
interface SessionProcessingContext {
|
|
47
|
+
responseDefaults: Record<string, QtiValue>;
|
|
48
|
+
evaluation: EvaluationContext;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ConditionalRules<Rule> {
|
|
52
|
+
ifExpression?: QtiProcessingExpression | undefined;
|
|
53
|
+
thenRules: Rule[];
|
|
54
|
+
elseIfs: Array<{
|
|
55
|
+
expression?: QtiProcessingExpression | undefined;
|
|
56
|
+
rules: Rule[];
|
|
57
|
+
}>;
|
|
58
|
+
elseRules: Rule[];
|
|
34
59
|
}
|
|
35
60
|
|
|
36
|
-
export type QtiCustomOperatorHandler = (context: QtiCustomOperatorContext) => QtiValue;
|
|
37
|
-
export type QtiCustomOperatorRegistry = Record<string, QtiCustomOperatorHandler>;
|
|
38
|
-
|
|
39
|
-
const COMPLETION_STATUS = "completionStatus";
|
|
40
|
-
const COMPLETION_NOT_ATTEMPTED = "not_attempted";
|
|
41
|
-
const COMPLETION_UNKNOWN = "unknown";
|
|
42
|
-
const COMPLETION_COMPLETED = "completed";
|
|
43
|
-
const ATTEMPT_STATE_SCHEMA = "qti3.attempt-state.v1";
|
|
44
|
-
const ATTEMPT_STATUSES = new Set<QtiAttemptStatus>([
|
|
45
|
-
"initialized",
|
|
46
|
-
"interacting",
|
|
47
|
-
"suspended",
|
|
48
|
-
"completed",
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
61
|
export interface QtiItemSessionOptions {
|
|
52
62
|
randomSeed?: string | number | undefined;
|
|
53
63
|
customOperators?: QtiCustomOperatorRegistry | undefined;
|
|
64
|
+
allowedUndeclaredResponseIdentifiers?: readonly string[] | undefined;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export interface QtiItemSession {
|
|
@@ -100,6 +111,9 @@ export function createItemSession(
|
|
|
100
111
|
let status: QtiAttemptStatus = priorState?.status ?? "initialized";
|
|
101
112
|
const random = seededRandom(options.randomSeed ?? document.item.identifier);
|
|
102
113
|
const customOperators = options.customOperators ?? {};
|
|
114
|
+
const allowedUndeclaredResponseIdentifiers = new Set(
|
|
115
|
+
options.allowedUndeclaredResponseIdentifiers ?? [],
|
|
116
|
+
);
|
|
103
117
|
const portableCustomResponseIdentifiers = new Set(
|
|
104
118
|
document.item.interactions
|
|
105
119
|
.filter((interaction) => interaction.type === "portableCustom")
|
|
@@ -123,16 +137,23 @@ export function createItemSession(
|
|
|
123
137
|
const baseResponses = cloneValueRecord(responses);
|
|
124
138
|
const baseResponseDefaults = cloneValueRecord(responseDefaults);
|
|
125
139
|
const baseOutcomes = cloneValueRecord(outcomes);
|
|
126
|
-
|
|
127
|
-
applyTemplateProcessing(
|
|
140
|
+
const evaluation = createEvaluationContext(
|
|
128
141
|
document,
|
|
129
|
-
templateValues,
|
|
130
142
|
responses,
|
|
131
|
-
responseDefaults,
|
|
132
143
|
outcomes,
|
|
144
|
+
templateValues,
|
|
133
145
|
correctResponses,
|
|
134
146
|
random,
|
|
135
147
|
customOperators,
|
|
148
|
+
{ allowedUndeclaredResponseIdentifiers },
|
|
149
|
+
);
|
|
150
|
+
const processingContext: SessionProcessingContext = {
|
|
151
|
+
responseDefaults,
|
|
152
|
+
evaluation,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
applyTemplateProcessing(
|
|
156
|
+
processingContext,
|
|
136
157
|
new Set(),
|
|
137
158
|
baseResponses,
|
|
138
159
|
baseResponseDefaults,
|
|
@@ -142,14 +163,7 @@ export function createItemSession(
|
|
|
142
163
|
Object.assign(templateValues, priorTemplateValues);
|
|
143
164
|
resetCorrectResponses(document, correctResponses);
|
|
144
165
|
applyTemplateProcessing(
|
|
145
|
-
|
|
146
|
-
templateValues,
|
|
147
|
-
responses,
|
|
148
|
-
responseDefaults,
|
|
149
|
-
outcomes,
|
|
150
|
-
correctResponses,
|
|
151
|
-
random,
|
|
152
|
-
customOperators,
|
|
166
|
+
processingContext,
|
|
153
167
|
new Set(Object.keys(priorTemplateValues)),
|
|
154
168
|
baseResponses,
|
|
155
169
|
baseResponseDefaults,
|
|
@@ -196,15 +210,7 @@ export function createItemSession(
|
|
|
196
210
|
resetRecord(outcomes, cloneValueRecord(defaultOutcomes));
|
|
197
211
|
outcomes[COMPLETION_STATUS] = completionStatus;
|
|
198
212
|
}
|
|
199
|
-
applyResponseProcessing(
|
|
200
|
-
document,
|
|
201
|
-
responses,
|
|
202
|
-
outcomes,
|
|
203
|
-
templateValues,
|
|
204
|
-
correctResponses,
|
|
205
|
-
random,
|
|
206
|
-
customOperators,
|
|
207
|
-
);
|
|
213
|
+
applyResponseProcessing(processingContext);
|
|
208
214
|
if (outcomes[COMPLETION_STATUS] === COMPLETION_COMPLETED) status = "completed";
|
|
209
215
|
validationMessages = diagnostics;
|
|
210
216
|
const state = serialize(
|
|
@@ -242,15 +248,6 @@ export function createItemSession(
|
|
|
242
248
|
}
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
export function isQtiAttemptStateV1(value: unknown): value is QtiAttemptStateV1 {
|
|
246
|
-
return attemptStateErrors(value).length === 0;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function assertQtiAttemptStateV1(value: unknown): asserts value is QtiAttemptStateV1 {
|
|
250
|
-
const [firstError] = attemptStateErrors(value);
|
|
251
|
-
if (firstError) throw new Error(firstError);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
251
|
function resetRecord<T>(target: Record<string, T>, source: Record<string, T>): void {
|
|
255
252
|
for (const key of Object.keys(target)) {
|
|
256
253
|
delete target[key];
|
|
@@ -258,242 +255,28 @@ function resetRecord<T>(target: Record<string, T>, source: Record<string, T>): v
|
|
|
258
255
|
Object.assign(target, source);
|
|
259
256
|
}
|
|
260
257
|
|
|
261
|
-
function assertCompatiblePriorState(
|
|
262
|
-
document: QtiDocument,
|
|
263
|
-
priorState: QtiAttemptStateV1 | undefined,
|
|
264
|
-
): void {
|
|
265
|
-
if (!priorState) return;
|
|
266
|
-
assertQtiAttemptStateV1(priorState);
|
|
267
|
-
if (priorState.itemIdentifier !== document.item.identifier) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`Cannot restore state for ${priorState.itemIdentifier} into ${document.item.identifier}.`,
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
const responseIdentifiers = new Set(
|
|
273
|
-
document.item.responseDeclarations.map((declaration) => declaration.identifier),
|
|
274
|
-
);
|
|
275
|
-
const outcomeIdentifiers = new Set([
|
|
276
|
-
...document.item.outcomeDeclarations.map((declaration) => declaration.identifier),
|
|
277
|
-
COMPLETION_STATUS,
|
|
278
|
-
]);
|
|
279
|
-
const templateIdentifiers = new Set(
|
|
280
|
-
document.item.templateDeclarations.map((declaration) => declaration.identifier),
|
|
281
|
-
);
|
|
282
|
-
const interactionStateIdentifiers = new Set(
|
|
283
|
-
document.item.interactions
|
|
284
|
-
.filter((interaction) => interaction.type === "portableCustom")
|
|
285
|
-
.map((interaction) => interaction.responseIdentifier)
|
|
286
|
-
.filter((identifier): identifier is string => Boolean(identifier)),
|
|
287
|
-
);
|
|
288
|
-
assertKnownStateIdentifiers("response", priorState.responses, responseIdentifiers);
|
|
289
|
-
assertKnownStateIdentifiers("outcome", priorState.outcomes, outcomeIdentifiers);
|
|
290
|
-
assertKnownStateIdentifiers("template", priorState.templateValues ?? {}, templateIdentifiers);
|
|
291
|
-
assertKnownPortableCustomStateIdentifiers(
|
|
292
|
-
priorState.interactionStates ?? {},
|
|
293
|
-
interactionStateIdentifiers,
|
|
294
|
-
);
|
|
295
|
-
for (const message of priorState.validationMessages) {
|
|
296
|
-
if (message.path && !responseIdentifiers.has(message.path)) {
|
|
297
|
-
throw new Error(`Cannot restore validation message for unknown response ${message.path}.`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
for (const declaration of document.item.responseDeclarations) {
|
|
301
|
-
assertRestoredValueMatchesDeclaration("response", declaration, priorState.responses);
|
|
302
|
-
}
|
|
303
|
-
for (const declaration of document.item.outcomeDeclarations) {
|
|
304
|
-
assertRestoredValueMatchesDeclaration("outcome", declaration, priorState.outcomes);
|
|
305
|
-
}
|
|
306
|
-
for (const declaration of document.item.templateDeclarations) {
|
|
307
|
-
assertRestoredValueMatchesDeclaration("template", declaration, priorState.templateValues ?? {});
|
|
308
|
-
}
|
|
309
|
-
const completionStatus = priorState.outcomes[COMPLETION_STATUS];
|
|
310
|
-
if (
|
|
311
|
-
completionStatus !== undefined &&
|
|
312
|
-
completionStatus !== COMPLETION_NOT_ATTEMPTED &&
|
|
313
|
-
completionStatus !== COMPLETION_UNKNOWN &&
|
|
314
|
-
completionStatus !== COMPLETION_COMPLETED &&
|
|
315
|
-
completionStatus !== "incomplete"
|
|
316
|
-
) {
|
|
317
|
-
throw new Error(
|
|
318
|
-
`Cannot restore unsupported completionStatus ${qtiValueToString(completionStatus)}.`,
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function assertKnownStateIdentifiers(
|
|
324
|
-
kind: string,
|
|
325
|
-
record: Record<string, QtiValue>,
|
|
326
|
-
allowed: Set<string>,
|
|
327
|
-
): void {
|
|
328
|
-
const unknown = Object.keys(record).find((identifier) => !allowed.has(identifier));
|
|
329
|
-
if (unknown) throw new Error(`Cannot restore unknown ${kind} identifier ${unknown}.`);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function assertKnownPortableCustomStateIdentifiers(
|
|
333
|
-
record: Record<string, QtiPortableCustomStateValue>,
|
|
334
|
-
allowed: Set<string>,
|
|
335
|
-
): void {
|
|
336
|
-
const unknown = Object.keys(record).find((identifier) => !allowed.has(identifier));
|
|
337
|
-
if (unknown) throw new Error(`Cannot restore unknown interaction state identifier ${unknown}.`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function assertRestoredValueMatchesDeclaration(
|
|
341
|
-
kind: string,
|
|
342
|
-
declaration: QtiVariableDeclaration,
|
|
343
|
-
record: Record<string, QtiValue>,
|
|
344
|
-
): void {
|
|
345
|
-
if (!(declaration.identifier in record)) return;
|
|
346
|
-
const value = record[declaration.identifier] ?? null;
|
|
347
|
-
const error = restoredValueError(declaration, value);
|
|
348
|
-
if (error) throw new Error(`Cannot restore ${kind} ${declaration.identifier}: ${error}.`);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function restoredValueError(
|
|
352
|
-
declaration: QtiVariableDeclaration,
|
|
353
|
-
value: QtiValue,
|
|
354
|
-
): string | undefined {
|
|
355
|
-
if (value === null) return undefined;
|
|
356
|
-
if (declaration.cardinality === "record") {
|
|
357
|
-
return isRecordValue(value) ? undefined : "expected record value";
|
|
358
|
-
}
|
|
359
|
-
if (isRecordValue(value)) return "expected scalar value";
|
|
360
|
-
if (declaration.cardinality === "single" && Array.isArray(value)) {
|
|
361
|
-
return "expected single value";
|
|
362
|
-
}
|
|
363
|
-
if (
|
|
364
|
-
(declaration.cardinality === "multiple" || declaration.cardinality === "ordered") &&
|
|
365
|
-
!Array.isArray(value)
|
|
366
|
-
) {
|
|
367
|
-
return `expected ${declaration.cardinality} value container`;
|
|
368
|
-
}
|
|
369
|
-
if (!declaration.baseType) return undefined;
|
|
370
|
-
for (const entry of restoredValueEntries(value)) {
|
|
371
|
-
if (!restoredScalarMatchesBaseType(entry, declaration.baseType)) {
|
|
372
|
-
return `value ${String(entry)} is not valid for base-type ${declaration.baseType}`;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return undefined;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function restoredValueEntries(value: QtiValue): QtiScalarValue[] {
|
|
379
|
-
if (value === null || isRecordValue(value)) return [];
|
|
380
|
-
return Array.isArray(value) ? value : [value];
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function restoredScalarMatchesBaseType(
|
|
384
|
-
value: QtiScalarValue,
|
|
385
|
-
baseType: QtiVariableDeclaration["baseType"],
|
|
386
|
-
): boolean {
|
|
387
|
-
if (!baseType) return true;
|
|
388
|
-
if (baseType === "integer") {
|
|
389
|
-
return typeof value === "number" ? Number.isInteger(value) : /^-?\d+$/.test(String(value));
|
|
390
|
-
}
|
|
391
|
-
if (baseType === "float") {
|
|
392
|
-
return typeof value === "number" ? Number.isFinite(value) : Number.isFinite(Number(value));
|
|
393
|
-
}
|
|
394
|
-
if (baseType === "boolean")
|
|
395
|
-
return typeof value === "boolean" || value === "true" || value === "false";
|
|
396
|
-
if (baseType === "point") return pointValueIsValid(value);
|
|
397
|
-
if (baseType === "pair" || baseType === "directedPair") return pairValueIsValid(value);
|
|
398
|
-
if (baseType === "identifier")
|
|
399
|
-
return typeof value === "string" && value.trim().length > 0 && !/\s/.test(value);
|
|
400
|
-
return typeof value === "string";
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function pointValueIsValid(value: QtiScalarValue): boolean {
|
|
404
|
-
const parts = String(value).trim().split(/\s+/);
|
|
405
|
-
return parts.length === 2 && parts.every((part) => Number.isFinite(Number(part)));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function pairValueIsValid(value: QtiScalarValue): boolean {
|
|
409
|
-
const parts = String(value).trim().split(/\s+/);
|
|
410
|
-
return parts.length === 2 && parts.every((part) => part.length > 0);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function attemptStateErrors(value: unknown): string[] {
|
|
414
|
-
if (!isRecord(value)) return ["QTI attempt state must be an object."];
|
|
415
|
-
|
|
416
|
-
const schema = value.schema;
|
|
417
|
-
if (schema !== ATTEMPT_STATE_SCHEMA) {
|
|
418
|
-
return [`Unsupported QTI attempt state schema ${String(schema)}.`];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const errors: string[] = [];
|
|
422
|
-
if (typeof value.itemIdentifier !== "string" || value.itemIdentifier.length === 0) {
|
|
423
|
-
errors.push("QTI attempt state itemIdentifier must be a non-empty string.");
|
|
424
|
-
}
|
|
425
|
-
if (typeof value.status !== "string" || !ATTEMPT_STATUSES.has(value.status as QtiAttemptStatus)) {
|
|
426
|
-
errors.push(`QTI attempt state status ${String(value.status)} is not supported.`);
|
|
427
|
-
}
|
|
428
|
-
if (!isQtiValueRecord(value.responses)) {
|
|
429
|
-
errors.push("QTI attempt state responses must be a record of QTI values.");
|
|
430
|
-
}
|
|
431
|
-
if (!isQtiValueRecord(value.outcomes)) {
|
|
432
|
-
errors.push("QTI attempt state outcomes must be a record of QTI values.");
|
|
433
|
-
}
|
|
434
|
-
if (value.templateValues !== undefined && !isQtiValueRecord(value.templateValues)) {
|
|
435
|
-
errors.push("QTI attempt state templateValues must be a record of QTI values.");
|
|
436
|
-
}
|
|
437
|
-
if (
|
|
438
|
-
value.interactionStates !== undefined &&
|
|
439
|
-
!isPortableCustomStateRecord(value.interactionStates)
|
|
440
|
-
) {
|
|
441
|
-
errors.push("QTI attempt state interactionStates must be a record of JSON values.");
|
|
442
|
-
}
|
|
443
|
-
if (!isDiagnosticArray(value.validationMessages)) {
|
|
444
|
-
errors.push("QTI attempt state validationMessages must be an array of diagnostics.");
|
|
445
|
-
}
|
|
446
|
-
return errors;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
258
|
function applyTemplateProcessing(
|
|
450
|
-
|
|
451
|
-
templateValues: Record<string, QtiValue>,
|
|
452
|
-
responses: Record<string, QtiValue>,
|
|
453
|
-
responseDefaults: Record<string, QtiValue>,
|
|
454
|
-
outcomes: Record<string, QtiValue>,
|
|
455
|
-
correctResponses: Record<string, QtiValue>,
|
|
456
|
-
random: () => number,
|
|
457
|
-
customOperators: QtiCustomOperatorRegistry,
|
|
259
|
+
context: SessionProcessingContext,
|
|
458
260
|
preservedTemplateIdentifiers = new Set<string>(),
|
|
459
|
-
baseResponses: Record<string, QtiValue> = cloneValueRecord(responses),
|
|
460
|
-
baseResponseDefaults: Record<string, QtiValue> = cloneValueRecord(responseDefaults),
|
|
461
|
-
baseOutcomes: Record<string, QtiValue> = cloneValueRecord(outcomes),
|
|
261
|
+
baseResponses: Record<string, QtiValue> = cloneValueRecord(context.evaluation.responses),
|
|
262
|
+
baseResponseDefaults: Record<string, QtiValue> = cloneValueRecord(context.responseDefaults),
|
|
263
|
+
baseOutcomes: Record<string, QtiValue> = cloneValueRecord(context.evaluation.outcomes),
|
|
462
264
|
): void {
|
|
463
|
-
const
|
|
265
|
+
const { evaluation } = context;
|
|
266
|
+
const rules = evaluation.document.item.templateProcessing?.rules ?? [];
|
|
464
267
|
let restarts = 0;
|
|
465
268
|
for (let index = 0; index < rules.length; index += 1) {
|
|
466
269
|
const rule = rules[index]!;
|
|
467
|
-
const shouldExit = applyTemplateRule(
|
|
468
|
-
rule,
|
|
469
|
-
document,
|
|
470
|
-
templateValues,
|
|
471
|
-
responses,
|
|
472
|
-
responseDefaults,
|
|
473
|
-
outcomes,
|
|
474
|
-
correctResponses,
|
|
475
|
-
random,
|
|
476
|
-
customOperators,
|
|
477
|
-
preservedTemplateIdentifiers,
|
|
478
|
-
);
|
|
270
|
+
const shouldExit = applyTemplateRule(context, rule, preservedTemplateIdentifiers);
|
|
479
271
|
if (shouldExit) return;
|
|
480
272
|
if (rule.type === "templateConstraint") {
|
|
481
|
-
const satisfied =
|
|
482
|
-
rule.expression,
|
|
483
|
-
document,
|
|
484
|
-
responses,
|
|
485
|
-
outcomes,
|
|
486
|
-
templateValues,
|
|
487
|
-
correctResponses,
|
|
488
|
-
random,
|
|
489
|
-
customOperators,
|
|
490
|
-
);
|
|
273
|
+
const satisfied = evaluateProcessingBoolean(context, rule.expression);
|
|
491
274
|
if (!satisfied) {
|
|
492
|
-
resetTemplateValues(document, templateValues);
|
|
493
|
-
resetRecord(responses, cloneValueRecord(baseResponses));
|
|
494
|
-
resetRecord(responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
495
|
-
resetRecord(outcomes, cloneValueRecord(baseOutcomes));
|
|
496
|
-
resetCorrectResponses(document, correctResponses);
|
|
275
|
+
resetTemplateValues(evaluation.document, evaluation.templateValues);
|
|
276
|
+
resetRecord(evaluation.responses, cloneValueRecord(baseResponses));
|
|
277
|
+
resetRecord(context.responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
278
|
+
resetRecord(evaluation.outcomes, cloneValueRecord(baseOutcomes));
|
|
279
|
+
resetCorrectResponses(evaluation.document, evaluation.correctResponses);
|
|
497
280
|
restarts += 1;
|
|
498
281
|
if (restarts <= 100) index = -1;
|
|
499
282
|
}
|
|
@@ -522,1808 +305,147 @@ function resetCorrectResponses(
|
|
|
522
305
|
}
|
|
523
306
|
}
|
|
524
307
|
|
|
308
|
+
function evaluateProcessingBoolean(
|
|
309
|
+
context: SessionProcessingContext,
|
|
310
|
+
expression: QtiResponseCondition["ifExpression"],
|
|
311
|
+
): boolean {
|
|
312
|
+
return expression ? booleanValue(context.evaluation.evaluate(expression)) : false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function resolveConditionalRules<Rule>(
|
|
316
|
+
context: SessionProcessingContext,
|
|
317
|
+
condition: ConditionalRules<Rule>,
|
|
318
|
+
): Rule[] {
|
|
319
|
+
if (evaluateProcessingBoolean(context, condition.ifExpression)) return condition.thenRules;
|
|
320
|
+
for (const elseIf of condition.elseIfs) {
|
|
321
|
+
if (evaluateProcessingBoolean(context, elseIf.expression)) return elseIf.rules;
|
|
322
|
+
}
|
|
323
|
+
return condition.elseRules;
|
|
324
|
+
}
|
|
325
|
+
|
|
525
326
|
function applyTemplateRule(
|
|
327
|
+
context: SessionProcessingContext,
|
|
526
328
|
rule: QtiTemplateRule,
|
|
527
|
-
document: QtiDocument,
|
|
528
|
-
templateValues: Record<string, QtiValue>,
|
|
529
|
-
responses: Record<string, QtiValue>,
|
|
530
|
-
responseDefaults: Record<string, QtiValue>,
|
|
531
|
-
outcomes: Record<string, QtiValue>,
|
|
532
|
-
correctResponses: Record<string, QtiValue>,
|
|
533
|
-
random: () => number,
|
|
534
|
-
customOperators: QtiCustomOperatorRegistry,
|
|
535
329
|
preservedTemplateIdentifiers: Set<string>,
|
|
536
330
|
): boolean {
|
|
331
|
+
const { evaluation } = context;
|
|
537
332
|
if (rule.type === "exitTemplate") return true;
|
|
538
333
|
if (rule.type === "templateConstraint") return false;
|
|
539
334
|
|
|
540
335
|
if (rule.type === "templateCondition") {
|
|
541
|
-
|
|
542
|
-
rule.ifExpression,
|
|
543
|
-
document,
|
|
544
|
-
responses,
|
|
545
|
-
outcomes,
|
|
546
|
-
templateValues,
|
|
547
|
-
correctResponses,
|
|
548
|
-
random,
|
|
549
|
-
customOperators,
|
|
550
|
-
)
|
|
551
|
-
? rule.thenRules
|
|
552
|
-
: undefined;
|
|
553
|
-
if (!branch) {
|
|
554
|
-
for (const elseIf of rule.elseIfs) {
|
|
555
|
-
if (
|
|
556
|
-
evaluateBoolean(
|
|
557
|
-
elseIf.expression,
|
|
558
|
-
document,
|
|
559
|
-
responses,
|
|
560
|
-
outcomes,
|
|
561
|
-
templateValues,
|
|
562
|
-
correctResponses,
|
|
563
|
-
random,
|
|
564
|
-
customOperators,
|
|
565
|
-
)
|
|
566
|
-
) {
|
|
567
|
-
branch = elseIf.rules;
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
branch ??= rule.elseRules;
|
|
336
|
+
const branch = resolveConditionalRules(context, rule);
|
|
573
337
|
for (const branchRule of branch) {
|
|
574
|
-
const shouldExit = applyTemplateRule(
|
|
575
|
-
branchRule,
|
|
576
|
-
document,
|
|
577
|
-
templateValues,
|
|
578
|
-
responses,
|
|
579
|
-
responseDefaults,
|
|
580
|
-
outcomes,
|
|
581
|
-
correctResponses,
|
|
582
|
-
random,
|
|
583
|
-
customOperators,
|
|
584
|
-
preservedTemplateIdentifiers,
|
|
585
|
-
);
|
|
338
|
+
const shouldExit = applyTemplateRule(context, branchRule, preservedTemplateIdentifiers);
|
|
586
339
|
if (shouldExit) return true;
|
|
587
340
|
}
|
|
588
341
|
return false;
|
|
589
342
|
}
|
|
590
343
|
|
|
591
|
-
const value =
|
|
592
|
-
rule.expression,
|
|
593
|
-
document,
|
|
594
|
-
responses,
|
|
595
|
-
outcomes,
|
|
596
|
-
templateValues,
|
|
597
|
-
correctResponses,
|
|
598
|
-
random,
|
|
599
|
-
customOperators,
|
|
600
|
-
);
|
|
344
|
+
const value = evaluation.evaluate(rule.expression);
|
|
601
345
|
if (rule.type === "setTemplateValue") {
|
|
602
346
|
if (preservedTemplateIdentifiers.has(rule.identifier)) return false;
|
|
603
|
-
templateValues[rule.identifier] = value;
|
|
347
|
+
evaluation.templateValues[rule.identifier] = value;
|
|
604
348
|
return false;
|
|
605
349
|
}
|
|
606
350
|
|
|
607
351
|
if (rule.type === "setDefaultValue") {
|
|
608
|
-
const responseDeclaration = getResponseDeclaration(document, rule.identifier);
|
|
352
|
+
const responseDeclaration = getResponseDeclaration(evaluation.document, rule.identifier);
|
|
609
353
|
if (responseDeclaration) {
|
|
610
354
|
const normalized = normalizeValueForCardinality(value, responseDeclaration.cardinality);
|
|
611
355
|
if (normalized === null) {
|
|
612
|
-
delete responseDefaults[rule.identifier];
|
|
356
|
+
delete context.responseDefaults[rule.identifier];
|
|
613
357
|
} else {
|
|
614
|
-
responseDefaults[rule.identifier] = normalized;
|
|
358
|
+
context.responseDefaults[rule.identifier] = normalized;
|
|
615
359
|
}
|
|
616
360
|
return false;
|
|
617
361
|
}
|
|
618
|
-
const outcomeDeclaration = document.item.outcomeDeclarations.find(
|
|
362
|
+
const outcomeDeclaration = evaluation.document.item.outcomeDeclarations.find(
|
|
619
363
|
(declaration) => declaration.identifier === rule.identifier,
|
|
620
364
|
);
|
|
621
365
|
if (outcomeDeclaration) {
|
|
622
|
-
outcomes[rule.identifier] = value;
|
|
366
|
+
evaluation.outcomes[rule.identifier] = value;
|
|
623
367
|
}
|
|
624
368
|
return false;
|
|
625
369
|
}
|
|
626
370
|
|
|
627
|
-
const declaration = getResponseDeclaration(document, rule.identifier);
|
|
371
|
+
const declaration = getResponseDeclaration(evaluation.document, rule.identifier);
|
|
628
372
|
if (declaration)
|
|
629
|
-
correctResponses[rule.identifier] = normalizeValueForCardinality(
|
|
373
|
+
evaluation.correctResponses[rule.identifier] = normalizeValueForCardinality(
|
|
630
374
|
value,
|
|
631
375
|
declaration.cardinality,
|
|
632
376
|
);
|
|
633
377
|
return false;
|
|
634
378
|
}
|
|
635
379
|
|
|
636
|
-
function applyResponseProcessing(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
outcomes: Record<string, QtiValue>,
|
|
640
|
-
templateValues: Record<string, QtiValue>,
|
|
641
|
-
correctResponses: Record<string, QtiValue>,
|
|
642
|
-
random: () => number,
|
|
643
|
-
customOperators: QtiCustomOperatorRegistry,
|
|
644
|
-
): void {
|
|
645
|
-
const processing = document.item.responseProcessing;
|
|
380
|
+
function applyResponseProcessing(context: SessionProcessingContext): void {
|
|
381
|
+
const { evaluation } = context;
|
|
382
|
+
const processing = evaluation.document.item.responseProcessing;
|
|
646
383
|
if (processing?.rules.length) {
|
|
647
|
-
applyResponseRules(
|
|
648
|
-
processing.rules,
|
|
649
|
-
document,
|
|
650
|
-
responses,
|
|
651
|
-
outcomes,
|
|
652
|
-
templateValues,
|
|
653
|
-
correctResponses,
|
|
654
|
-
random,
|
|
655
|
-
customOperators,
|
|
656
|
-
);
|
|
384
|
+
applyResponseRules(context, processing.rules);
|
|
657
385
|
return;
|
|
658
386
|
}
|
|
659
387
|
|
|
660
|
-
const templateKind =
|
|
388
|
+
const templateKind = processing?.template
|
|
389
|
+
? (responseProcessingTemplateKind(processing.template) ?? "unsupported")
|
|
390
|
+
: undefined;
|
|
661
391
|
if (templateKind === "unsupported") {
|
|
662
392
|
return;
|
|
663
393
|
}
|
|
664
394
|
if (templateKind === "mapResponse" || templateKind === "mapResponsePoint") {
|
|
665
|
-
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
666
|
-
outcomes.SCORE = declaration
|
|
395
|
+
const declaration = getResponseDeclaration(evaluation.document, "RESPONSE");
|
|
396
|
+
evaluation.outcomes.SCORE = declaration
|
|
667
397
|
? mapOrMatchResponse(
|
|
668
398
|
declaration,
|
|
669
|
-
responses.RESPONSE ?? null,
|
|
670
|
-
correctResponses.RESPONSE ?? null,
|
|
399
|
+
evaluation.responses.RESPONSE ?? null,
|
|
400
|
+
evaluation.correctResponses.RESPONSE ?? null,
|
|
671
401
|
)
|
|
672
402
|
: 0;
|
|
673
403
|
return;
|
|
674
404
|
}
|
|
675
405
|
|
|
676
406
|
if (templateKind === "matchCorrect") {
|
|
677
|
-
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
407
|
+
const declaration = getResponseDeclaration(evaluation.document, "RESPONSE");
|
|
678
408
|
const matches = declaration
|
|
679
409
|
? qtiMatchValues(
|
|
680
|
-
responses.RESPONSE ?? null,
|
|
681
|
-
correctResponses.RESPONSE ?? null,
|
|
410
|
+
evaluation.responses.RESPONSE ?? null,
|
|
411
|
+
evaluation.correctResponses.RESPONSE ?? null,
|
|
682
412
|
declaration.cardinality === "ordered",
|
|
683
413
|
)
|
|
684
414
|
: null;
|
|
685
|
-
outcomes.SCORE = matches === true ? 1 : 0;
|
|
415
|
+
evaluation.outcomes.SCORE = matches === true ? 1 : 0;
|
|
686
416
|
}
|
|
687
417
|
}
|
|
688
418
|
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
): "matchCorrect" | "mapResponse" | "mapResponsePoint" | "unsupported" | undefined {
|
|
692
|
-
if (!template) return undefined;
|
|
693
|
-
const path = template.split(/[?#]/, 1)[0] ?? "";
|
|
694
|
-
const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
|
|
695
|
-
if (name === "match_correct") return "matchCorrect";
|
|
696
|
-
if (name === "map_response") return "mapResponse";
|
|
697
|
-
if (name === "map_response_point") return "mapResponsePoint";
|
|
698
|
-
return "unsupported";
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function applyResponseRules(
|
|
702
|
-
rules: QtiResponseRule[],
|
|
703
|
-
document: QtiDocument,
|
|
704
|
-
responses: Record<string, QtiValue>,
|
|
705
|
-
outcomes: Record<string, QtiValue>,
|
|
706
|
-
templateValues: Record<string, QtiValue>,
|
|
707
|
-
correctResponses: Record<string, QtiValue>,
|
|
708
|
-
random: () => number,
|
|
709
|
-
customOperators: QtiCustomOperatorRegistry,
|
|
710
|
-
): boolean {
|
|
419
|
+
function applyResponseRules(context: SessionProcessingContext, rules: QtiResponseRule[]): boolean {
|
|
420
|
+
const { evaluation } = context;
|
|
711
421
|
for (const rule of rules) {
|
|
712
422
|
if (rule.type === "exitResponse") return true;
|
|
713
423
|
if (rule.type === "responseCondition") {
|
|
714
|
-
const shouldExit = applyResponseCondition(
|
|
715
|
-
rule.condition,
|
|
716
|
-
document,
|
|
717
|
-
responses,
|
|
718
|
-
outcomes,
|
|
719
|
-
templateValues,
|
|
720
|
-
correctResponses,
|
|
721
|
-
random,
|
|
722
|
-
customOperators,
|
|
723
|
-
);
|
|
424
|
+
const shouldExit = applyResponseCondition(context, rule.condition);
|
|
724
425
|
if (shouldExit) return true;
|
|
725
426
|
continue;
|
|
726
427
|
}
|
|
727
428
|
if (rule.type === "responseProcessingFragment") {
|
|
728
|
-
const shouldExit = applyResponseRules(
|
|
729
|
-
rule.rules,
|
|
730
|
-
document,
|
|
731
|
-
responses,
|
|
732
|
-
outcomes,
|
|
733
|
-
templateValues,
|
|
734
|
-
correctResponses,
|
|
735
|
-
random,
|
|
736
|
-
customOperators,
|
|
737
|
-
);
|
|
429
|
+
const shouldExit = applyResponseRules(context, rule.rules);
|
|
738
430
|
if (shouldExit) return true;
|
|
739
431
|
continue;
|
|
740
432
|
}
|
|
741
433
|
if (rule.type === "lookupOutcomeValue") {
|
|
742
|
-
outcomes[rule.identifier] = lookupOutcomeValue(
|
|
743
|
-
document,
|
|
434
|
+
evaluation.outcomes[rule.identifier] = lookupOutcomeValue(
|
|
435
|
+
evaluation.document,
|
|
744
436
|
rule.identifier,
|
|
745
|
-
|
|
746
|
-
rule.expression,
|
|
747
|
-
document,
|
|
748
|
-
responses,
|
|
749
|
-
outcomes,
|
|
750
|
-
templateValues,
|
|
751
|
-
correctResponses,
|
|
752
|
-
random,
|
|
753
|
-
customOperators,
|
|
754
|
-
),
|
|
437
|
+
evaluation.evaluate(rule.expression),
|
|
755
438
|
);
|
|
756
439
|
continue;
|
|
757
440
|
}
|
|
758
|
-
outcomes[rule.identifier] =
|
|
759
|
-
rule.expression,
|
|
760
|
-
document,
|
|
761
|
-
responses,
|
|
762
|
-
outcomes,
|
|
763
|
-
templateValues,
|
|
764
|
-
correctResponses,
|
|
765
|
-
random,
|
|
766
|
-
customOperators,
|
|
767
|
-
);
|
|
441
|
+
evaluation.outcomes[rule.identifier] = evaluation.evaluate(rule.expression);
|
|
768
442
|
}
|
|
769
443
|
return false;
|
|
770
444
|
}
|
|
771
445
|
|
|
772
446
|
function applyResponseCondition(
|
|
447
|
+
context: SessionProcessingContext,
|
|
773
448
|
condition: QtiResponseCondition,
|
|
774
|
-
document: QtiDocument,
|
|
775
|
-
responses: Record<string, QtiValue>,
|
|
776
|
-
outcomes: Record<string, QtiValue>,
|
|
777
|
-
templateValues: Record<string, QtiValue>,
|
|
778
|
-
correctResponses: Record<string, QtiValue>,
|
|
779
|
-
random: () => number,
|
|
780
|
-
customOperators: QtiCustomOperatorRegistry,
|
|
781
|
-
): boolean {
|
|
782
|
-
let branch = evaluateBoolean(
|
|
783
|
-
condition.ifExpression,
|
|
784
|
-
document,
|
|
785
|
-
responses,
|
|
786
|
-
outcomes,
|
|
787
|
-
templateValues,
|
|
788
|
-
correctResponses,
|
|
789
|
-
random,
|
|
790
|
-
customOperators,
|
|
791
|
-
)
|
|
792
|
-
? condition.thenRules
|
|
793
|
-
: undefined;
|
|
794
|
-
if (!branch) {
|
|
795
|
-
for (const elseIf of condition.elseIfs) {
|
|
796
|
-
if (
|
|
797
|
-
evaluateBoolean(
|
|
798
|
-
elseIf.expression,
|
|
799
|
-
document,
|
|
800
|
-
responses,
|
|
801
|
-
outcomes,
|
|
802
|
-
templateValues,
|
|
803
|
-
correctResponses,
|
|
804
|
-
random,
|
|
805
|
-
customOperators,
|
|
806
|
-
)
|
|
807
|
-
) {
|
|
808
|
-
branch = elseIf.rules;
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
branch ??= condition.elseRules;
|
|
814
|
-
return applyResponseRules(
|
|
815
|
-
branch,
|
|
816
|
-
document,
|
|
817
|
-
responses,
|
|
818
|
-
outcomes,
|
|
819
|
-
templateValues,
|
|
820
|
-
correctResponses,
|
|
821
|
-
random,
|
|
822
|
-
customOperators,
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function evaluateBoolean(
|
|
827
|
-
expression: QtiProcessingExpression | undefined,
|
|
828
|
-
document: QtiDocument,
|
|
829
|
-
responses: Record<string, QtiValue>,
|
|
830
|
-
outcomes: Record<string, QtiValue> = {},
|
|
831
|
-
templateValues: Record<string, QtiValue> = {},
|
|
832
|
-
correctResponses: Record<string, QtiValue> = {},
|
|
833
|
-
random: () => number = Math.random,
|
|
834
|
-
customOperators: QtiCustomOperatorRegistry = {},
|
|
835
|
-
): boolean {
|
|
836
|
-
if (!expression) return false;
|
|
837
|
-
return booleanValue(
|
|
838
|
-
evaluateValue(
|
|
839
|
-
expression,
|
|
840
|
-
document,
|
|
841
|
-
responses,
|
|
842
|
-
outcomes,
|
|
843
|
-
templateValues,
|
|
844
|
-
correctResponses,
|
|
845
|
-
random,
|
|
846
|
-
customOperators,
|
|
847
|
-
),
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function evaluateValue(
|
|
852
|
-
expression: QtiProcessingExpression,
|
|
853
|
-
document: QtiDocument,
|
|
854
|
-
responses: Record<string, QtiValue>,
|
|
855
|
-
outcomes: Record<string, QtiValue> = {},
|
|
856
|
-
templateValues: Record<string, QtiValue> = {},
|
|
857
|
-
correctResponses: Record<string, QtiValue> = {},
|
|
858
|
-
random: () => number = Math.random,
|
|
859
|
-
customOperators: QtiCustomOperatorRegistry = {},
|
|
860
|
-
): QtiValue {
|
|
861
|
-
if (expression.type === "baseValue") return expression.value;
|
|
862
|
-
if (expression.type === "null") return null;
|
|
863
|
-
if (expression.type === "isNull") return isNullResponse(responses[expression.identifier] ?? null);
|
|
864
|
-
if (expression.type === "matchCorrect") {
|
|
865
|
-
const declaration = getResponseDeclaration(document, expression.correctIdentifier);
|
|
866
|
-
return declaration
|
|
867
|
-
? qtiMatchValues(
|
|
868
|
-
responses[expression.identifier] ?? null,
|
|
869
|
-
correctResponses[expression.correctIdentifier] ?? null,
|
|
870
|
-
declaration.cardinality === "ordered",
|
|
871
|
-
)
|
|
872
|
-
: false;
|
|
873
|
-
}
|
|
874
|
-
if (expression.type === "match") {
|
|
875
|
-
const left = evaluateValue(
|
|
876
|
-
expression.left,
|
|
877
|
-
document,
|
|
878
|
-
responses,
|
|
879
|
-
outcomes,
|
|
880
|
-
templateValues,
|
|
881
|
-
correctResponses,
|
|
882
|
-
random,
|
|
883
|
-
customOperators,
|
|
884
|
-
);
|
|
885
|
-
const right = evaluateValue(
|
|
886
|
-
expression.right,
|
|
887
|
-
document,
|
|
888
|
-
responses,
|
|
889
|
-
outcomes,
|
|
890
|
-
templateValues,
|
|
891
|
-
correctResponses,
|
|
892
|
-
random,
|
|
893
|
-
customOperators,
|
|
894
|
-
);
|
|
895
|
-
return qtiMatchValues(
|
|
896
|
-
left,
|
|
897
|
-
right,
|
|
898
|
-
expressionIsOrdered(expression.left, document) ||
|
|
899
|
-
expressionIsOrdered(expression.right, document),
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
if (expression.type === "mapResponse") {
|
|
903
|
-
const declaration = getResponseDeclaration(document, expression.identifier);
|
|
904
|
-
return declaration
|
|
905
|
-
? mapOrMatchResponse(
|
|
906
|
-
declaration,
|
|
907
|
-
responses[expression.identifier] ?? null,
|
|
908
|
-
correctResponses[expression.identifier] ?? null,
|
|
909
|
-
)
|
|
910
|
-
: 0;
|
|
911
|
-
}
|
|
912
|
-
if (expression.type === "mapResponsePoint") {
|
|
913
|
-
const declaration = getResponseDeclaration(document, expression.identifier);
|
|
914
|
-
return declaration?.areaMapping
|
|
915
|
-
? scoreAreaMapping(responses[expression.identifier] ?? null, declaration.areaMapping)
|
|
916
|
-
: 0;
|
|
917
|
-
}
|
|
918
|
-
if (expression.type === "correct") {
|
|
919
|
-
return correctResponses[expression.identifier] ?? null;
|
|
920
|
-
}
|
|
921
|
-
if (expression.type === "default") {
|
|
922
|
-
return defaultValueForIdentifier(document, expression.identifier);
|
|
923
|
-
}
|
|
924
|
-
if (expression.type === "variable") {
|
|
925
|
-
return (
|
|
926
|
-
responses[expression.identifier] ??
|
|
927
|
-
outcomes[expression.identifier] ??
|
|
928
|
-
templateValues[expression.identifier] ??
|
|
929
|
-
null
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
if (expression.type === "randomInteger") {
|
|
933
|
-
const step = expression.step > 0 ? expression.step : 1;
|
|
934
|
-
const count = Math.floor((expression.max - expression.min) / step) + 1;
|
|
935
|
-
return expression.min + Math.floor(random() * count) * step;
|
|
936
|
-
}
|
|
937
|
-
if (expression.type === "randomFloat") {
|
|
938
|
-
return expression.min + random() * (expression.max - expression.min);
|
|
939
|
-
}
|
|
940
|
-
if (expression.type === "random") {
|
|
941
|
-
if (expression.values.length === 0) return null;
|
|
942
|
-
const index = Math.floor(random() * expression.values.length);
|
|
943
|
-
return evaluateValue(
|
|
944
|
-
expression.values[index]!,
|
|
945
|
-
document,
|
|
946
|
-
responses,
|
|
947
|
-
outcomes,
|
|
948
|
-
templateValues,
|
|
949
|
-
correctResponses,
|
|
950
|
-
random,
|
|
951
|
-
customOperators,
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
if (expression.type === "multiple" || expression.type === "ordered") {
|
|
955
|
-
const values = expression.expressions.flatMap((item) =>
|
|
956
|
-
valueContainer(
|
|
957
|
-
evaluateValue(
|
|
958
|
-
item,
|
|
959
|
-
document,
|
|
960
|
-
responses,
|
|
961
|
-
outcomes,
|
|
962
|
-
templateValues,
|
|
963
|
-
correctResponses,
|
|
964
|
-
random,
|
|
965
|
-
customOperators,
|
|
966
|
-
),
|
|
967
|
-
),
|
|
968
|
-
);
|
|
969
|
-
return values.length > 0 ? values : null;
|
|
970
|
-
}
|
|
971
|
-
if (expression.type === "index") {
|
|
972
|
-
const values = valueContainer(
|
|
973
|
-
evaluateValue(
|
|
974
|
-
expression.expression,
|
|
975
|
-
document,
|
|
976
|
-
responses,
|
|
977
|
-
outcomes,
|
|
978
|
-
templateValues,
|
|
979
|
-
correctResponses,
|
|
980
|
-
random,
|
|
981
|
-
customOperators,
|
|
982
|
-
),
|
|
983
|
-
);
|
|
984
|
-
const n = indexValue(expression.n, outcomes, templateValues);
|
|
985
|
-
if (n === undefined || n < 1 || n > values.length) return null;
|
|
986
|
-
return values[n - 1] ?? null;
|
|
987
|
-
}
|
|
988
|
-
if (expression.type === "containerSize") {
|
|
989
|
-
return valueContainer(
|
|
990
|
-
evaluateValue(
|
|
991
|
-
expression.expression,
|
|
992
|
-
document,
|
|
993
|
-
responses,
|
|
994
|
-
outcomes,
|
|
995
|
-
templateValues,
|
|
996
|
-
correctResponses,
|
|
997
|
-
random,
|
|
998
|
-
customOperators,
|
|
999
|
-
),
|
|
1000
|
-
).length;
|
|
1001
|
-
}
|
|
1002
|
-
if (expression.type === "sum") {
|
|
1003
|
-
const values = evaluateNumericOperands(
|
|
1004
|
-
expression.expressions,
|
|
1005
|
-
document,
|
|
1006
|
-
responses,
|
|
1007
|
-
outcomes,
|
|
1008
|
-
templateValues,
|
|
1009
|
-
correctResponses,
|
|
1010
|
-
random,
|
|
1011
|
-
customOperators,
|
|
1012
|
-
);
|
|
1013
|
-
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
1014
|
-
}
|
|
1015
|
-
if (expression.type === "product") {
|
|
1016
|
-
const values = evaluateNumericOperands(
|
|
1017
|
-
expression.expressions,
|
|
1018
|
-
document,
|
|
1019
|
-
responses,
|
|
1020
|
-
outcomes,
|
|
1021
|
-
templateValues,
|
|
1022
|
-
correctResponses,
|
|
1023
|
-
random,
|
|
1024
|
-
customOperators,
|
|
1025
|
-
);
|
|
1026
|
-
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
1027
|
-
}
|
|
1028
|
-
if (expression.type === "min" || expression.type === "max") {
|
|
1029
|
-
const values = evaluateNumericOperands(
|
|
1030
|
-
expression.expressions,
|
|
1031
|
-
document,
|
|
1032
|
-
responses,
|
|
1033
|
-
outcomes,
|
|
1034
|
-
templateValues,
|
|
1035
|
-
correctResponses,
|
|
1036
|
-
random,
|
|
1037
|
-
customOperators,
|
|
1038
|
-
);
|
|
1039
|
-
if (!values || values.length === 0) return null;
|
|
1040
|
-
return expression.type === "min" ? Math.min(...values) : Math.max(...values);
|
|
1041
|
-
}
|
|
1042
|
-
if (expression.type === "subtract") {
|
|
1043
|
-
const values = evaluateNumericOperands(
|
|
1044
|
-
[expression.left, expression.right],
|
|
1045
|
-
document,
|
|
1046
|
-
responses,
|
|
1047
|
-
outcomes,
|
|
1048
|
-
templateValues,
|
|
1049
|
-
correctResponses,
|
|
1050
|
-
random,
|
|
1051
|
-
customOperators,
|
|
1052
|
-
);
|
|
1053
|
-
return values && values.length === 2 ? values[0]! - values[1]! : null;
|
|
1054
|
-
}
|
|
1055
|
-
if (expression.type === "divide") {
|
|
1056
|
-
const dividendValue = evaluateValue(
|
|
1057
|
-
expression.left,
|
|
1058
|
-
document,
|
|
1059
|
-
responses,
|
|
1060
|
-
outcomes,
|
|
1061
|
-
templateValues,
|
|
1062
|
-
correctResponses,
|
|
1063
|
-
random,
|
|
1064
|
-
customOperators,
|
|
1065
|
-
);
|
|
1066
|
-
const divisorValue = evaluateValue(
|
|
1067
|
-
expression.right,
|
|
1068
|
-
document,
|
|
1069
|
-
responses,
|
|
1070
|
-
outcomes,
|
|
1071
|
-
templateValues,
|
|
1072
|
-
correctResponses,
|
|
1073
|
-
random,
|
|
1074
|
-
customOperators,
|
|
1075
|
-
);
|
|
1076
|
-
if (dividendValue === null || divisorValue === null) return null;
|
|
1077
|
-
const dividend = numericValueOrNull(dividendValue);
|
|
1078
|
-
const divisor = numericValueOrNull(divisorValue);
|
|
1079
|
-
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1080
|
-
const quotient = dividend / divisor;
|
|
1081
|
-
return Number.isFinite(quotient) ? quotient : null;
|
|
1082
|
-
}
|
|
1083
|
-
if (expression.type === "power") {
|
|
1084
|
-
const values = evaluateNumericOperands(
|
|
1085
|
-
[expression.left, expression.right],
|
|
1086
|
-
document,
|
|
1087
|
-
responses,
|
|
1088
|
-
outcomes,
|
|
1089
|
-
templateValues,
|
|
1090
|
-
correctResponses,
|
|
1091
|
-
random,
|
|
1092
|
-
customOperators,
|
|
1093
|
-
);
|
|
1094
|
-
if (!values || values.length !== 2) return null;
|
|
1095
|
-
const value = Math.pow(values[0]!, values[1]!);
|
|
1096
|
-
return Number.isFinite(value) ? value : null;
|
|
1097
|
-
}
|
|
1098
|
-
if (expression.type === "integerDivide") {
|
|
1099
|
-
const dividendValue = evaluateValue(
|
|
1100
|
-
expression.left,
|
|
1101
|
-
document,
|
|
1102
|
-
responses,
|
|
1103
|
-
outcomes,
|
|
1104
|
-
templateValues,
|
|
1105
|
-
correctResponses,
|
|
1106
|
-
random,
|
|
1107
|
-
customOperators,
|
|
1108
|
-
);
|
|
1109
|
-
const divisorValue = evaluateValue(
|
|
1110
|
-
expression.right,
|
|
1111
|
-
document,
|
|
1112
|
-
responses,
|
|
1113
|
-
outcomes,
|
|
1114
|
-
templateValues,
|
|
1115
|
-
correctResponses,
|
|
1116
|
-
random,
|
|
1117
|
-
customOperators,
|
|
1118
|
-
);
|
|
1119
|
-
if (dividendValue === null || divisorValue === null) return null;
|
|
1120
|
-
const dividend = numericValueOrNull(dividendValue);
|
|
1121
|
-
const divisor = numericValueOrNull(divisorValue);
|
|
1122
|
-
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1123
|
-
return Math.floor(dividend / divisor);
|
|
1124
|
-
}
|
|
1125
|
-
if (expression.type === "integerModulus") {
|
|
1126
|
-
const dividendValue = evaluateValue(
|
|
1127
|
-
expression.left,
|
|
1128
|
-
document,
|
|
1129
|
-
responses,
|
|
1130
|
-
outcomes,
|
|
1131
|
-
templateValues,
|
|
1132
|
-
correctResponses,
|
|
1133
|
-
random,
|
|
1134
|
-
customOperators,
|
|
1135
|
-
);
|
|
1136
|
-
const divisorValue = evaluateValue(
|
|
1137
|
-
expression.right,
|
|
1138
|
-
document,
|
|
1139
|
-
responses,
|
|
1140
|
-
outcomes,
|
|
1141
|
-
templateValues,
|
|
1142
|
-
correctResponses,
|
|
1143
|
-
random,
|
|
1144
|
-
customOperators,
|
|
1145
|
-
);
|
|
1146
|
-
if (dividendValue === null || divisorValue === null) return null;
|
|
1147
|
-
const dividend = numericValueOrNull(dividendValue);
|
|
1148
|
-
const divisor = numericValueOrNull(divisorValue);
|
|
1149
|
-
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1150
|
-
return dividend - Math.floor(dividend / divisor) * divisor;
|
|
1151
|
-
}
|
|
1152
|
-
if (expression.type === "round") {
|
|
1153
|
-
const value = numericValueOrNull(
|
|
1154
|
-
evaluateValue(
|
|
1155
|
-
expression.expression,
|
|
1156
|
-
document,
|
|
1157
|
-
responses,
|
|
1158
|
-
outcomes,
|
|
1159
|
-
templateValues,
|
|
1160
|
-
correctResponses,
|
|
1161
|
-
random,
|
|
1162
|
-
customOperators,
|
|
1163
|
-
),
|
|
1164
|
-
);
|
|
1165
|
-
return value === null ? null : Math.round(value);
|
|
1166
|
-
}
|
|
1167
|
-
if (expression.type === "roundTo") {
|
|
1168
|
-
const value = numericValueOrNull(
|
|
1169
|
-
evaluateValue(
|
|
1170
|
-
expression.expression,
|
|
1171
|
-
document,
|
|
1172
|
-
responses,
|
|
1173
|
-
outcomes,
|
|
1174
|
-
templateValues,
|
|
1175
|
-
correctResponses,
|
|
1176
|
-
random,
|
|
1177
|
-
customOperators,
|
|
1178
|
-
),
|
|
1179
|
-
);
|
|
1180
|
-
if (value === null) return null;
|
|
1181
|
-
return expression.roundingMode === "decimalPlaces"
|
|
1182
|
-
? roundToDecimalPlaces(value, expression.figures)
|
|
1183
|
-
: roundToSignificantFigures(value, expression.figures);
|
|
1184
|
-
}
|
|
1185
|
-
if (expression.type === "truncate") {
|
|
1186
|
-
const value = numericValueOrNull(
|
|
1187
|
-
evaluateValue(
|
|
1188
|
-
expression.expression,
|
|
1189
|
-
document,
|
|
1190
|
-
responses,
|
|
1191
|
-
outcomes,
|
|
1192
|
-
templateValues,
|
|
1193
|
-
correctResponses,
|
|
1194
|
-
random,
|
|
1195
|
-
customOperators,
|
|
1196
|
-
),
|
|
1197
|
-
);
|
|
1198
|
-
return value === null ? null : Math.trunc(value);
|
|
1199
|
-
}
|
|
1200
|
-
if (expression.type === "integerToFloat") {
|
|
1201
|
-
const value = evaluateValue(
|
|
1202
|
-
expression.expression,
|
|
1203
|
-
document,
|
|
1204
|
-
responses,
|
|
1205
|
-
outcomes,
|
|
1206
|
-
templateValues,
|
|
1207
|
-
correctResponses,
|
|
1208
|
-
random,
|
|
1209
|
-
customOperators,
|
|
1210
|
-
);
|
|
1211
|
-
return numericValueOrNull(value);
|
|
1212
|
-
}
|
|
1213
|
-
if (expression.type === "and") {
|
|
1214
|
-
let sawNull = false;
|
|
1215
|
-
for (const item of expression.expressions) {
|
|
1216
|
-
const value = booleanValueOrNull(
|
|
1217
|
-
evaluateValue(
|
|
1218
|
-
item,
|
|
1219
|
-
document,
|
|
1220
|
-
responses,
|
|
1221
|
-
outcomes,
|
|
1222
|
-
templateValues,
|
|
1223
|
-
correctResponses,
|
|
1224
|
-
random,
|
|
1225
|
-
customOperators,
|
|
1226
|
-
),
|
|
1227
|
-
);
|
|
1228
|
-
if (value === false) return false;
|
|
1229
|
-
if (value === null) sawNull = true;
|
|
1230
|
-
}
|
|
1231
|
-
return sawNull ? null : true;
|
|
1232
|
-
}
|
|
1233
|
-
if (expression.type === "anyN") {
|
|
1234
|
-
const min = indexValue(expression.min, outcomes, templateValues) ?? 0;
|
|
1235
|
-
const max = indexValue(expression.max, outcomes, templateValues) ?? 0;
|
|
1236
|
-
const values = expression.expressions.map((item) =>
|
|
1237
|
-
evaluateValue(
|
|
1238
|
-
item,
|
|
1239
|
-
document,
|
|
1240
|
-
responses,
|
|
1241
|
-
outcomes,
|
|
1242
|
-
templateValues,
|
|
1243
|
-
correctResponses,
|
|
1244
|
-
random,
|
|
1245
|
-
customOperators,
|
|
1246
|
-
),
|
|
1247
|
-
);
|
|
1248
|
-
const trueCount = values.filter((value) => value === true).length;
|
|
1249
|
-
const nullCount = values.filter((value) => value === null).length;
|
|
1250
|
-
if (min > max || trueCount > max || trueCount + nullCount < min) return false;
|
|
1251
|
-
if (trueCount >= min && trueCount <= max) return true;
|
|
1252
|
-
return null;
|
|
1253
|
-
}
|
|
1254
|
-
if (expression.type === "or") {
|
|
1255
|
-
let sawNull = false;
|
|
1256
|
-
for (const item of expression.expressions) {
|
|
1257
|
-
const value = booleanValueOrNull(
|
|
1258
|
-
evaluateValue(
|
|
1259
|
-
item,
|
|
1260
|
-
document,
|
|
1261
|
-
responses,
|
|
1262
|
-
outcomes,
|
|
1263
|
-
templateValues,
|
|
1264
|
-
correctResponses,
|
|
1265
|
-
random,
|
|
1266
|
-
customOperators,
|
|
1267
|
-
),
|
|
1268
|
-
);
|
|
1269
|
-
if (value === true) return true;
|
|
1270
|
-
if (value === null) sawNull = true;
|
|
1271
|
-
}
|
|
1272
|
-
return sawNull ? null : false;
|
|
1273
|
-
}
|
|
1274
|
-
if (expression.type === "not") {
|
|
1275
|
-
const value = booleanValueOrNull(
|
|
1276
|
-
evaluateValue(
|
|
1277
|
-
expression.expression,
|
|
1278
|
-
document,
|
|
1279
|
-
responses,
|
|
1280
|
-
outcomes,
|
|
1281
|
-
templateValues,
|
|
1282
|
-
correctResponses,
|
|
1283
|
-
random,
|
|
1284
|
-
customOperators,
|
|
1285
|
-
),
|
|
1286
|
-
);
|
|
1287
|
-
return value === null ? null : !value;
|
|
1288
|
-
}
|
|
1289
|
-
if (expression.type === "equal") {
|
|
1290
|
-
const left = evaluateValue(
|
|
1291
|
-
expression.left,
|
|
1292
|
-
document,
|
|
1293
|
-
responses,
|
|
1294
|
-
outcomes,
|
|
1295
|
-
templateValues,
|
|
1296
|
-
correctResponses,
|
|
1297
|
-
random,
|
|
1298
|
-
customOperators,
|
|
1299
|
-
);
|
|
1300
|
-
const right = evaluateValue(
|
|
1301
|
-
expression.right,
|
|
1302
|
-
document,
|
|
1303
|
-
responses,
|
|
1304
|
-
outcomes,
|
|
1305
|
-
templateValues,
|
|
1306
|
-
correctResponses,
|
|
1307
|
-
random,
|
|
1308
|
-
customOperators,
|
|
1309
|
-
);
|
|
1310
|
-
return left === null || right === null ? null : valuesEqual(left, right);
|
|
1311
|
-
}
|
|
1312
|
-
if (expression.type === "equalRounded") {
|
|
1313
|
-
const left = evaluateValue(
|
|
1314
|
-
expression.left,
|
|
1315
|
-
document,
|
|
1316
|
-
responses,
|
|
1317
|
-
outcomes,
|
|
1318
|
-
templateValues,
|
|
1319
|
-
correctResponses,
|
|
1320
|
-
random,
|
|
1321
|
-
customOperators,
|
|
1322
|
-
);
|
|
1323
|
-
const right = evaluateValue(
|
|
1324
|
-
expression.right,
|
|
1325
|
-
document,
|
|
1326
|
-
responses,
|
|
1327
|
-
outcomes,
|
|
1328
|
-
templateValues,
|
|
1329
|
-
correctResponses,
|
|
1330
|
-
random,
|
|
1331
|
-
customOperators,
|
|
1332
|
-
);
|
|
1333
|
-
if (left === null || right === null) return null;
|
|
1334
|
-
const leftNumber = numericValueOrNull(left);
|
|
1335
|
-
const rightNumber = numericValueOrNull(right);
|
|
1336
|
-
if (leftNumber === null || rightNumber === null) return null;
|
|
1337
|
-
const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
|
|
1338
|
-
const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
|
|
1339
|
-
return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
|
|
1340
|
-
}
|
|
1341
|
-
if (expression.type === "numericCompare") {
|
|
1342
|
-
const leftValue = evaluateValue(
|
|
1343
|
-
expression.left,
|
|
1344
|
-
document,
|
|
1345
|
-
responses,
|
|
1346
|
-
outcomes,
|
|
1347
|
-
templateValues,
|
|
1348
|
-
correctResponses,
|
|
1349
|
-
random,
|
|
1350
|
-
customOperators,
|
|
1351
|
-
);
|
|
1352
|
-
const rightValue = evaluateValue(
|
|
1353
|
-
expression.right,
|
|
1354
|
-
document,
|
|
1355
|
-
responses,
|
|
1356
|
-
outcomes,
|
|
1357
|
-
templateValues,
|
|
1358
|
-
correctResponses,
|
|
1359
|
-
random,
|
|
1360
|
-
customOperators,
|
|
1361
|
-
);
|
|
1362
|
-
if (leftValue === null || rightValue === null) return null;
|
|
1363
|
-
const left = numericValueOrNull(leftValue);
|
|
1364
|
-
const right = numericValueOrNull(rightValue);
|
|
1365
|
-
if (left === null || right === null) return null;
|
|
1366
|
-
if (expression.operator === "lt") return left < right;
|
|
1367
|
-
if (expression.operator === "lte") return left <= right;
|
|
1368
|
-
if (expression.operator === "gt") return left > right;
|
|
1369
|
-
return left >= right;
|
|
1370
|
-
}
|
|
1371
|
-
if (expression.type === "durationCompare") {
|
|
1372
|
-
const leftValue = evaluateValue(
|
|
1373
|
-
expression.left,
|
|
1374
|
-
document,
|
|
1375
|
-
responses,
|
|
1376
|
-
outcomes,
|
|
1377
|
-
templateValues,
|
|
1378
|
-
correctResponses,
|
|
1379
|
-
random,
|
|
1380
|
-
customOperators,
|
|
1381
|
-
);
|
|
1382
|
-
const rightValue = evaluateValue(
|
|
1383
|
-
expression.right,
|
|
1384
|
-
document,
|
|
1385
|
-
responses,
|
|
1386
|
-
outcomes,
|
|
1387
|
-
templateValues,
|
|
1388
|
-
correctResponses,
|
|
1389
|
-
random,
|
|
1390
|
-
customOperators,
|
|
1391
|
-
);
|
|
1392
|
-
const left = durationSeconds(leftValue);
|
|
1393
|
-
const right = durationSeconds(rightValue);
|
|
1394
|
-
if (left === null || right === null) return null;
|
|
1395
|
-
return expression.operator === "lt" ? left < right : left >= right;
|
|
1396
|
-
}
|
|
1397
|
-
if (expression.type === "stringMatch") {
|
|
1398
|
-
return stringMatch(
|
|
1399
|
-
evaluateValue(
|
|
1400
|
-
expression.left,
|
|
1401
|
-
document,
|
|
1402
|
-
responses,
|
|
1403
|
-
outcomes,
|
|
1404
|
-
templateValues,
|
|
1405
|
-
correctResponses,
|
|
1406
|
-
random,
|
|
1407
|
-
customOperators,
|
|
1408
|
-
),
|
|
1409
|
-
evaluateValue(
|
|
1410
|
-
expression.right,
|
|
1411
|
-
document,
|
|
1412
|
-
responses,
|
|
1413
|
-
outcomes,
|
|
1414
|
-
templateValues,
|
|
1415
|
-
correctResponses,
|
|
1416
|
-
random,
|
|
1417
|
-
customOperators,
|
|
1418
|
-
),
|
|
1419
|
-
expression.caseSensitive,
|
|
1420
|
-
expression.substring,
|
|
1421
|
-
);
|
|
1422
|
-
}
|
|
1423
|
-
if (expression.type === "substring") {
|
|
1424
|
-
return stringMatch(
|
|
1425
|
-
evaluateValue(
|
|
1426
|
-
expression.right,
|
|
1427
|
-
document,
|
|
1428
|
-
responses,
|
|
1429
|
-
outcomes,
|
|
1430
|
-
templateValues,
|
|
1431
|
-
correctResponses,
|
|
1432
|
-
random,
|
|
1433
|
-
customOperators,
|
|
1434
|
-
),
|
|
1435
|
-
evaluateValue(
|
|
1436
|
-
expression.left,
|
|
1437
|
-
document,
|
|
1438
|
-
responses,
|
|
1439
|
-
outcomes,
|
|
1440
|
-
templateValues,
|
|
1441
|
-
correctResponses,
|
|
1442
|
-
random,
|
|
1443
|
-
customOperators,
|
|
1444
|
-
),
|
|
1445
|
-
expression.caseSensitive,
|
|
1446
|
-
true,
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
if (expression.type === "patternMatch") {
|
|
1450
|
-
const value = evaluateValue(
|
|
1451
|
-
expression.expression,
|
|
1452
|
-
document,
|
|
1453
|
-
responses,
|
|
1454
|
-
outcomes,
|
|
1455
|
-
templateValues,
|
|
1456
|
-
correctResponses,
|
|
1457
|
-
random,
|
|
1458
|
-
customOperators,
|
|
1459
|
-
);
|
|
1460
|
-
if (value === null) return null;
|
|
1461
|
-
const patternValue =
|
|
1462
|
-
responses[expression.pattern] ??
|
|
1463
|
-
outcomes[expression.pattern] ??
|
|
1464
|
-
templateValues[expression.pattern] ??
|
|
1465
|
-
expression.pattern;
|
|
1466
|
-
try {
|
|
1467
|
-
return new RegExp(
|
|
1468
|
-
typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue),
|
|
1469
|
-
).test(qtiValueToString(value));
|
|
1470
|
-
} catch {
|
|
1471
|
-
return null;
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
if (expression.type === "fieldValue") {
|
|
1475
|
-
const value = evaluateValue(
|
|
1476
|
-
expression.expression,
|
|
1477
|
-
document,
|
|
1478
|
-
responses,
|
|
1479
|
-
outcomes,
|
|
1480
|
-
templateValues,
|
|
1481
|
-
correctResponses,
|
|
1482
|
-
random,
|
|
1483
|
-
customOperators,
|
|
1484
|
-
);
|
|
1485
|
-
return isRecordValue(value) ? (value[expression.fieldIdentifier] ?? null) : null;
|
|
1486
|
-
}
|
|
1487
|
-
if (expression.type === "member") {
|
|
1488
|
-
const value = evaluateValue(
|
|
1489
|
-
expression.value,
|
|
1490
|
-
document,
|
|
1491
|
-
responses,
|
|
1492
|
-
outcomes,
|
|
1493
|
-
templateValues,
|
|
1494
|
-
correctResponses,
|
|
1495
|
-
random,
|
|
1496
|
-
customOperators,
|
|
1497
|
-
);
|
|
1498
|
-
const collection = evaluateValue(
|
|
1499
|
-
expression.collection,
|
|
1500
|
-
document,
|
|
1501
|
-
responses,
|
|
1502
|
-
outcomes,
|
|
1503
|
-
templateValues,
|
|
1504
|
-
correctResponses,
|
|
1505
|
-
random,
|
|
1506
|
-
customOperators,
|
|
1507
|
-
);
|
|
1508
|
-
if (value === null || collection === null) return null;
|
|
1509
|
-
const values = valueContainer(collection);
|
|
1510
|
-
return values.some((item) => valuesEqual(item, value));
|
|
1511
|
-
}
|
|
1512
|
-
if (expression.type === "delete") {
|
|
1513
|
-
const value = evaluateValue(
|
|
1514
|
-
expression.value,
|
|
1515
|
-
document,
|
|
1516
|
-
responses,
|
|
1517
|
-
outcomes,
|
|
1518
|
-
templateValues,
|
|
1519
|
-
correctResponses,
|
|
1520
|
-
random,
|
|
1521
|
-
customOperators,
|
|
1522
|
-
);
|
|
1523
|
-
const collectionValue = evaluateValue(
|
|
1524
|
-
expression.collection,
|
|
1525
|
-
document,
|
|
1526
|
-
responses,
|
|
1527
|
-
outcomes,
|
|
1528
|
-
templateValues,
|
|
1529
|
-
correctResponses,
|
|
1530
|
-
random,
|
|
1531
|
-
customOperators,
|
|
1532
|
-
);
|
|
1533
|
-
if (value === null || collectionValue === null) return null;
|
|
1534
|
-
const collection = valueContainer(collectionValue);
|
|
1535
|
-
if (collection.length === 0) return null;
|
|
1536
|
-
const filtered = collection.filter((item) => !valuesEqual(item, value));
|
|
1537
|
-
return filtered.length > 0 ? filtered : null;
|
|
1538
|
-
}
|
|
1539
|
-
if (expression.type === "contains") {
|
|
1540
|
-
const collectionValue = evaluateValue(
|
|
1541
|
-
expression.collection,
|
|
1542
|
-
document,
|
|
1543
|
-
responses,
|
|
1544
|
-
outcomes,
|
|
1545
|
-
templateValues,
|
|
1546
|
-
correctResponses,
|
|
1547
|
-
random,
|
|
1548
|
-
customOperators,
|
|
1549
|
-
);
|
|
1550
|
-
const valuesValue = evaluateValue(
|
|
1551
|
-
expression.values,
|
|
1552
|
-
document,
|
|
1553
|
-
responses,
|
|
1554
|
-
outcomes,
|
|
1555
|
-
templateValues,
|
|
1556
|
-
correctResponses,
|
|
1557
|
-
random,
|
|
1558
|
-
customOperators,
|
|
1559
|
-
);
|
|
1560
|
-
if (collectionValue === null || valuesValue === null) return null;
|
|
1561
|
-
const collection = valueContainer(collectionValue);
|
|
1562
|
-
const values = valueContainer(valuesValue);
|
|
1563
|
-
if (collection.length === 0 || values.length === 0) return null;
|
|
1564
|
-
return containsValues(collection, values);
|
|
1565
|
-
}
|
|
1566
|
-
if (expression.type === "gcd" || expression.type === "lcm") {
|
|
1567
|
-
const values = expression.expressions.flatMap((item) => {
|
|
1568
|
-
const value = evaluateValue(
|
|
1569
|
-
item,
|
|
1570
|
-
document,
|
|
1571
|
-
responses,
|
|
1572
|
-
outcomes,
|
|
1573
|
-
templateValues,
|
|
1574
|
-
correctResponses,
|
|
1575
|
-
random,
|
|
1576
|
-
customOperators,
|
|
1577
|
-
);
|
|
1578
|
-
return value === null ? [null] : valueContainer(value);
|
|
1579
|
-
});
|
|
1580
|
-
if (values.length === 0 || values.some((value) => value === null)) return null;
|
|
1581
|
-
const integers = values.map((value) => Math.trunc(numericValue(value)));
|
|
1582
|
-
return expression.type === "gcd" ? generalizedGcd(integers) : generalizedLcm(integers);
|
|
1583
|
-
}
|
|
1584
|
-
if (expression.type === "inside") {
|
|
1585
|
-
const value = evaluateValue(
|
|
1586
|
-
expression.expression,
|
|
1587
|
-
document,
|
|
1588
|
-
responses,
|
|
1589
|
-
outcomes,
|
|
1590
|
-
templateValues,
|
|
1591
|
-
correctResponses,
|
|
1592
|
-
random,
|
|
1593
|
-
customOperators,
|
|
1594
|
-
);
|
|
1595
|
-
if (value === null) return null;
|
|
1596
|
-
if (expression.shape === "default") return true;
|
|
1597
|
-
return valueContainer(value).some((pointValue) => {
|
|
1598
|
-
const point = parsePoint(String(pointValue));
|
|
1599
|
-
return point
|
|
1600
|
-
? pointInsideArea(point, {
|
|
1601
|
-
shape: expression.shape,
|
|
1602
|
-
coords: expression.coords,
|
|
1603
|
-
mappedValue: 0,
|
|
1604
|
-
attributes: {},
|
|
1605
|
-
})
|
|
1606
|
-
: false;
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
if (expression.type === "mathConstant") {
|
|
1610
|
-
if (expression.name === "pi") return Math.PI;
|
|
1611
|
-
if (expression.name === "e") return Math.E;
|
|
1612
|
-
return null;
|
|
1613
|
-
}
|
|
1614
|
-
if (expression.type === "mathOperator") {
|
|
1615
|
-
const values = expression.expressions.map((item) =>
|
|
1616
|
-
evaluateValue(
|
|
1617
|
-
item,
|
|
1618
|
-
document,
|
|
1619
|
-
responses,
|
|
1620
|
-
outcomes,
|
|
1621
|
-
templateValues,
|
|
1622
|
-
correctResponses,
|
|
1623
|
-
random,
|
|
1624
|
-
customOperators,
|
|
1625
|
-
),
|
|
1626
|
-
);
|
|
1627
|
-
if (values.length === 0 || values.some((value) => value === null)) return null;
|
|
1628
|
-
return mathOperatorValue(expression.name, values.map(numericValue));
|
|
1629
|
-
}
|
|
1630
|
-
if (expression.type === "repeat") {
|
|
1631
|
-
const repeats = indexValue(expression.numberRepeats, outcomes, templateValues);
|
|
1632
|
-
if (repeats === undefined || repeats < 1) return null;
|
|
1633
|
-
const container: QtiScalarValue[] = [];
|
|
1634
|
-
for (let repeat = 0; repeat < repeats; repeat += 1) {
|
|
1635
|
-
for (const item of expression.expressions) {
|
|
1636
|
-
const value = evaluateValue(
|
|
1637
|
-
item,
|
|
1638
|
-
document,
|
|
1639
|
-
responses,
|
|
1640
|
-
outcomes,
|
|
1641
|
-
templateValues,
|
|
1642
|
-
correctResponses,
|
|
1643
|
-
random,
|
|
1644
|
-
customOperators,
|
|
1645
|
-
);
|
|
1646
|
-
container.push(...valueContainer(value));
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
return container.length > 0 ? container : null;
|
|
1650
|
-
}
|
|
1651
|
-
if (expression.type === "statsOperator") {
|
|
1652
|
-
const value = evaluateValue(
|
|
1653
|
-
expression.expression,
|
|
1654
|
-
document,
|
|
1655
|
-
responses,
|
|
1656
|
-
outcomes,
|
|
1657
|
-
templateValues,
|
|
1658
|
-
correctResponses,
|
|
1659
|
-
random,
|
|
1660
|
-
customOperators,
|
|
1661
|
-
);
|
|
1662
|
-
if (value === null) return null;
|
|
1663
|
-
const values = valueContainer(value).map(numericValue);
|
|
1664
|
-
return statsOperatorValue(expression.name, values);
|
|
1665
|
-
}
|
|
1666
|
-
if (expression.type === "customOperator") {
|
|
1667
|
-
const operatorKey = expression.definition ?? expression.className ?? "";
|
|
1668
|
-
const handler = customOperators[operatorKey];
|
|
1669
|
-
if (!handler) return null;
|
|
1670
|
-
return handler({
|
|
1671
|
-
definition: expression.definition,
|
|
1672
|
-
className: expression.className,
|
|
1673
|
-
attributes: expression.attributes,
|
|
1674
|
-
values: expression.expressions.map((item) =>
|
|
1675
|
-
evaluateValue(
|
|
1676
|
-
item,
|
|
1677
|
-
document,
|
|
1678
|
-
responses,
|
|
1679
|
-
outcomes,
|
|
1680
|
-
templateValues,
|
|
1681
|
-
correctResponses,
|
|
1682
|
-
random,
|
|
1683
|
-
customOperators,
|
|
1684
|
-
),
|
|
1685
|
-
),
|
|
1686
|
-
expression,
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
return null;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
function getResponseDeclaration(
|
|
1693
|
-
document: QtiDocument,
|
|
1694
|
-
identifier: string,
|
|
1695
|
-
): QtiResponseDeclaration | undefined {
|
|
1696
|
-
return document.item.responseDeclarations.find(
|
|
1697
|
-
(declaration) => declaration.identifier === identifier,
|
|
1698
|
-
);
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function expressionIsOrdered(expression: QtiProcessingExpression, document: QtiDocument): boolean {
|
|
1702
|
-
if (expression.type === "ordered") return true;
|
|
1703
|
-
if (
|
|
1704
|
-
(expression.type === "variable" ||
|
|
1705
|
-
expression.type === "correct" ||
|
|
1706
|
-
expression.type === "default" ||
|
|
1707
|
-
expression.type === "isNull") &&
|
|
1708
|
-
variableCardinality(document, expression.identifier) === "ordered"
|
|
1709
|
-
) {
|
|
1710
|
-
return true;
|
|
1711
|
-
}
|
|
1712
|
-
return false;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
function variableCardinality(document: QtiDocument, identifier: string): string | undefined {
|
|
1716
|
-
return (
|
|
1717
|
-
document.item.responseDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
1718
|
-
?.cardinality ??
|
|
1719
|
-
document.item.outcomeDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
1720
|
-
?.cardinality ??
|
|
1721
|
-
document.item.templateDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
1722
|
-
?.cardinality
|
|
1723
|
-
);
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
function defaultValueForIdentifier(document: QtiDocument, identifier: string): QtiValue {
|
|
1727
|
-
return (
|
|
1728
|
-
document.item.responseDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
1729
|
-
?.defaultValue ??
|
|
1730
|
-
document.item.outcomeDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
1731
|
-
?.defaultValue ??
|
|
1732
|
-
document.item.templateDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
1733
|
-
?.defaultValue ??
|
|
1734
|
-
null
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
function lookupOutcomeValue(document: QtiDocument, identifier: string, value: QtiValue): QtiValue {
|
|
1739
|
-
const declaration = document.item.outcomeDeclarations.find(
|
|
1740
|
-
(outcome) => outcome.identifier === identifier,
|
|
1741
|
-
);
|
|
1742
|
-
const lookupTable = declaration?.lookupTable;
|
|
1743
|
-
if (!lookupTable) return null;
|
|
1744
|
-
if (value === null) return lookupTable.defaultValue;
|
|
1745
|
-
const numeric = numericValue(value);
|
|
1746
|
-
if (lookupTable.type === "match") {
|
|
1747
|
-
return (
|
|
1748
|
-
lookupTable.entries.find((entry) => entry.sourceValue === numeric)?.targetValue ??
|
|
1749
|
-
lookupTable.defaultValue
|
|
1750
|
-
);
|
|
1751
|
-
}
|
|
1752
|
-
const entry = [...lookupTable.entries]
|
|
1753
|
-
.sort((left, right) => left.sourceValue - right.sourceValue)
|
|
1754
|
-
.find(
|
|
1755
|
-
(candidate) =>
|
|
1756
|
-
numeric < candidate.sourceValue ||
|
|
1757
|
-
(candidate.includeBoundary !== false && numeric === candidate.sourceValue),
|
|
1758
|
-
);
|
|
1759
|
-
return entry?.targetValue ?? lookupTable.defaultValue;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function mapOrMatchResponse(
|
|
1763
|
-
declaration: QtiResponseDeclaration,
|
|
1764
|
-
response: QtiValue,
|
|
1765
|
-
correctResponse: QtiValue,
|
|
1766
|
-
): number {
|
|
1767
|
-
if (declaration.areaMapping) return scoreAreaMapping(response, declaration.areaMapping);
|
|
1768
|
-
if (declaration.mapping) return scoreMapping(response, declaration.mapping);
|
|
1769
|
-
return valuesEqual(response, correctResponse, declaration.cardinality === "ordered") ? 1 : 0;
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
function scoreAreaMapping(
|
|
1773
|
-
response: QtiValue,
|
|
1774
|
-
areaMapping: NonNullable<QtiResponseDeclaration["areaMapping"]>,
|
|
1775
|
-
): number {
|
|
1776
|
-
const points = Array.isArray(response)
|
|
1777
|
-
? response.map(qtiScalarToString)
|
|
1778
|
-
: response === null
|
|
1779
|
-
? []
|
|
1780
|
-
: qtiValueToStringList(response);
|
|
1781
|
-
let score = 0;
|
|
1782
|
-
for (const point of points) {
|
|
1783
|
-
const parsed = parsePoint(String(point));
|
|
1784
|
-
if (!parsed) {
|
|
1785
|
-
score += areaMapping.defaultValue;
|
|
1786
|
-
continue;
|
|
1787
|
-
}
|
|
1788
|
-
const entry = areaMapping.entries.find((candidate) => pointInsideArea(parsed, candidate));
|
|
1789
|
-
score += entry?.mappedValue ?? areaMapping.defaultValue;
|
|
1790
|
-
}
|
|
1791
|
-
return clampMappedScore(score, areaMapping.attributes);
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
function parsePoint(value: string): { x: number; y: number } | undefined {
|
|
1795
|
-
const [x, y] = value
|
|
1796
|
-
.trim()
|
|
1797
|
-
.split(/[,\s]+/)
|
|
1798
|
-
.map((part) => Number(part));
|
|
1799
|
-
if (x === undefined || y === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1800
|
-
return undefined;
|
|
1801
|
-
}
|
|
1802
|
-
return { x, y };
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
function pointInsideArea(
|
|
1806
|
-
point: { x: number; y: number },
|
|
1807
|
-
entry: NonNullable<QtiResponseDeclaration["areaMapping"]>["entries"][number],
|
|
1808
449
|
): boolean {
|
|
1809
|
-
|
|
1810
|
-
const [cx, cy, radius] = entry.coords;
|
|
1811
|
-
if (cx === undefined || cy === undefined || radius === undefined) return false;
|
|
1812
|
-
return Math.hypot(point.x - cx, point.y - cy) <= radius;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
if (entry.shape === "rect") {
|
|
1816
|
-
const [left, top, right, bottom] = entry.coords;
|
|
1817
|
-
if (left === undefined || top === undefined || right === undefined || bottom === undefined) {
|
|
1818
|
-
return false;
|
|
1819
|
-
}
|
|
1820
|
-
return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
if (entry.shape === "poly") {
|
|
1824
|
-
return pointInsidePolygon(point, entry.coords);
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
return false;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
function pointInsidePolygon(point: { x: number; y: number }, coords: number[]): boolean {
|
|
1831
|
-
if (coords.length < 6 || coords.length % 2 !== 0) return false;
|
|
1832
|
-
let inside = false;
|
|
1833
|
-
for (let index = 0, previous = coords.length - 2; index < coords.length; index += 2) {
|
|
1834
|
-
const xi = coords[index]!;
|
|
1835
|
-
const yi = coords[index + 1]!;
|
|
1836
|
-
const xj = coords[previous]!;
|
|
1837
|
-
const yj = coords[previous + 1]!;
|
|
1838
|
-
const intersects =
|
|
1839
|
-
yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
|
|
1840
|
-
if (intersects) inside = !inside;
|
|
1841
|
-
previous = index;
|
|
1842
|
-
}
|
|
1843
|
-
return inside;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
function isNullResponse(response: QtiValue): boolean {
|
|
1847
|
-
return response === null || response === "" || (Array.isArray(response) && response.length === 0);
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
function serialize(
|
|
1851
|
-
itemIdentifier: string,
|
|
1852
|
-
status: QtiAttemptStatus,
|
|
1853
|
-
responses: Record<string, QtiValue>,
|
|
1854
|
-
outcomes: Record<string, QtiValue>,
|
|
1855
|
-
templateValues: Record<string, QtiValue>,
|
|
1856
|
-
interactionStates: Record<string, QtiPortableCustomStateValue>,
|
|
1857
|
-
validationMessages: QtiDiagnostic[],
|
|
1858
|
-
): QtiAttemptStateV1 {
|
|
1859
|
-
return {
|
|
1860
|
-
schema: ATTEMPT_STATE_SCHEMA,
|
|
1861
|
-
itemIdentifier,
|
|
1862
|
-
status,
|
|
1863
|
-
responses: cloneValueRecord(responses),
|
|
1864
|
-
outcomes: cloneValueRecord(outcomes),
|
|
1865
|
-
templateValues: cloneValueRecord(templateValues),
|
|
1866
|
-
interactionStates: clonePortableCustomStateRecord(interactionStates),
|
|
1867
|
-
validationMessages: cloneDiagnostics(validationMessages),
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
function cloneValueRecord(record: Record<string, QtiValue>): Record<string, QtiValue> {
|
|
1872
|
-
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, cloneValue(value)]));
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
function clonePortableCustomStateRecord(
|
|
1876
|
-
record: Record<string, QtiPortableCustomStateValue>,
|
|
1877
|
-
): Record<string, QtiPortableCustomStateValue> {
|
|
1878
|
-
return Object.fromEntries(
|
|
1879
|
-
Object.entries(record).map(([key, value]) => [key, clonePortableCustomState(value)]),
|
|
1880
|
-
);
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
function cloneValue(value: QtiValue): QtiValue {
|
|
1884
|
-
if (Array.isArray(value)) return [...value];
|
|
1885
|
-
if (isRecordValue(value)) return cloneValueRecord(value);
|
|
1886
|
-
return value;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
function clonePortableCustomState(value: QtiPortableCustomStateValue): QtiPortableCustomStateValue {
|
|
1890
|
-
if (Array.isArray(value)) return value.map(clonePortableCustomState);
|
|
1891
|
-
if (isPortableCustomStateObject(value)) return clonePortableCustomStateRecord(value);
|
|
1892
|
-
return value;
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
function cloneDiagnostics(diagnostics: QtiDiagnostic[]): QtiDiagnostic[] {
|
|
1896
|
-
return diagnostics.map((diagnostic) => ({
|
|
1897
|
-
...diagnostic,
|
|
1898
|
-
source: diagnostic.source ? { ...diagnostic.source } : undefined,
|
|
1899
|
-
}));
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
function scoreMapping(
|
|
1903
|
-
response: QtiValue,
|
|
1904
|
-
mapping: NonNullable<QtiResponseDeclaration["mapping"]>,
|
|
1905
|
-
): number {
|
|
1906
|
-
const values = Object.fromEntries(
|
|
1907
|
-
mapping.entries
|
|
1908
|
-
.filter((entry) => entry.mapKey !== undefined)
|
|
1909
|
-
.map((entry) => [entry.mapKey!, entry.mappedValue]),
|
|
1910
|
-
);
|
|
1911
|
-
if (Array.isArray(response)) {
|
|
1912
|
-
const score = response.reduce<number>(
|
|
1913
|
-
(sum, value) => sum + (values[String(value)] ?? mapping.defaultValue),
|
|
1914
|
-
0,
|
|
1915
|
-
);
|
|
1916
|
-
return clampMappedScore(score, mapping.attributes);
|
|
1917
|
-
}
|
|
1918
|
-
const score =
|
|
1919
|
-
response === null || isRecordValue(response)
|
|
1920
|
-
? 0
|
|
1921
|
-
: (values[String(response)] ?? mapping.defaultValue);
|
|
1922
|
-
return clampMappedScore(score, mapping.attributes);
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
function clampMappedScore(score: number, attributes: Record<string, string>): number {
|
|
1926
|
-
const lower = numericBound(attributes["lower-bound"]);
|
|
1927
|
-
const upper = numericBound(attributes["upper-bound"]);
|
|
1928
|
-
let clamped = score;
|
|
1929
|
-
if (lower !== undefined) clamped = Math.max(clamped, lower);
|
|
1930
|
-
if (upper !== undefined) clamped = Math.min(clamped, upper);
|
|
1931
|
-
return clamped;
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
function numericBound(value: string | undefined): number | undefined {
|
|
1935
|
-
if (value === undefined) return undefined;
|
|
1936
|
-
const numberValue = Number(value);
|
|
1937
|
-
return Number.isFinite(numberValue) ? numberValue : undefined;
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
function valuesEqual(actual: QtiValue, expected: QtiValue, ordered = false): boolean {
|
|
1941
|
-
if (isRecordValue(actual) || isRecordValue(expected)) {
|
|
1942
|
-
if (!isRecordValue(actual) || !isRecordValue(expected)) return false;
|
|
1943
|
-
const actualKeys = Object.keys(actual).sort();
|
|
1944
|
-
const expectedKeys = Object.keys(expected).sort();
|
|
1945
|
-
return (
|
|
1946
|
-
valuesEqual(actualKeys, expectedKeys, true) &&
|
|
1947
|
-
actualKeys.every((key) => valuesEqual(actual[key] ?? null, expected[key] ?? null, ordered))
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
if (Array.isArray(actual) || Array.isArray(expected)) {
|
|
1951
|
-
const actualValues = valueContainer(actual);
|
|
1952
|
-
const expectedValues = Array.isArray(expected) ? expected : expected === null ? [] : [expected];
|
|
1953
|
-
if (actualValues.length !== expectedValues.length) return false;
|
|
1954
|
-
if (ordered)
|
|
1955
|
-
return actualValues.every((value, index) => scalarValuesEqual(value, expectedValues[index]!));
|
|
1956
|
-
const sortedExpected = [...expectedValues].sort(compareScalarValues);
|
|
1957
|
-
return [...actualValues]
|
|
1958
|
-
.sort(compareScalarValues)
|
|
1959
|
-
.every((value, index) => scalarValuesEqual(value, sortedExpected[index]!));
|
|
1960
|
-
}
|
|
1961
|
-
return scalarValuesEqual(actual, expected);
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
function qtiMatchValues(actual: QtiValue, expected: QtiValue, ordered = false): boolean | null {
|
|
1965
|
-
if (actual === null || expected === null) return null;
|
|
1966
|
-
return valuesEqual(actual, expected, ordered);
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function scalarValuesEqual(actual: QtiValue, expected: QtiValue): boolean {
|
|
1970
|
-
if (typeof actual === "boolean" && typeof expected === "string") {
|
|
1971
|
-
return String(actual) === expected;
|
|
1972
|
-
}
|
|
1973
|
-
if (typeof actual === "string" && typeof expected === "boolean") {
|
|
1974
|
-
return actual === String(expected);
|
|
1975
|
-
}
|
|
1976
|
-
return actual === expected;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
function normalizeValueForCardinality(
|
|
1980
|
-
value: QtiValue,
|
|
1981
|
-
cardinality: QtiResponseDeclaration["cardinality"],
|
|
1982
|
-
): QtiValue {
|
|
1983
|
-
if (
|
|
1984
|
-
(cardinality === "multiple" || cardinality === "ordered") &&
|
|
1985
|
-
value !== null &&
|
|
1986
|
-
!Array.isArray(value) &&
|
|
1987
|
-
!isRecordValue(value)
|
|
1988
|
-
) {
|
|
1989
|
-
return [value];
|
|
1990
|
-
}
|
|
1991
|
-
return value;
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
function valueContainer(value: QtiValue): QtiScalarValue[] {
|
|
1995
|
-
if (value === null) return [];
|
|
1996
|
-
if (isRecordValue(value)) return [];
|
|
1997
|
-
return Array.isArray(value) ? value.filter((item) => item !== null) : [value];
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
function isRecordValue(value: QtiValue): value is QtiRecordValue {
|
|
2001
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
function isQtiValueRecord(value: unknown): value is Record<string, QtiValue> {
|
|
2005
|
-
if (!isRecord(value)) return false;
|
|
2006
|
-
return Object.values(value).every(isQtiValue);
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
function isPortableCustomState(value: unknown): value is QtiPortableCustomStateValue {
|
|
2010
|
-
return isQtiPortableCustomStateValue(value);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function isPortableCustomStateObject(
|
|
2014
|
-
value: QtiPortableCustomStateValue,
|
|
2015
|
-
): value is { [key: string]: QtiPortableCustomStateValue } {
|
|
2016
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
function isPortableCustomStateRecord(
|
|
2020
|
-
value: unknown,
|
|
2021
|
-
): value is Record<string, QtiPortableCustomStateValue> {
|
|
2022
|
-
if (!isRecord(value)) return false;
|
|
2023
|
-
return Object.values(value).every(isPortableCustomState);
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
function isDiagnosticArray(value: unknown): value is QtiDiagnostic[] {
|
|
2027
|
-
if (!Array.isArray(value)) return false;
|
|
2028
|
-
return value.every((item) => {
|
|
2029
|
-
if (!isRecord(item)) return false;
|
|
2030
|
-
if (typeof item.code !== "string" || item.code.length === 0) return false;
|
|
2031
|
-
if (item.severity !== "info" && item.severity !== "warning" && item.severity !== "error") {
|
|
2032
|
-
return false;
|
|
2033
|
-
}
|
|
2034
|
-
if (typeof item.message !== "string" || item.message.length === 0) return false;
|
|
2035
|
-
return item.path === undefined || typeof item.path === "string";
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2040
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
function indexValue(
|
|
2044
|
-
n: string,
|
|
2045
|
-
outcomes: Record<string, QtiValue>,
|
|
2046
|
-
templateValues: Record<string, QtiValue>,
|
|
2047
|
-
): number | undefined {
|
|
2048
|
-
const parsed = Number(n);
|
|
2049
|
-
if (Number.isInteger(parsed)) return parsed;
|
|
2050
|
-
const value = outcomes[n] ?? templateValues[n] ?? null;
|
|
2051
|
-
const numeric = numericValueOrNull(value);
|
|
2052
|
-
return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
function evaluateNumericOperands(
|
|
2056
|
-
expressions: QtiProcessingExpression[],
|
|
2057
|
-
document: QtiDocument,
|
|
2058
|
-
responses: Record<string, QtiValue>,
|
|
2059
|
-
outcomes: Record<string, QtiValue>,
|
|
2060
|
-
templateValues: Record<string, QtiValue>,
|
|
2061
|
-
correctResponses: Record<string, QtiValue>,
|
|
2062
|
-
random: () => number,
|
|
2063
|
-
customOperators: QtiCustomOperatorRegistry,
|
|
2064
|
-
): number[] | null {
|
|
2065
|
-
const numericValues: number[] = [];
|
|
2066
|
-
for (const expression of expressions) {
|
|
2067
|
-
const value = evaluateValue(
|
|
2068
|
-
expression,
|
|
2069
|
-
document,
|
|
2070
|
-
responses,
|
|
2071
|
-
outcomes,
|
|
2072
|
-
templateValues,
|
|
2073
|
-
correctResponses,
|
|
2074
|
-
random,
|
|
2075
|
-
customOperators,
|
|
2076
|
-
);
|
|
2077
|
-
if (value === null || isRecordValue(value)) return null;
|
|
2078
|
-
const values = Array.isArray(value) ? value : [value];
|
|
2079
|
-
for (const item of values) {
|
|
2080
|
-
const numeric = numericValueOrNull(item);
|
|
2081
|
-
if (numeric === null) return null;
|
|
2082
|
-
numericValues.push(numeric);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
return numericValues;
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
function containsValues(collection: QtiScalarValue[], values: QtiScalarValue[]): boolean {
|
|
2089
|
-
const remaining = [...collection];
|
|
2090
|
-
for (const value of values) {
|
|
2091
|
-
const index = remaining.findIndex((candidate) => valuesEqual(candidate, value));
|
|
2092
|
-
if (index === -1) return false;
|
|
2093
|
-
remaining.splice(index, 1);
|
|
2094
|
-
}
|
|
2095
|
-
return true;
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
function compareScalarValues(left: QtiScalarValue, right: QtiScalarValue): number {
|
|
2099
|
-
return String(left).localeCompare(String(right));
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
function numericValue(value: QtiValue): number {
|
|
2103
|
-
if (typeof value === "number") return value;
|
|
2104
|
-
if (typeof value === "boolean") return value ? 1 : 0;
|
|
2105
|
-
if (typeof value === "string") return Number(value);
|
|
2106
|
-
return 0;
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
function numericValueOrNull(value: QtiValue): number | null {
|
|
2110
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
2111
|
-
if (typeof value === "boolean") return value ? 1 : 0;
|
|
2112
|
-
if (typeof value === "string") {
|
|
2113
|
-
const parsed = Number(value);
|
|
2114
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
2115
|
-
}
|
|
2116
|
-
return null;
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
function durationSeconds(value: QtiValue): number | null {
|
|
2120
|
-
if (value === null || Array.isArray(value) || isRecordValue(value)) return null;
|
|
2121
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
2122
|
-
const raw = String(value).trim();
|
|
2123
|
-
if (raw.length === 0) return null;
|
|
2124
|
-
const numeric = Number(raw);
|
|
2125
|
-
if (Number.isFinite(numeric)) return numeric;
|
|
2126
|
-
const match =
|
|
2127
|
-
/^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i.exec(
|
|
2128
|
-
raw,
|
|
2129
|
-
);
|
|
2130
|
-
if (!match) return null;
|
|
2131
|
-
const [, days, hours, minutes, seconds] = match;
|
|
2132
|
-
if (!days && !hours && !minutes && !seconds) return null;
|
|
2133
|
-
const total =
|
|
2134
|
-
Number(days ?? 0) * 86_400 +
|
|
2135
|
-
Number(hours ?? 0) * 3_600 +
|
|
2136
|
-
Number(minutes ?? 0) * 60 +
|
|
2137
|
-
Number(seconds ?? 0);
|
|
2138
|
-
return Number.isFinite(total) ? total : null;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
function booleanValue(value: QtiValue): boolean {
|
|
2142
|
-
if (typeof value === "boolean") return value;
|
|
2143
|
-
if (typeof value === "number") return value !== 0;
|
|
2144
|
-
if (typeof value === "string") return value.length > 0 && value !== "false";
|
|
2145
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
2146
|
-
return false;
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
function booleanValueOrNull(value: QtiValue): boolean | null {
|
|
2150
|
-
if (value === null) return null;
|
|
2151
|
-
return booleanValue(value);
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
function roundToDecimalPlaces(value: number, figures: number): number {
|
|
2155
|
-
const factor = 10 ** figures;
|
|
2156
|
-
return Math.round(value * factor) / factor;
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
function roundToSignificantFigures(value: number, figures: number): number {
|
|
2160
|
-
if (value === 0 || figures <= 0) return 0;
|
|
2161
|
-
const factor = 10 ** (figures - 1 - Math.floor(Math.log10(Math.abs(value))));
|
|
2162
|
-
return Math.round(value * factor) / factor;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
function roundWithMode(value: number, roundingMode: string, figures: number): number | null {
|
|
2166
|
-
if (!Number.isFinite(value)) return null;
|
|
2167
|
-
if (roundingMode === "decimalPlaces") return roundToDecimalPlaces(value, figures);
|
|
2168
|
-
if (roundingMode === "significantFigures") return roundToSignificantFigures(value, figures);
|
|
2169
|
-
return null;
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
function generalizedGcd(values: number[]): number {
|
|
2173
|
-
let result = 0;
|
|
2174
|
-
for (const value of values) {
|
|
2175
|
-
result = gcd(result, Math.abs(value));
|
|
2176
|
-
}
|
|
2177
|
-
return result;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
function generalizedLcm(values: number[]): number {
|
|
2181
|
-
if (values.some((value) => value === 0)) return 0;
|
|
2182
|
-
return values.reduce((result, value) => lcm(result, Math.abs(value)), 1);
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
function gcd(left: number, right: number): number {
|
|
2186
|
-
let a = Math.abs(left);
|
|
2187
|
-
let b = Math.abs(right);
|
|
2188
|
-
while (b !== 0) {
|
|
2189
|
-
const next = a % b;
|
|
2190
|
-
a = b;
|
|
2191
|
-
b = next;
|
|
2192
|
-
}
|
|
2193
|
-
return a;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
function lcm(left: number, right: number): number {
|
|
2197
|
-
return Math.abs(left * right) / gcd(left, right);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
function mathOperatorValue(name: string, values: number[]): QtiValue {
|
|
2201
|
-
const [first, second] = values;
|
|
2202
|
-
if (first === undefined) return null;
|
|
2203
|
-
switch (name) {
|
|
2204
|
-
case "abs":
|
|
2205
|
-
return finiteOrNull(Math.abs(first));
|
|
2206
|
-
case "acos":
|
|
2207
|
-
return Math.abs(first) > 1 ? null : finiteOrNull(Math.acos(first));
|
|
2208
|
-
case "acot":
|
|
2209
|
-
return finiteOrNull(Math.PI / 2 - Math.atan(1 / first));
|
|
2210
|
-
case "acsc":
|
|
2211
|
-
return Math.abs(first) < 1 ? null : finiteOrNull(Math.PI / 2 - Math.asin(1 / first));
|
|
2212
|
-
case "asec":
|
|
2213
|
-
return Math.abs(first) < 1 ? null : finiteOrNull(Math.PI / 2 - Math.acos(1 / first));
|
|
2214
|
-
case "asin":
|
|
2215
|
-
return Math.abs(first) > 1 ? null : finiteOrNull(Math.asin(first));
|
|
2216
|
-
case "atan":
|
|
2217
|
-
return finiteOrNull(Math.atan(first));
|
|
2218
|
-
case "atan2":
|
|
2219
|
-
return second === undefined ? null : finiteOrNull(Math.atan2(first, second));
|
|
2220
|
-
case "ceil":
|
|
2221
|
-
return finiteOrNull(Math.ceil(first));
|
|
2222
|
-
case "cos":
|
|
2223
|
-
return finiteOrNull(Math.cos(first));
|
|
2224
|
-
case "cosh":
|
|
2225
|
-
return finiteOrNull(Math.cosh(first));
|
|
2226
|
-
case "cot":
|
|
2227
|
-
return finiteOrNull(1 / Math.tan(first));
|
|
2228
|
-
case "coth":
|
|
2229
|
-
return finiteOrNull(1 / Math.tanh(first));
|
|
2230
|
-
case "csc":
|
|
2231
|
-
return finiteOrNull(1 / Math.sin(first));
|
|
2232
|
-
case "csch":
|
|
2233
|
-
return finiteOrNull(1 / Math.sinh(first));
|
|
2234
|
-
case "exp":
|
|
2235
|
-
return finiteOrNull(Math.exp(first));
|
|
2236
|
-
case "floor":
|
|
2237
|
-
return finiteOrNull(Math.floor(first));
|
|
2238
|
-
case "ln":
|
|
2239
|
-
return first < 0 ? null : finiteOrNull(Math.log(first));
|
|
2240
|
-
case "log":
|
|
2241
|
-
return first < 0 ? null : finiteOrNull(Math.log10(first));
|
|
2242
|
-
case "sec":
|
|
2243
|
-
return finiteOrNull(1 / Math.cos(first));
|
|
2244
|
-
case "sech":
|
|
2245
|
-
return finiteOrNull(1 / Math.cosh(first));
|
|
2246
|
-
case "signum":
|
|
2247
|
-
return finiteOrNull(Math.sign(first));
|
|
2248
|
-
case "sin":
|
|
2249
|
-
return finiteOrNull(Math.sin(first));
|
|
2250
|
-
case "sinh":
|
|
2251
|
-
return finiteOrNull(Math.sinh(first));
|
|
2252
|
-
case "tan":
|
|
2253
|
-
return finiteOrNull(Math.tan(first));
|
|
2254
|
-
case "tanh":
|
|
2255
|
-
return finiteOrNull(Math.tanh(first));
|
|
2256
|
-
case "toDegrees":
|
|
2257
|
-
return finiteOrNull((first * 180) / Math.PI);
|
|
2258
|
-
case "toRadians":
|
|
2259
|
-
return finiteOrNull((first * Math.PI) / 180);
|
|
2260
|
-
default:
|
|
2261
|
-
return null;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
function statsOperatorValue(name: string, values: number[]): QtiValue {
|
|
2266
|
-
if (values.length === 0) return 0;
|
|
2267
|
-
const meanValue = mean(values);
|
|
2268
|
-
const squareDiffs = values.map((value) => (value - meanValue) ** 2);
|
|
2269
|
-
switch (name) {
|
|
2270
|
-
case "mean":
|
|
2271
|
-
return meanValue;
|
|
2272
|
-
case "sampleVariance":
|
|
2273
|
-
return meanWithDivisor(squareDiffs, values.length > 1 ? values.length - 1 : 1);
|
|
2274
|
-
case "sampleSD":
|
|
2275
|
-
return Math.sqrt(meanWithDivisor(squareDiffs, values.length > 1 ? values.length - 1 : 1));
|
|
2276
|
-
case "popVariance":
|
|
2277
|
-
return mean(squareDiffs);
|
|
2278
|
-
case "popSD":
|
|
2279
|
-
return Math.sqrt(mean(squareDiffs));
|
|
2280
|
-
default:
|
|
2281
|
-
return null;
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
function mean(values: number[]): number {
|
|
2286
|
-
return meanWithDivisor(values, values.length);
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
function meanWithDivisor(values: number[], divisor: number): number {
|
|
2290
|
-
if (values.length === 0) return 0;
|
|
2291
|
-
return values.reduce((sum, value) => sum + value, 0) / divisor;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
function finiteOrNull(value: number): QtiValue {
|
|
2295
|
-
return Number.isFinite(value) ? value : null;
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
function stringMatch(
|
|
2299
|
-
left: QtiValue,
|
|
2300
|
-
right: QtiValue,
|
|
2301
|
-
caseSensitive: boolean,
|
|
2302
|
-
substring: boolean,
|
|
2303
|
-
): boolean | null {
|
|
2304
|
-
if (left === null || right === null) return null;
|
|
2305
|
-
let actual = qtiValueToString(left);
|
|
2306
|
-
let expected = qtiValueToString(right);
|
|
2307
|
-
if (!caseSensitive) {
|
|
2308
|
-
actual = actual.toLocaleLowerCase();
|
|
2309
|
-
expected = expected.toLocaleLowerCase();
|
|
2310
|
-
}
|
|
2311
|
-
return substring ? actual.includes(expected) : actual === expected;
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
function seededRandom(seed: string | number): () => number {
|
|
2315
|
-
let state = typeof seed === "number" ? seed >>> 0 : hashSeed(seed);
|
|
2316
|
-
return () => {
|
|
2317
|
-
state = (1664525 * state + 1013904223) >>> 0;
|
|
2318
|
-
return state / 0x100000000;
|
|
2319
|
-
};
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
function hashSeed(seed: string): number {
|
|
2323
|
-
let hash = 2166136261;
|
|
2324
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
2325
|
-
hash ^= seed.charCodeAt(index);
|
|
2326
|
-
hash = Math.imul(hash, 16777619);
|
|
2327
|
-
}
|
|
2328
|
-
return hash >>> 0;
|
|
450
|
+
return applyResponseRules(context, resolveConditionalRules(context, condition));
|
|
2329
451
|
}
|