@longsightgroup/qti3-core 0.10.2 → 0.10.3
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 +21 -2
- package/dist/attempt-state.d.ts.map +1 -1
- package/dist/attempt-state.js +18 -0
- package/dist/attempt-state.js.map +1 -1
- package/dist/content-text.d.ts +4 -0
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +112 -9
- package/dist/content-text.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/media-definition.d.ts +55 -0
- package/dist/media-definition.d.ts.map +1 -0
- package/dist/media-definition.js +97 -0
- package/dist/media-definition.js.map +1 -0
- package/dist/parser-declarations.d.ts +4 -4
- package/dist/parser-declarations.d.ts.map +1 -1
- package/dist/parser-declarations.js +87 -46
- package/dist/parser-declarations.js.map +1 -1
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +4 -3
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/parser-processing.js +10 -9
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts +2 -0
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +7 -0
- package/dist/parser-values.js.map +1 -1
- package/dist/parser.js +16 -13
- package/dist/parser.js.map +1 -1
- package/dist/processing-evaluator-collection.d.ts.map +1 -1
- package/dist/processing-evaluator-collection.js +8 -2
- package/dist/processing-evaluator-collection.js.map +1 -1
- package/dist/processing-evaluator-comparison.d.ts.map +1 -1
- package/dist/processing-evaluator-comparison.js +6 -0
- package/dist/processing-evaluator-comparison.js.map +1 -1
- package/dist/processing-evaluator-numeric.d.ts.map +1 -1
- package/dist/processing-evaluator-numeric.js +8 -1
- package/dist/processing-evaluator-numeric.js.map +1 -1
- package/dist/processing-expression-children.d.ts.map +1 -1
- package/dist/processing-expression-children.js +2 -1
- package/dist/processing-expression-children.js.map +1 -1
- package/dist/processing-limits.d.ts +3 -0
- package/dist/processing-limits.d.ts.map +1 -0
- package/dist/processing-limits.js +3 -0
- package/dist/processing-limits.js.map +1 -0
- package/dist/processing-mapping.d.ts.map +1 -1
- package/dist/processing-mapping.js +13 -2
- package/dist/processing-mapping.js.map +1 -1
- package/dist/processing-operators.d.ts +2 -0
- package/dist/processing-operators.d.ts.map +1 -1
- package/dist/processing-operators.js +34 -5
- package/dist/processing-operators.js.map +1 -1
- package/dist/response-validation-policy.d.ts.map +1 -1
- package/dist/response-validation-policy.js +5 -6
- package/dist/response-validation-policy.js.map +1 -1
- package/dist/serializer-processing-expressions.js +1 -1
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/slider-definition.d.ts +59 -0
- package/dist/slider-definition.d.ts.map +1 -0
- package/dist/slider-definition.js +198 -0
- package/dist/slider-definition.js.map +1 -0
- package/dist/support-evidence.d.ts +6 -2
- package/dist/support-evidence.d.ts.map +1 -1
- package/dist/support-evidence.js +15 -11
- package/dist/support-evidence.js.map +1 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +96 -100
- package/dist/support.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-declaration-entries.d.ts +13 -0
- package/dist/validation-declaration-entries.d.ts.map +1 -0
- package/dist/validation-declaration-entries.js +136 -0
- package/dist/validation-declaration-entries.js.map +1 -0
- package/dist/validation-interactions.d.ts.map +1 -1
- package/dist/validation-interactions.js +14 -46
- package/dist/validation-interactions.js.map +1 -1
- package/dist/validation-primitives.js +2 -2
- package/dist/validation-primitives.js.map +1 -1
- package/dist/validation-processing.d.ts.map +1 -1
- package/dist/validation-processing.js +125 -1
- package/dist/validation-processing.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +8 -134
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +3 -2
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
- package/src/attempt-state.ts +20 -0
- package/src/content-text.ts +135 -11
- package/src/index.ts +25 -0
- package/src/media-definition.ts +174 -0
- package/src/parser-declarations.ts +134 -40
- package/src/parser-item-metadata.ts +4 -3
- package/src/parser-processing.ts +10 -9
- package/src/parser-values.ts +7 -0
- package/src/parser.ts +16 -13
- package/src/processing-evaluator-collection.ts +7 -2
- package/src/processing-evaluator-comparison.ts +6 -0
- package/src/processing-evaluator-numeric.ts +5 -1
- package/src/processing-expression-children.ts +2 -1
- package/src/processing-limits.ts +2 -0
- package/src/processing-mapping.ts +10 -2
- package/src/processing-operators.ts +38 -5
- package/src/response-validation-policy.ts +5 -5
- package/src/serializer-processing-expressions.ts +1 -1
- package/src/slider-definition.ts +363 -0
- package/src/support-evidence.ts +15 -17
- package/src/support.ts +106 -102
- package/src/types.ts +8 -1
- package/src/validation-declaration-entries.ts +162 -0
- package/src/validation-interactions.ts +14 -64
- package/src/validation-primitives.ts +2 -2
- package/src/validation-processing.ts +145 -1
- package/src/validation.ts +11 -151
- package/src/xml.ts +3 -4
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type ParsedCompanionMaterialChildQtiName,
|
|
20
20
|
pushCompanionMaterialParseWarning,
|
|
21
21
|
} from "./companion-materials.js";
|
|
22
|
+
import { visibleTextContent } from "./content-text.js";
|
|
22
23
|
import { parseXmlBoolean } from "./parser-values.js";
|
|
23
24
|
import { childElements, descendants, textContent, type XmlNode } from "./xml.js";
|
|
24
25
|
|
|
@@ -75,7 +76,7 @@ export function parseModalFeedback(node: XmlNode): QtiModalFeedback {
|
|
|
75
76
|
identifier: node.attributes.identifier ?? "",
|
|
76
77
|
outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
|
|
77
78
|
showHide,
|
|
78
|
-
text:
|
|
79
|
+
text: visibleTextContent(node),
|
|
79
80
|
source: node.source,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -119,7 +120,7 @@ function parseCatalogCardEntry(node: XmlNode): QtiCatalogCardEntry {
|
|
|
119
120
|
function parseCatalogHtmlContent(node: XmlNode | undefined): QtiCatalogHtmlContent | undefined {
|
|
120
121
|
if (!node) return undefined;
|
|
121
122
|
return {
|
|
122
|
-
text:
|
|
123
|
+
text: visibleTextContent(node),
|
|
123
124
|
children: parseCatalogHtmlChildren(node),
|
|
124
125
|
attributes: node.attributes,
|
|
125
126
|
source: node.source,
|
|
@@ -202,7 +203,7 @@ function parsePhysicalMaterial(
|
|
|
202
203
|
node: XmlNode,
|
|
203
204
|
diagnostics: QtiDiagnostic[],
|
|
204
205
|
): QtiPhysicalMaterial | undefined {
|
|
205
|
-
const text =
|
|
206
|
+
const text = visibleTextContent(node);
|
|
206
207
|
if (text.length === 0) {
|
|
207
208
|
return pushCompanionMaterialParseWarning(
|
|
208
209
|
diagnostics,
|
package/src/parser-processing.ts
CHANGED
|
@@ -646,15 +646,16 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
|
|
|
646
646
|
}
|
|
647
647
|
|
|
648
648
|
if (node.localName === "qti-stats-operator") {
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
649
|
+
const expressions = childElements(node)
|
|
650
|
+
.map(parseExpression)
|
|
651
|
+
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
652
|
+
return {
|
|
653
|
+
type: "statsOperator",
|
|
654
|
+
name: node.attributes.name ?? "",
|
|
655
|
+
expression: expressions[0] ?? { type: "null", source: node.source },
|
|
656
|
+
expressions,
|
|
657
|
+
source: node.source,
|
|
658
|
+
};
|
|
658
659
|
}
|
|
659
660
|
|
|
660
661
|
if (node.localName === "qti-custom-operator") {
|
package/src/parser-values.ts
CHANGED
|
@@ -66,6 +66,13 @@ export function parseXmlBoolean(value: string | undefined): boolean | undefined
|
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Parse a non-empty finite numeric XML attribute value. */
|
|
70
|
+
export function parseFiniteNumber(value: string | undefined): number | undefined {
|
|
71
|
+
if (value === undefined || value.trim().length === 0) return undefined;
|
|
72
|
+
const parsed = Number(value);
|
|
73
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export function normalizeValueForCardinality(
|
|
70
77
|
value: QtiValue,
|
|
71
78
|
cardinality: QtiCardinality,
|
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendContentTextNode, flatTextFromContent } from "./content-text.js";
|
|
1
|
+
import { appendContentTextNode, flatTextFromContent, visibleTextContent } from "./content-text.js";
|
|
2
2
|
import {
|
|
3
3
|
parseOutcomeDeclaration,
|
|
4
4
|
parseResponseDeclaration,
|
|
@@ -36,7 +36,7 @@ import type {
|
|
|
36
36
|
QtiResponseDeclaration,
|
|
37
37
|
} from "./types.js";
|
|
38
38
|
import { validateAssessmentItem } from "./validation.js";
|
|
39
|
-
import { childElements, descendants, parseXmlTree,
|
|
39
|
+
import { childElements, descendants, parseXmlTree, type XmlNode } from "./xml.js";
|
|
40
40
|
|
|
41
41
|
const qtiAssessmentItemNamespace = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0";
|
|
42
42
|
const supportedProcessingNames = new Set(processingSupport.map((entry) => entry.qtiName));
|
|
@@ -109,16 +109,16 @@ export function parseQtiXml(xml: string): QtiParseResult {
|
|
|
109
109
|
function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAssessmentItem {
|
|
110
110
|
const identifier = node.attributes.identifier ?? "";
|
|
111
111
|
const responseDeclarations = childElements(node, "qti-response-declaration").map(
|
|
112
|
-
parseResponseDeclaration,
|
|
112
|
+
(declarationNode) => parseResponseDeclaration(declarationNode, diagnostics),
|
|
113
113
|
);
|
|
114
114
|
const responseDeclarationMap = new Map(
|
|
115
115
|
responseDeclarations.map((declaration) => [declaration.identifier, declaration]),
|
|
116
116
|
);
|
|
117
117
|
const outcomeDeclarations = childElements(node, "qti-outcome-declaration").map(
|
|
118
|
-
parseOutcomeDeclaration,
|
|
118
|
+
(declarationNode) => parseOutcomeDeclaration(declarationNode, diagnostics),
|
|
119
119
|
);
|
|
120
120
|
const templateDeclarations = childElements(node, "qti-template-declaration").map(
|
|
121
|
-
parseTemplateDeclaration,
|
|
121
|
+
(declarationNode) => parseTemplateDeclaration(declarationNode, diagnostics),
|
|
122
122
|
);
|
|
123
123
|
const templateProcessing = parseTemplateProcessing(
|
|
124
124
|
childElements(node, "qti-template-processing")[0],
|
|
@@ -166,7 +166,7 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
|
|
|
166
166
|
adaptive: parseXmlBoolean(node.attributes.adaptive) === true,
|
|
167
167
|
timeDependent: parseXmlBoolean(node.attributes["time-dependent"]),
|
|
168
168
|
attributes: node.attributes,
|
|
169
|
-
prompt: prompt ?
|
|
169
|
+
prompt: prompt ? visibleTextContent(prompt) : undefined,
|
|
170
170
|
itemBodyAttributes: itemBody?.attributes,
|
|
171
171
|
itemBodySource: itemBody?.source,
|
|
172
172
|
responseDeclarations,
|
|
@@ -181,7 +181,10 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
|
|
|
181
181
|
catalogReferences,
|
|
182
182
|
stylesheets,
|
|
183
183
|
body,
|
|
184
|
-
bodyText:
|
|
184
|
+
bodyText: flatTextFromContent(body, {
|
|
185
|
+
excludeAnnotations: true,
|
|
186
|
+
interactionText: (interactionIndex) => interactions[interactionIndex]?.prompt ?? "",
|
|
187
|
+
}),
|
|
185
188
|
source: node.source,
|
|
186
189
|
};
|
|
187
190
|
}
|
|
@@ -334,7 +337,7 @@ function parseInteraction(
|
|
|
334
337
|
prompt: promptContent
|
|
335
338
|
? flatTextFromContent(promptContent, { excludeAnnotations: true }) || undefined
|
|
336
339
|
: prompt
|
|
337
|
-
?
|
|
340
|
+
? visibleTextContent(prompt)
|
|
338
341
|
: undefined,
|
|
339
342
|
promptContent: promptContent && promptContent.length > 0 ? promptContent : undefined,
|
|
340
343
|
promptAttributes: prompt?.attributes,
|
|
@@ -357,7 +360,7 @@ function parseInteraction(
|
|
|
357
360
|
source: child.source,
|
|
358
361
|
})),
|
|
359
362
|
attributes: node.attributes,
|
|
360
|
-
text:
|
|
363
|
+
text: visibleTextContent(node),
|
|
361
364
|
source: node.source,
|
|
362
365
|
};
|
|
363
366
|
}
|
|
@@ -417,7 +420,7 @@ function parseHottextSegments(node: XmlNode): QtiInteraction["hottextSegments"]
|
|
|
417
420
|
segments.push({
|
|
418
421
|
kind: "hottext",
|
|
419
422
|
identifier: entry.attributes.identifier ?? "",
|
|
420
|
-
text:
|
|
423
|
+
text: visibleTextContent(entry),
|
|
421
424
|
attributes: entry.attributes,
|
|
422
425
|
source: entry.source,
|
|
423
426
|
});
|
|
@@ -475,7 +478,7 @@ function inlineInteractionContext(
|
|
|
475
478
|
if (interactionType !== "inlineChoice" && interactionType !== "textEntry") return undefined;
|
|
476
479
|
const parent = node.parent;
|
|
477
480
|
if (!parent) return undefined;
|
|
478
|
-
return normalizeInlineContext(parent.text) ?? normalizeInlineContext(
|
|
481
|
+
return normalizeInlineContext(parent.text) ?? normalizeInlineContext(visibleTextContent(parent));
|
|
479
482
|
}
|
|
480
483
|
|
|
481
484
|
function normalizeInlineContext(value: string): string | undefined {
|
|
@@ -505,7 +508,7 @@ function parseObjectAsset(node: XmlNode | undefined): QtiObjectAsset | undefined
|
|
|
505
508
|
node.attributes.height ?? pictureImage?.attributes.height ?? inferredSvgDimensions?.height,
|
|
506
509
|
sources,
|
|
507
510
|
tracks,
|
|
508
|
-
text:
|
|
511
|
+
text: visibleTextContent(node) || node.attributes.alt || pictureImage?.attributes.alt || "",
|
|
509
512
|
attributes: node.attributes,
|
|
510
513
|
source: node.source,
|
|
511
514
|
};
|
|
@@ -670,7 +673,7 @@ function parseChoices(
|
|
|
670
673
|
const flatChoiceText =
|
|
671
674
|
content.length > 0
|
|
672
675
|
? flatTextFromContent(content, { excludeAnnotations: true })
|
|
673
|
-
:
|
|
676
|
+
: visibleTextContent(choice);
|
|
674
677
|
return {
|
|
675
678
|
identifier,
|
|
676
679
|
text:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { QtiProcessingExpression, QtiScalarValue, QtiValue } from "./types.js";
|
|
2
2
|
import { assertNever } from "./assert-never.js";
|
|
3
3
|
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import { MAX_QTI_REPEAT_RESULT_ELEMENTS } from "./processing-limits.js";
|
|
4
5
|
import { valueContainer } from "./processing-values.js";
|
|
5
6
|
|
|
6
7
|
type CollectionExpression = Extract<
|
|
@@ -39,11 +40,15 @@ export function evaluateRepeatExpression(
|
|
|
39
40
|
context: EvaluationContext,
|
|
40
41
|
): QtiValue {
|
|
41
42
|
const repeats = context.indexValue(expression.numberRepeats);
|
|
42
|
-
if (repeats === undefined || repeats < 1)
|
|
43
|
+
if (repeats === undefined || repeats < 1 || repeats > MAX_QTI_REPEAT_RESULT_ELEMENTS) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
43
46
|
const container: QtiScalarValue[] = [];
|
|
44
47
|
for (let repeat = 0; repeat < repeats; repeat += 1) {
|
|
45
48
|
for (const item of expression.expressions) {
|
|
46
|
-
|
|
49
|
+
const values = valueContainer(context.evaluate(item));
|
|
50
|
+
if (values.length > MAX_QTI_REPEAT_RESULT_ELEMENTS - container.length) return null;
|
|
51
|
+
for (const value of values) container.push(value);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
return container.length > 0 ? container : null;
|
|
@@ -48,6 +48,12 @@ export function evaluateComparisonExpression(
|
|
|
48
48
|
return left === null || right === null ? null : valuesEqual(left, right);
|
|
49
49
|
}
|
|
50
50
|
case "equalRounded": {
|
|
51
|
+
const validRounding =
|
|
52
|
+
Number.isInteger(expression.figures) &&
|
|
53
|
+
(expression.roundingMode === "decimalPlaces"
|
|
54
|
+
? expression.figures >= 0
|
|
55
|
+
: expression.roundingMode === "significantFigures" && expression.figures > 0);
|
|
56
|
+
if (!validRounding) return null;
|
|
51
57
|
const left = context.evaluate(expression.left);
|
|
52
58
|
const right = context.evaluate(expression.right);
|
|
53
59
|
if (left === null || right === null) return null;
|
|
@@ -47,10 +47,12 @@ export function evaluateNumericExpression(
|
|
|
47
47
|
): QtiValue {
|
|
48
48
|
switch (expression.type) {
|
|
49
49
|
case "sum": {
|
|
50
|
+
if (expression.expressions.length === 0) return null;
|
|
50
51
|
const values = context.numericOperands(expression.expressions);
|
|
51
52
|
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
52
53
|
}
|
|
53
54
|
case "product": {
|
|
55
|
+
if (expression.expressions.length === 0) return null;
|
|
54
56
|
const values = context.numericOperands(expression.expressions);
|
|
55
57
|
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
56
58
|
}
|
|
@@ -127,7 +129,9 @@ export function evaluateNumericExpression(
|
|
|
127
129
|
return mathOperatorValue(expression.name, values.map(numericValue));
|
|
128
130
|
}
|
|
129
131
|
case "statsOperator": {
|
|
130
|
-
const
|
|
132
|
+
const expressions = expression.expressions ?? [expression.expression];
|
|
133
|
+
if (expressions.length !== 1) return null;
|
|
134
|
+
const value = context.evaluate(expressions[0]!);
|
|
131
135
|
if (value === null) return null;
|
|
132
136
|
return statsOperatorValue(expression.name, valueContainer(value).map(numericValue));
|
|
133
137
|
}
|
|
@@ -43,8 +43,9 @@ export function expressionChildren(expression: QtiProcessingExpression): QtiProc
|
|
|
43
43
|
case "patternMatch":
|
|
44
44
|
case "fieldValue":
|
|
45
45
|
case "inside":
|
|
46
|
-
case "statsOperator":
|
|
47
46
|
return [expression.expression];
|
|
47
|
+
case "statsOperator":
|
|
48
|
+
return expression.expressions ?? [expression.expression];
|
|
48
49
|
case "member":
|
|
49
50
|
case "delete":
|
|
50
51
|
return [expression.value, expression.collection];
|
|
@@ -49,6 +49,7 @@ export function scoreAreaMapping(
|
|
|
49
49
|
: response === null
|
|
50
50
|
? []
|
|
51
51
|
: qtiValueToStringList(response);
|
|
52
|
+
const matchedAreaIndexes = new Set<number>();
|
|
52
53
|
let score = 0;
|
|
53
54
|
for (const point of points) {
|
|
54
55
|
const parsed = parsePoint(point);
|
|
@@ -56,8 +57,15 @@ export function scoreAreaMapping(
|
|
|
56
57
|
score += areaMapping.defaultValue;
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
let matchedArea = false;
|
|
61
|
+
for (const [index, entry] of areaMapping.entries.entries()) {
|
|
62
|
+
if (!pointInsideArea(parsed, entry)) continue;
|
|
63
|
+
matchedArea = true;
|
|
64
|
+
if (matchedAreaIndexes.has(index)) continue;
|
|
65
|
+
matchedAreaIndexes.add(index);
|
|
66
|
+
score += entry.mappedValue;
|
|
67
|
+
}
|
|
68
|
+
if (!matchedArea) score += areaMapping.defaultValue;
|
|
61
69
|
}
|
|
62
70
|
return clampMappedScore(score, areaMapping.attributes);
|
|
63
71
|
}
|
|
@@ -39,15 +39,45 @@ export const STATS_OPERATOR_NAMES = [
|
|
|
39
39
|
"popSD",
|
|
40
40
|
] as const;
|
|
41
41
|
|
|
42
|
+
/** Round decimal ties away from zero without an intermediate scaled binary float. */
|
|
42
43
|
export function roundToDecimalPlaces(value: number, figures: number): number {
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if (!Number.isFinite(value) || value === 0) return value;
|
|
45
|
+
const decimal = decimalDigits(value);
|
|
46
|
+
const retainedDigits = decimal.exponent + figures + 1;
|
|
47
|
+
return roundDecimalDigits(value, decimal, retainedDigits);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** Round significant decimal digits, with half-up ties on the absolute magnitude. */
|
|
47
51
|
export function roundToSignificantFigures(value: number, figures: number): number {
|
|
48
52
|
if (value === 0 || figures <= 0) return 0;
|
|
49
|
-
|
|
50
|
-
return
|
|
53
|
+
if (!Number.isFinite(value)) return Number.NaN;
|
|
54
|
+
return roundDecimalDigits(value, decimalDigits(value), figures);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DecimalDigits {
|
|
58
|
+
digits: string;
|
|
59
|
+
exponent: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decimalDigits(value: number): DecimalDigits {
|
|
63
|
+
const [coefficient = "", exponent = "0"] = Math.abs(value).toExponential().split("e");
|
|
64
|
+
return {
|
|
65
|
+
digits: coefficient.replace(".", ""),
|
|
66
|
+
exponent: Number(exponent),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function roundDecimalDigits(value: number, decimal: DecimalDigits, retainedDigits: number): number {
|
|
71
|
+
if (retainedDigits >= decimal.digits.length) return value;
|
|
72
|
+
if (retainedDigits < 0) return value < 0 ? -0 : 0;
|
|
73
|
+
|
|
74
|
+
const roundingDigit = decimal.digits[retainedDigits] ?? "0";
|
|
75
|
+
const retained = retainedDigits === 0 ? 0n : BigInt(decimal.digits.slice(0, retainedDigits));
|
|
76
|
+
const rounded = roundingDigit >= "5" ? retained + 1n : retained;
|
|
77
|
+
if (rounded === 0n) return value < 0 ? -0 : 0;
|
|
78
|
+
|
|
79
|
+
const unitExponent = decimal.exponent - retainedDigits + 1;
|
|
80
|
+
return Number(`${value < 0 ? "-" : ""}${rounded}e${unitExponent}`);
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
export function roundWithMode(value: number, roundingMode: string, figures: number): number | null {
|
|
@@ -151,7 +181,10 @@ export function mathOperatorValue(name: string, values: number[]): QtiValue {
|
|
|
151
181
|
}
|
|
152
182
|
|
|
153
183
|
export function statsOperatorValue(name: string, values: number[]): QtiValue {
|
|
154
|
-
|
|
184
|
+
// QTI 3.0.1 defines the input as a numeric container but does not define empty-container
|
|
185
|
+
// results, so this implementation conservatively returns NULL.
|
|
186
|
+
// https://www.imsglobal.org/sites/default/files/spec/qti/v3/info/index.html#OpStatsOp
|
|
187
|
+
if (values.length === 0) return null;
|
|
155
188
|
const meanValue = mean(values);
|
|
156
189
|
const squareDiffs = values.map((value) => (value - meanValue) ** 2);
|
|
157
190
|
switch (name) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QtiChoice, QtiDiagnostic, QtiInteraction, QtiValue } from "./types.js";
|
|
2
|
+
import { parseQtiMediaDefinition } from "./media-definition.js";
|
|
2
3
|
import { parseXmlBoolean } from "./parser-values.js";
|
|
3
4
|
import { parseNonNegativeInteger } from "./validation-primitives.js";
|
|
4
5
|
import { qtiValueToIdentifierList } from "./value-format.js";
|
|
@@ -75,14 +76,13 @@ export function mediaPlayCount(value: QtiValue): number {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function minimumMediaPlays(interaction: QtiInteraction): number {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
return interactionRequiresResponse(interaction) ? 1 : 0;
|
|
79
|
+
const definition = parseQtiMediaDefinition(interaction);
|
|
80
|
+
return definition.ok ? definition.value.minPlays : 0;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function maximumMediaPlays(interaction: QtiInteraction): number | undefined {
|
|
84
|
-
const
|
|
85
|
-
return
|
|
84
|
+
const definition = parseQtiMediaDefinition(interaction);
|
|
85
|
+
return definition.ok ? definition.value.maxPlays : undefined;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export { maximumMediaPlays, minimumMediaPlays };
|