@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runTrustedAdaptiveItemTurn } from "./adaptive-turn.js";
|
|
2
|
+
import type { QtiItemSubmissionScoringDisposition } from "./scoring-disposition-policy.js";
|
|
3
|
+
import {
|
|
4
|
+
buildItemSubmissionMaterializationFailure,
|
|
5
|
+
buildItemSubmissionMaterializationSuccess,
|
|
6
|
+
} from "./trusted-session-materialization.js";
|
|
7
|
+
import {
|
|
8
|
+
parseTrustedItemDocument,
|
|
9
|
+
readPriorAttemptState,
|
|
10
|
+
runTrustedItemSession,
|
|
11
|
+
type QtiTrustedItemDocument,
|
|
12
|
+
type QtiTrustedItemParseOptions,
|
|
13
|
+
type QtiTrustedResponseInput,
|
|
14
|
+
type QtiTrustedResponseApplication,
|
|
15
|
+
} from "./trusted-item-session.js";
|
|
16
|
+
import type { QtiAttemptStateV1, QtiDiagnostic } from "./types.js";
|
|
17
|
+
import type { QtiVariableSnapshot } from "./variable-snapshot.js";
|
|
18
|
+
|
|
19
|
+
export type QtiItemSubmissionResponseInput = QtiTrustedResponseInput;
|
|
20
|
+
export type { QtiItemSubmissionScoringDisposition } from "./scoring-disposition-policy.js";
|
|
21
|
+
export type { QtiVariableSnapshot } from "./variable-snapshot.js";
|
|
22
|
+
|
|
23
|
+
export interface QtiItemSubmissionMaterializationInput
|
|
24
|
+
extends QtiTrustedItemParseOptions, QtiTrustedResponseApplication {
|
|
25
|
+
itemXml: string;
|
|
26
|
+
existingState?: unknown;
|
|
27
|
+
allowIncompleteResponses?: boolean | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface QtiItemSubmissionMaterializationResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
diagnostics: QtiDiagnostic[];
|
|
33
|
+
state?: QtiAttemptStateV1 | undefined;
|
|
34
|
+
responseVariables: QtiVariableSnapshot[];
|
|
35
|
+
outcomeVariables: QtiVariableSnapshot[];
|
|
36
|
+
score: number | null;
|
|
37
|
+
scoringDisposition: QtiItemSubmissionScoringDisposition;
|
|
38
|
+
candidateSafeXml?: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Materialize trusted submitted responses into QTI attempt state and variable snapshots. */
|
|
42
|
+
export function materializeQtiItemSubmission(
|
|
43
|
+
input: QtiItemSubmissionMaterializationInput,
|
|
44
|
+
): QtiItemSubmissionMaterializationResult {
|
|
45
|
+
const parsedResult = parseTrustedItemDocument(
|
|
46
|
+
input.itemXml,
|
|
47
|
+
input.allowedUndeclaredResponseIdentifiers,
|
|
48
|
+
);
|
|
49
|
+
if (!parsedResult.ok) {
|
|
50
|
+
return buildItemSubmissionMaterializationFailure(parsedResult.diagnostics);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const item = parsedResult.parsed.document.item;
|
|
54
|
+
if (item.adaptive) {
|
|
55
|
+
return materializeAdaptiveSubmission(input, parsedResult.parsed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const priorState = readPriorAttemptState(input.existingState, "itemSubmission");
|
|
59
|
+
if (priorState.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
60
|
+
return buildItemSubmissionMaterializationFailure(priorState.diagnostics);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = runTrustedItemSession({
|
|
64
|
+
itemXml: input.itemXml,
|
|
65
|
+
parsedItem: parsedResult.parsed,
|
|
66
|
+
diagnosticPrefix: "itemSubmission",
|
|
67
|
+
allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
|
|
68
|
+
priorState: priorState.state,
|
|
69
|
+
submission: {
|
|
70
|
+
trustedResponses: input.trustedResponses,
|
|
71
|
+
trustedInteractionStates: input.trustedInteractionStates,
|
|
72
|
+
},
|
|
73
|
+
scoring: "always",
|
|
74
|
+
requireNumericScore: false,
|
|
75
|
+
submissionValidation: "strict",
|
|
76
|
+
allowIncompleteResponses: input.allowIncompleteResponses,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
return buildItemSubmissionMaterializationFailure(result.diagnostics, result.state ?? undefined);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return buildItemSubmissionMaterializationSuccess(item, {
|
|
84
|
+
diagnostics: result.diagnostics,
|
|
85
|
+
state: result.state,
|
|
86
|
+
responses: result.responses,
|
|
87
|
+
outcomes: result.outcomes,
|
|
88
|
+
score: result.score,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function materializeAdaptiveSubmission(
|
|
93
|
+
input: QtiItemSubmissionMaterializationInput,
|
|
94
|
+
parsedItem: QtiTrustedItemDocument,
|
|
95
|
+
): QtiItemSubmissionMaterializationResult {
|
|
96
|
+
const result = runTrustedAdaptiveItemTurn({
|
|
97
|
+
itemXml: input.itemXml,
|
|
98
|
+
parsedItem,
|
|
99
|
+
diagnosticPrefix: "itemSubmission",
|
|
100
|
+
allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
|
|
101
|
+
priorState: input.existingState,
|
|
102
|
+
trustedResponses: input.trustedResponses,
|
|
103
|
+
trustedInteractionStates: input.trustedInteractionStates,
|
|
104
|
+
submissionValidation: "strict",
|
|
105
|
+
allowIncompleteResponses: input.allowIncompleteResponses,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!result.ok || !result.state) {
|
|
109
|
+
return buildItemSubmissionMaterializationFailure(result.diagnostics, result.state ?? undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return buildItemSubmissionMaterializationSuccess(parsedItem.document.item, {
|
|
113
|
+
diagnostics: result.diagnostics,
|
|
114
|
+
state: result.state,
|
|
115
|
+
responses: result.responses,
|
|
116
|
+
outcomes: result.outcomes,
|
|
117
|
+
score: result.score,
|
|
118
|
+
candidateSafeXml: result.candidateSafeXml,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -212,7 +212,7 @@ function parseCustomMarkupNode(
|
|
|
212
212
|
code: options.nestedInteractionCode,
|
|
213
213
|
severity: "error",
|
|
214
214
|
message: `${options.contextName} must not contain nested QTI interaction ${node.localName}.`,
|
|
215
|
-
path: node.source
|
|
215
|
+
path: node.source.path,
|
|
216
216
|
source: node.source,
|
|
217
217
|
});
|
|
218
218
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
coerceValue,
|
|
3
3
|
normalizeValueForCardinality,
|
|
4
|
+
parseBaseType,
|
|
4
5
|
parseCardinality,
|
|
5
6
|
parseCoords,
|
|
6
7
|
parseShape,
|
|
@@ -18,7 +19,7 @@ import { childElements, textContent, type XmlNode } from "./xml.js";
|
|
|
18
19
|
|
|
19
20
|
export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration {
|
|
20
21
|
const cardinality = parseCardinality(node.attributes.cardinality);
|
|
21
|
-
const baseType = node.attributes["base-type"]
|
|
22
|
+
const baseType = parseBaseType(node.attributes["base-type"]);
|
|
22
23
|
return {
|
|
23
24
|
kind: "response",
|
|
24
25
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -37,7 +38,7 @@ export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
|
|
40
|
-
const baseType = node.attributes["base-type"]
|
|
41
|
+
const baseType = parseBaseType(node.attributes["base-type"]);
|
|
41
42
|
return {
|
|
42
43
|
kind: "outcome",
|
|
43
44
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -51,7 +52,7 @@ export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration {
|
|
54
|
-
const baseType = node.attributes["base-type"]
|
|
55
|
+
const baseType = parseBaseType(node.attributes["base-type"]);
|
|
55
56
|
return {
|
|
56
57
|
kind: "template",
|
|
57
58
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -28,13 +28,14 @@ export function firstChildElement(
|
|
|
28
28
|
duplicateCode: string,
|
|
29
29
|
): XmlNode | undefined {
|
|
30
30
|
const matches = childElements(parent, childName);
|
|
31
|
-
|
|
31
|
+
const duplicate = matches[1];
|
|
32
|
+
if (duplicate) {
|
|
32
33
|
diagnostics.push({
|
|
33
34
|
code: duplicateCode,
|
|
34
35
|
severity: "error",
|
|
35
36
|
message: `${parent.localName} allows at most one ${childName} child.`,
|
|
36
|
-
path:
|
|
37
|
-
source:
|
|
37
|
+
path: duplicate.source.path,
|
|
38
|
+
source: duplicate.source,
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
return matches[0];
|
|
@@ -177,7 +178,7 @@ export function parseCompanionMaterialsInfo(
|
|
|
177
178
|
code: "companionMaterials.child.unsupported",
|
|
178
179
|
severity: "warning",
|
|
179
180
|
message: `${child.localName} is not currently modeled in companion materials parsing.`,
|
|
180
|
-
path: child.source
|
|
181
|
+
path: child.source.path,
|
|
181
182
|
source: child.source,
|
|
182
183
|
});
|
|
183
184
|
}
|
|
@@ -200,7 +201,7 @@ function parsePhysicalMaterial(
|
|
|
200
201
|
diagnostics,
|
|
201
202
|
"companionMaterials.physicalMaterial.empty",
|
|
202
203
|
"qti-physical-material requires non-empty text content.",
|
|
203
|
-
node.source
|
|
204
|
+
node.source.path,
|
|
204
205
|
node.source,
|
|
205
206
|
);
|
|
206
207
|
}
|
|
@@ -225,7 +226,7 @@ function parseDigitalMaterial(
|
|
|
225
226
|
diagnostics,
|
|
226
227
|
"companionMaterials.digitalMaterial.fileHref.missing",
|
|
227
228
|
"qti-digital-material requires a qti-file-href child.",
|
|
228
|
-
node.source
|
|
229
|
+
node.source.path,
|
|
229
230
|
node.source,
|
|
230
231
|
);
|
|
231
232
|
}
|
|
@@ -236,7 +237,7 @@ function parseDigitalMaterial(
|
|
|
236
237
|
diagnostics,
|
|
237
238
|
"companionMaterials.digitalMaterial.fileHref.empty",
|
|
238
239
|
"qti-digital-material qti-file-href requires non-empty text content.",
|
|
239
|
-
fileHrefNode.source
|
|
240
|
+
fileHrefNode.source.path,
|
|
240
241
|
fileHrefNode.source,
|
|
241
242
|
);
|
|
242
243
|
}
|
package/src/parser-processing.ts
CHANGED
|
@@ -672,8 +672,8 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
|
|
|
672
672
|
if (variable && correct) {
|
|
673
673
|
return {
|
|
674
674
|
type: "matchCorrect",
|
|
675
|
-
identifier: variable
|
|
676
|
-
correctIdentifier: correct
|
|
675
|
+
identifier: variable.attributes.identifier ?? "",
|
|
676
|
+
correctIdentifier: correct.attributes.identifier ?? "",
|
|
677
677
|
source: node.source,
|
|
678
678
|
};
|
|
679
679
|
}
|
package/src/parser-values.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
QtiBaseType,
|
|
2
3
|
QtiCardinality,
|
|
3
4
|
QtiRecordValue,
|
|
4
5
|
QtiResponseDeclaration,
|
|
@@ -6,6 +7,41 @@ import type {
|
|
|
6
7
|
QtiValue,
|
|
7
8
|
} from "./types.js";
|
|
8
9
|
|
|
10
|
+
const QTI_BASE_TYPES = new Set<string>([
|
|
11
|
+
"identifier",
|
|
12
|
+
"boolean",
|
|
13
|
+
"integer",
|
|
14
|
+
"float",
|
|
15
|
+
"string",
|
|
16
|
+
"point",
|
|
17
|
+
"pair",
|
|
18
|
+
"directedPair",
|
|
19
|
+
"duration",
|
|
20
|
+
"file",
|
|
21
|
+
"uri",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function isQtiBaseType(value: string): value is QtiBaseType {
|
|
25
|
+
return QTI_BASE_TYPES.has(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseBaseType(value: string | undefined): QtiBaseType | undefined {
|
|
29
|
+
if (value === undefined) return undefined;
|
|
30
|
+
return isQtiBaseType(value) ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function numericTuple3(values: number[]): [number, number, number] | undefined {
|
|
34
|
+
const [a, b, c] = values;
|
|
35
|
+
if (a === undefined || b === undefined || c === undefined) return undefined;
|
|
36
|
+
return [a, b, c];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function numericTuple4(values: number[]): [number, number, number, number] | undefined {
|
|
40
|
+
const [a, b, c, d] = values;
|
|
41
|
+
if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
|
|
42
|
+
return [a, b, c, d];
|
|
43
|
+
}
|
|
44
|
+
|
|
9
45
|
export function coerceValue(value: string, baseType: string | undefined): QtiScalarValue {
|
|
10
46
|
if (baseType === "integer") return Number.parseInt(value, 10);
|
|
11
47
|
if (baseType === "float") return Number.parseFloat(value);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiValue } from "./types.js";
|
|
2
|
+
import { assertNever } from "./assert-never.js";
|
|
3
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import { booleanValueOrNull } from "./processing-values.js";
|
|
5
|
+
|
|
6
|
+
type BooleanExpression = Extract<QtiProcessingExpression, { type: "and" | "anyN" | "or" | "not" }>;
|
|
7
|
+
|
|
8
|
+
export function evaluateBooleanExpression(
|
|
9
|
+
expression: BooleanExpression,
|
|
10
|
+
context: EvaluationContext,
|
|
11
|
+
): QtiValue {
|
|
12
|
+
switch (expression.type) {
|
|
13
|
+
case "and": {
|
|
14
|
+
let sawNull = false;
|
|
15
|
+
for (const item of expression.expressions) {
|
|
16
|
+
const value = booleanValueOrNull(context.evaluate(item));
|
|
17
|
+
if (value === false) return false;
|
|
18
|
+
if (value === null) sawNull = true;
|
|
19
|
+
}
|
|
20
|
+
return sawNull ? null : true;
|
|
21
|
+
}
|
|
22
|
+
case "anyN": {
|
|
23
|
+
const min = context.indexValue(expression.min) ?? 0;
|
|
24
|
+
const max = context.indexValue(expression.max) ?? 0;
|
|
25
|
+
const values = expression.expressions.map((item) => context.evaluate(item));
|
|
26
|
+
const trueCount = values.filter((value) => value === true).length;
|
|
27
|
+
const nullCount = values.filter((value) => value === null).length;
|
|
28
|
+
if (min > max || trueCount > max || trueCount + nullCount < min) return false;
|
|
29
|
+
if (trueCount >= min && trueCount <= max) return true;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
case "or": {
|
|
33
|
+
let sawNull = false;
|
|
34
|
+
for (const item of expression.expressions) {
|
|
35
|
+
const value = booleanValueOrNull(context.evaluate(item));
|
|
36
|
+
if (value === true) return true;
|
|
37
|
+
if (value === null) sawNull = true;
|
|
38
|
+
}
|
|
39
|
+
return sawNull ? null : false;
|
|
40
|
+
}
|
|
41
|
+
case "not": {
|
|
42
|
+
const value = booleanValueOrNull(context.evaluate(expression.expression));
|
|
43
|
+
return value === null ? null : !value;
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return assertNever(expression);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiScalarValue, QtiValue } from "./types.js";
|
|
2
|
+
import { assertNever } from "./assert-never.js";
|
|
3
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import { valueContainer } from "./processing-values.js";
|
|
5
|
+
|
|
6
|
+
type CollectionExpression = Extract<
|
|
7
|
+
QtiProcessingExpression,
|
|
8
|
+
{ type: "multiple" | "ordered" | "index" | "containerSize" }
|
|
9
|
+
>;
|
|
10
|
+
type RepeatExpression = Extract<QtiProcessingExpression, { type: "repeat" }>;
|
|
11
|
+
|
|
12
|
+
export function evaluateCollectionExpression(
|
|
13
|
+
expression: CollectionExpression,
|
|
14
|
+
context: EvaluationContext,
|
|
15
|
+
): QtiValue {
|
|
16
|
+
switch (expression.type) {
|
|
17
|
+
case "multiple":
|
|
18
|
+
case "ordered": {
|
|
19
|
+
const values = expression.expressions.flatMap((item) =>
|
|
20
|
+
valueContainer(context.evaluate(item)),
|
|
21
|
+
);
|
|
22
|
+
return values.length > 0 ? values : null;
|
|
23
|
+
}
|
|
24
|
+
case "index": {
|
|
25
|
+
const values = valueContainer(context.evaluate(expression.expression));
|
|
26
|
+
const n = context.indexValue(expression.n);
|
|
27
|
+
if (n === undefined || n < 1 || n > values.length) return null;
|
|
28
|
+
return values[n - 1] ?? null;
|
|
29
|
+
}
|
|
30
|
+
case "containerSize":
|
|
31
|
+
return valueContainer(context.evaluate(expression.expression)).length;
|
|
32
|
+
default:
|
|
33
|
+
return assertNever(expression);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function evaluateRepeatExpression(
|
|
38
|
+
expression: RepeatExpression,
|
|
39
|
+
context: EvaluationContext,
|
|
40
|
+
): QtiValue {
|
|
41
|
+
const repeats = context.indexValue(expression.numberRepeats);
|
|
42
|
+
if (repeats === undefined || repeats < 1) return null;
|
|
43
|
+
const container: QtiScalarValue[] = [];
|
|
44
|
+
for (let repeat = 0; repeat < repeats; repeat += 1) {
|
|
45
|
+
for (const item of expression.expressions) {
|
|
46
|
+
container.push(...valueContainer(context.evaluate(item)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return container.length > 0 ? container : null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiValue } from "./types.js";
|
|
2
|
+
import { assertNever } from "./assert-never.js";
|
|
3
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import {
|
|
5
|
+
containsValues,
|
|
6
|
+
durationSeconds,
|
|
7
|
+
numericValueOrNull,
|
|
8
|
+
qtiMatchValues,
|
|
9
|
+
valuesEqual,
|
|
10
|
+
valueContainer,
|
|
11
|
+
} from "./processing-values.js";
|
|
12
|
+
import { expressionIsOrdered } from "./processing-variables.js";
|
|
13
|
+
import { roundWithMode } from "./processing-operators.js";
|
|
14
|
+
|
|
15
|
+
type ComparisonExpression = Extract<
|
|
16
|
+
QtiProcessingExpression,
|
|
17
|
+
{
|
|
18
|
+
type:
|
|
19
|
+
| "match"
|
|
20
|
+
| "equal"
|
|
21
|
+
| "equalRounded"
|
|
22
|
+
| "numericCompare"
|
|
23
|
+
| "durationCompare"
|
|
24
|
+
| "member"
|
|
25
|
+
| "delete"
|
|
26
|
+
| "contains";
|
|
27
|
+
}
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export function evaluateComparisonExpression(
|
|
31
|
+
expression: ComparisonExpression,
|
|
32
|
+
context: EvaluationContext,
|
|
33
|
+
): QtiValue {
|
|
34
|
+
switch (expression.type) {
|
|
35
|
+
case "match": {
|
|
36
|
+
const left = context.evaluate(expression.left);
|
|
37
|
+
const right = context.evaluate(expression.right);
|
|
38
|
+
return qtiMatchValues(
|
|
39
|
+
left,
|
|
40
|
+
right,
|
|
41
|
+
expressionIsOrdered(expression.left, context.document) ||
|
|
42
|
+
expressionIsOrdered(expression.right, context.document),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
case "equal": {
|
|
46
|
+
const left = context.evaluate(expression.left);
|
|
47
|
+
const right = context.evaluate(expression.right);
|
|
48
|
+
return left === null || right === null ? null : valuesEqual(left, right);
|
|
49
|
+
}
|
|
50
|
+
case "equalRounded": {
|
|
51
|
+
const left = context.evaluate(expression.left);
|
|
52
|
+
const right = context.evaluate(expression.right);
|
|
53
|
+
if (left === null || right === null) return null;
|
|
54
|
+
const leftNumber = numericValueOrNull(left);
|
|
55
|
+
const rightNumber = numericValueOrNull(right);
|
|
56
|
+
if (leftNumber === null || rightNumber === null) return null;
|
|
57
|
+
const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
|
|
58
|
+
const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
|
|
59
|
+
return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
|
|
60
|
+
}
|
|
61
|
+
case "numericCompare": {
|
|
62
|
+
const leftValue = context.evaluate(expression.left);
|
|
63
|
+
const rightValue = context.evaluate(expression.right);
|
|
64
|
+
if (leftValue === null || rightValue === null) return null;
|
|
65
|
+
const left = numericValueOrNull(leftValue);
|
|
66
|
+
const right = numericValueOrNull(rightValue);
|
|
67
|
+
if (left === null || right === null) return null;
|
|
68
|
+
if (expression.operator === "lt") return left < right;
|
|
69
|
+
if (expression.operator === "lte") return left <= right;
|
|
70
|
+
if (expression.operator === "gt") return left > right;
|
|
71
|
+
return left >= right;
|
|
72
|
+
}
|
|
73
|
+
case "durationCompare": {
|
|
74
|
+
const left = durationSeconds(context.evaluate(expression.left));
|
|
75
|
+
const right = durationSeconds(context.evaluate(expression.right));
|
|
76
|
+
if (left === null || right === null) return null;
|
|
77
|
+
return expression.operator === "lt" ? left < right : left >= right;
|
|
78
|
+
}
|
|
79
|
+
case "member": {
|
|
80
|
+
const value = context.evaluate(expression.value);
|
|
81
|
+
const collection = context.evaluate(expression.collection);
|
|
82
|
+
if (value === null || collection === null) return null;
|
|
83
|
+
return valueContainer(collection).some((item) => valuesEqual(item, value));
|
|
84
|
+
}
|
|
85
|
+
case "delete": {
|
|
86
|
+
const value = context.evaluate(expression.value);
|
|
87
|
+
const collectionValue = context.evaluate(expression.collection);
|
|
88
|
+
if (value === null || collectionValue === null) return null;
|
|
89
|
+
const collection = valueContainer(collectionValue);
|
|
90
|
+
if (collection.length === 0) return null;
|
|
91
|
+
const filtered = collection.filter((item) => !valuesEqual(item, value));
|
|
92
|
+
return filtered.length > 0 ? filtered : null;
|
|
93
|
+
}
|
|
94
|
+
case "contains": {
|
|
95
|
+
const collectionValue = context.evaluate(expression.collection);
|
|
96
|
+
const valuesValue = context.evaluate(expression.values);
|
|
97
|
+
if (collectionValue === null || valuesValue === null) return null;
|
|
98
|
+
const collection = valueContainer(collectionValue);
|
|
99
|
+
const values = valueContainer(valuesValue);
|
|
100
|
+
if (collection.length === 0 || values.length === 0) return null;
|
|
101
|
+
return containsValues(collection, values);
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
return assertNever(expression);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiValue } from "./types.js";
|
|
2
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
3
|
+
|
|
4
|
+
type CustomOperatorExpression = Extract<QtiProcessingExpression, { type: "customOperator" }>;
|
|
5
|
+
|
|
6
|
+
export function evaluateCustomOperatorExpression(
|
|
7
|
+
expression: CustomOperatorExpression,
|
|
8
|
+
context: EvaluationContext,
|
|
9
|
+
): QtiValue {
|
|
10
|
+
const operatorKey = expression.definition ?? expression.className ?? "";
|
|
11
|
+
const handler = context.customOperators[operatorKey];
|
|
12
|
+
if (!handler) return null;
|
|
13
|
+
return handler({
|
|
14
|
+
definition: expression.definition,
|
|
15
|
+
className: expression.className,
|
|
16
|
+
attributes: expression.attributes,
|
|
17
|
+
values: expression.expressions.map((item) => context.evaluate(item)),
|
|
18
|
+
expression,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiValue } from "./types.js";
|
|
2
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
3
|
+
import { parsePoint, pointInsideArea } from "./processing-mapping.js";
|
|
4
|
+
import { valueContainer } from "./processing-values.js";
|
|
5
|
+
|
|
6
|
+
type GeometryExpression = Extract<QtiProcessingExpression, { type: "inside" }>;
|
|
7
|
+
|
|
8
|
+
export function evaluateGeometryExpression(
|
|
9
|
+
expression: GeometryExpression,
|
|
10
|
+
context: EvaluationContext,
|
|
11
|
+
): QtiValue {
|
|
12
|
+
const value = context.evaluate(expression.expression);
|
|
13
|
+
if (value === null) return null;
|
|
14
|
+
if (expression.shape === "default") return true;
|
|
15
|
+
return valueContainer(value).some((pointValue) => {
|
|
16
|
+
const point = parsePoint(String(pointValue));
|
|
17
|
+
return point
|
|
18
|
+
? pointInsideArea(point, {
|
|
19
|
+
shape: expression.shape,
|
|
20
|
+
coords: expression.coords,
|
|
21
|
+
mappedValue: 0,
|
|
22
|
+
attributes: {},
|
|
23
|
+
})
|
|
24
|
+
: false;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiValue } from "./types.js";
|
|
2
|
+
import { assertNever } from "./assert-never.js";
|
|
3
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import {
|
|
5
|
+
generalizedGcd,
|
|
6
|
+
generalizedLcm,
|
|
7
|
+
mathOperatorValue,
|
|
8
|
+
roundToDecimalPlaces,
|
|
9
|
+
roundToSignificantFigures,
|
|
10
|
+
statsOperatorValue,
|
|
11
|
+
} from "./processing-operators.js";
|
|
12
|
+
import {
|
|
13
|
+
isRecordValue,
|
|
14
|
+
numericValue,
|
|
15
|
+
numericValueOrNull,
|
|
16
|
+
valueContainer,
|
|
17
|
+
} from "./processing-values.js";
|
|
18
|
+
|
|
19
|
+
type NumericExpression = Extract<
|
|
20
|
+
QtiProcessingExpression,
|
|
21
|
+
{
|
|
22
|
+
type:
|
|
23
|
+
| "sum"
|
|
24
|
+
| "product"
|
|
25
|
+
| "min"
|
|
26
|
+
| "max"
|
|
27
|
+
| "subtract"
|
|
28
|
+
| "divide"
|
|
29
|
+
| "power"
|
|
30
|
+
| "integerDivide"
|
|
31
|
+
| "integerModulus"
|
|
32
|
+
| "round"
|
|
33
|
+
| "roundTo"
|
|
34
|
+
| "truncate"
|
|
35
|
+
| "integerToFloat"
|
|
36
|
+
| "gcd"
|
|
37
|
+
| "lcm"
|
|
38
|
+
| "mathConstant"
|
|
39
|
+
| "mathOperator"
|
|
40
|
+
| "statsOperator";
|
|
41
|
+
}
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
export function evaluateNumericExpression(
|
|
45
|
+
expression: NumericExpression,
|
|
46
|
+
context: EvaluationContext,
|
|
47
|
+
): QtiValue {
|
|
48
|
+
switch (expression.type) {
|
|
49
|
+
case "sum": {
|
|
50
|
+
const values = context.numericOperands(expression.expressions);
|
|
51
|
+
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
52
|
+
}
|
|
53
|
+
case "product": {
|
|
54
|
+
const values = context.numericOperands(expression.expressions);
|
|
55
|
+
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
56
|
+
}
|
|
57
|
+
case "min":
|
|
58
|
+
case "max": {
|
|
59
|
+
const values = context.numericOperands(expression.expressions);
|
|
60
|
+
if (!values || values.length === 0) return null;
|
|
61
|
+
return expression.type === "min" ? Math.min(...values) : Math.max(...values);
|
|
62
|
+
}
|
|
63
|
+
case "subtract": {
|
|
64
|
+
const values = context.numericOperands([expression.left, expression.right]);
|
|
65
|
+
return values && values.length === 2 ? values[0]! - values[1]! : null;
|
|
66
|
+
}
|
|
67
|
+
case "divide":
|
|
68
|
+
return divide(context.evaluate(expression.left), context.evaluate(expression.right));
|
|
69
|
+
case "power": {
|
|
70
|
+
const values = context.numericOperands([expression.left, expression.right]);
|
|
71
|
+
if (!values || values.length !== 2) return null;
|
|
72
|
+
const value = Math.pow(values[0]!, values[1]!);
|
|
73
|
+
return Number.isFinite(value) ? value : null;
|
|
74
|
+
}
|
|
75
|
+
case "integerDivide": {
|
|
76
|
+
const values = integerOperands(
|
|
77
|
+
context.evaluate(expression.left),
|
|
78
|
+
context.evaluate(expression.right),
|
|
79
|
+
);
|
|
80
|
+
return values ? Math.floor(values.dividend / values.divisor) : null;
|
|
81
|
+
}
|
|
82
|
+
case "integerModulus": {
|
|
83
|
+
const values = integerOperands(
|
|
84
|
+
context.evaluate(expression.left),
|
|
85
|
+
context.evaluate(expression.right),
|
|
86
|
+
);
|
|
87
|
+
return values
|
|
88
|
+
? values.dividend - Math.floor(values.dividend / values.divisor) * values.divisor
|
|
89
|
+
: null;
|
|
90
|
+
}
|
|
91
|
+
case "round": {
|
|
92
|
+
const value = numericValueOrNull(context.evaluate(expression.expression));
|
|
93
|
+
return value === null ? null : Math.round(value);
|
|
94
|
+
}
|
|
95
|
+
case "roundTo": {
|
|
96
|
+
const value = numericValueOrNull(context.evaluate(expression.expression));
|
|
97
|
+
if (value === null) return null;
|
|
98
|
+
return expression.roundingMode === "decimalPlaces"
|
|
99
|
+
? roundToDecimalPlaces(value, expression.figures)
|
|
100
|
+
: roundToSignificantFigures(value, expression.figures);
|
|
101
|
+
}
|
|
102
|
+
case "truncate": {
|
|
103
|
+
const value = numericValueOrNull(context.evaluate(expression.expression));
|
|
104
|
+
return value === null ? null : Math.trunc(value);
|
|
105
|
+
}
|
|
106
|
+
case "integerToFloat":
|
|
107
|
+
return numericValueOrNull(context.evaluate(expression.expression));
|
|
108
|
+
case "gcd":
|
|
109
|
+
case "lcm": {
|
|
110
|
+
const values = expression.expressions.flatMap((item) => {
|
|
111
|
+
const value = context.evaluate(item);
|
|
112
|
+
return value === null ? [null] : valueContainer(value);
|
|
113
|
+
});
|
|
114
|
+
if (values.length === 0 || values.some((value) => value === null || isRecordValue(value))) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const integers = values.map((value) => Math.trunc(numericValue(value)));
|
|
118
|
+
return expression.type === "gcd" ? generalizedGcd(integers) : generalizedLcm(integers);
|
|
119
|
+
}
|
|
120
|
+
case "mathConstant":
|
|
121
|
+
if (expression.name === "pi") return Math.PI;
|
|
122
|
+
if (expression.name === "e") return Math.E;
|
|
123
|
+
return null;
|
|
124
|
+
case "mathOperator": {
|
|
125
|
+
const values = expression.expressions.map((item) => context.evaluate(item));
|
|
126
|
+
if (values.length === 0 || values.some((value) => value === null)) return null;
|
|
127
|
+
return mathOperatorValue(expression.name, values.map(numericValue));
|
|
128
|
+
}
|
|
129
|
+
case "statsOperator": {
|
|
130
|
+
const value = context.evaluate(expression.expression);
|
|
131
|
+
if (value === null) return null;
|
|
132
|
+
return statsOperatorValue(expression.name, valueContainer(value).map(numericValue));
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
return assertNever(expression);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function divide(dividendValue: QtiValue, divisorValue: QtiValue): QtiValue {
|
|
140
|
+
const values = integerOperands(dividendValue, divisorValue);
|
|
141
|
+
if (!values) return null;
|
|
142
|
+
const quotient = values.dividend / values.divisor;
|
|
143
|
+
return Number.isFinite(quotient) ? quotient : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function integerOperands(
|
|
147
|
+
dividendValue: QtiValue,
|
|
148
|
+
divisorValue: QtiValue,
|
|
149
|
+
): { dividend: number; divisor: number } | undefined {
|
|
150
|
+
if (dividendValue === null || divisorValue === null) return undefined;
|
|
151
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
152
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
153
|
+
if (dividend === null || divisor === null || divisor === 0) return undefined;
|
|
154
|
+
return { dividend, divisor };
|
|
155
|
+
}
|