@longsightgroup/qti3-core 0.9.2 → 0.9.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.
Files changed (219) hide show
  1. package/README.md +20 -3
  2. package/dist/assert-never.d.ts +3 -0
  3. package/dist/assert-never.d.ts.map +1 -0
  4. package/dist/assert-never.js +5 -0
  5. package/dist/assert-never.js.map +1 -0
  6. package/dist/attempt-state-constants.d.ts +6 -0
  7. package/dist/attempt-state-constants.d.ts.map +1 -0
  8. package/dist/attempt-state-constants.js +6 -0
  9. package/dist/attempt-state-constants.js.map +1 -0
  10. package/dist/attempt-state.d.ts +5 -0
  11. package/dist/attempt-state.d.ts.map +1 -0
  12. package/dist/attempt-state.js +190 -0
  13. package/dist/attempt-state.js.map +1 -0
  14. package/dist/catalog.js +1 -1
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/content-text.d.ts.map +1 -1
  17. package/dist/content-text.js +3 -0
  18. package/dist/content-text.js.map +1 -1
  19. package/dist/custom-operators.d.ts +13 -0
  20. package/dist/custom-operators.d.ts.map +1 -0
  21. package/dist/custom-operators.js +2 -0
  22. package/dist/custom-operators.js.map +1 -0
  23. package/dist/delivery-redaction.js +1 -1
  24. package/dist/delivery-redaction.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/parser-custom-interactions.js +1 -1
  30. package/dist/parser-custom-interactions.js.map +1 -1
  31. package/dist/parser-declarations.d.ts.map +1 -1
  32. package/dist/parser-declarations.js +4 -4
  33. package/dist/parser-declarations.js.map +1 -1
  34. package/dist/parser-item-metadata.d.ts.map +1 -1
  35. package/dist/parser-item-metadata.js +8 -7
  36. package/dist/parser-item-metadata.js.map +1 -1
  37. package/dist/parser-processing.js +2 -2
  38. package/dist/parser-processing.js.map +1 -1
  39. package/dist/parser-values.d.ts +5 -1
  40. package/dist/parser-values.d.ts.map +1 -1
  41. package/dist/parser-values.js +33 -0
  42. package/dist/parser-values.js.map +1 -1
  43. package/dist/processing-evaluator-boolean.d.ts +8 -0
  44. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  45. package/dist/processing-evaluator-boolean.js +47 -0
  46. package/dist/processing-evaluator-boolean.js.map +1 -0
  47. package/dist/processing-evaluator-collection.d.ts +12 -0
  48. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  49. package/dist/processing-evaluator-collection.js +35 -0
  50. package/dist/processing-evaluator-collection.js.map +1 -0
  51. package/dist/processing-evaluator-comparison.d.ts +8 -0
  52. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  53. package/dist/processing-evaluator-comparison.js +88 -0
  54. package/dist/processing-evaluator-comparison.js.map +1 -0
  55. package/dist/processing-evaluator-custom.d.ts +8 -0
  56. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  57. package/dist/processing-evaluator-custom.js +14 -0
  58. package/dist/processing-evaluator-custom.js.map +1 -0
  59. package/dist/processing-evaluator-geometry.d.ts +8 -0
  60. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  61. package/dist/processing-evaluator-geometry.js +21 -0
  62. package/dist/processing-evaluator-geometry.js.map +1 -0
  63. package/dist/processing-evaluator-numeric.d.ts +8 -0
  64. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  65. package/dist/processing-evaluator-numeric.js +112 -0
  66. package/dist/processing-evaluator-numeric.js.map +1 -0
  67. package/dist/processing-evaluator-string.d.ts +8 -0
  68. package/dist/processing-evaluator-string.d.ts.map +1 -0
  69. package/dist/processing-evaluator-string.js +33 -0
  70. package/dist/processing-evaluator-string.js.map +1 -0
  71. package/dist/processing-evaluator-variable.d.ts +8 -0
  72. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  73. package/dist/processing-evaluator-variable.js +39 -0
  74. package/dist/processing-evaluator-variable.js.map +1 -0
  75. package/dist/processing-evaluator.d.ts +22 -0
  76. package/dist/processing-evaluator.d.ts.map +1 -0
  77. package/dist/processing-evaluator.js +137 -0
  78. package/dist/processing-evaluator.js.map +1 -0
  79. package/dist/processing-expression-children.d.ts +3 -0
  80. package/dist/processing-expression-children.d.ts.map +1 -0
  81. package/dist/processing-expression-children.js +68 -0
  82. package/dist/processing-expression-children.js.map +1 -0
  83. package/dist/processing-mapping.d.ts +13 -0
  84. package/dist/processing-mapping.d.ts.map +1 -0
  85. package/dist/processing-mapping.js +120 -0
  86. package/dist/processing-mapping.js.map +1 -0
  87. package/dist/processing-operators.d.ts +11 -0
  88. package/dist/processing-operators.d.ts.map +1 -0
  89. package/dist/processing-operators.js +178 -0
  90. package/dist/processing-operators.js.map +1 -0
  91. package/dist/processing-random.d.ts +2 -0
  92. package/dist/processing-random.d.ts.map +1 -0
  93. package/dist/processing-random.js +16 -0
  94. package/dist/processing-random.js.map +1 -0
  95. package/dist/processing-state.d.ts +8 -0
  96. package/dist/processing-state.d.ts.map +1 -0
  97. package/dist/processing-state.js +44 -0
  98. package/dist/processing-state.js.map +1 -0
  99. package/dist/processing-templates.d.ts +3 -0
  100. package/dist/processing-templates.d.ts.map +1 -0
  101. package/dist/processing-templates.js +12 -0
  102. package/dist/processing-templates.js.map +1 -0
  103. package/dist/processing-values.d.ts +15 -0
  104. package/dist/processing-values.d.ts.map +1 -0
  105. package/dist/processing-values.js +150 -0
  106. package/dist/processing-values.js.map +1 -0
  107. package/dist/processing-variables.d.ts +7 -0
  108. package/dist/processing-variables.d.ts.map +1 -0
  109. package/dist/processing-variables.js +40 -0
  110. package/dist/processing-variables.js.map +1 -0
  111. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  112. package/dist/serializer-processing-expressions.js +2 -8
  113. package/dist/serializer-processing-expressions.js.map +1 -1
  114. package/dist/serializer-processing-xml.js +2 -2
  115. package/dist/serializer-processing-xml.js.map +1 -1
  116. package/dist/serializer-processing.d.ts.map +1 -1
  117. package/dist/serializer-processing.js +3 -0
  118. package/dist/serializer-processing.js.map +1 -1
  119. package/dist/session.d.ts +5 -14
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +76 -1158
  122. package/dist/session.js.map +1 -1
  123. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  124. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  125. package/dist/shared-vocabulary-generated-families.js +1 -1
  126. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  127. package/dist/shared-vocabulary-support.js +3 -3
  128. package/dist/shared-vocabulary-support.js.map +1 -1
  129. package/dist/shared-vocabulary.d.ts.map +1 -1
  130. package/dist/shared-vocabulary.js +4 -3
  131. package/dist/shared-vocabulary.js.map +1 -1
  132. package/dist/support-evidence.d.ts +22 -0
  133. package/dist/support-evidence.d.ts.map +1 -0
  134. package/dist/support-evidence.js +93 -0
  135. package/dist/support-evidence.js.map +1 -0
  136. package/dist/support.d.ts.map +1 -1
  137. package/dist/support.js +30 -43
  138. package/dist/support.js.map +1 -1
  139. package/dist/trusted-item-session.d.ts.map +1 -1
  140. package/dist/trusted-item-session.js +6 -5
  141. package/dist/trusted-item-session.js.map +1 -1
  142. package/dist/tts.d.ts.map +1 -1
  143. package/dist/tts.js +16 -2
  144. package/dist/tts.js.map +1 -1
  145. package/dist/validation-geometry.d.ts +8 -0
  146. package/dist/validation-geometry.d.ts.map +1 -0
  147. package/dist/validation-geometry.js +187 -0
  148. package/dist/validation-geometry.js.map +1 -0
  149. package/dist/validation-interactions.d.ts +3 -0
  150. package/dist/validation-interactions.d.ts.map +1 -0
  151. package/dist/validation-interactions.js +644 -0
  152. package/dist/validation-interactions.js.map +1 -0
  153. package/dist/validation-primitives.d.ts +11 -0
  154. package/dist/validation-primitives.d.ts.map +1 -0
  155. package/dist/validation-primitives.js +48 -0
  156. package/dist/validation-primitives.js.map +1 -0
  157. package/dist/validation-processing.d.ts +4 -0
  158. package/dist/validation-processing.d.ts.map +1 -0
  159. package/dist/validation-processing.js +569 -0
  160. package/dist/validation-processing.js.map +1 -0
  161. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  162. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  163. package/dist/validation-shared-vocabulary-layout.js +102 -0
  164. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  165. package/dist/validation.d.ts.map +1 -1
  166. package/dist/validation.js +27 -1641
  167. package/dist/validation.js.map +1 -1
  168. package/dist/xml.js +1 -1
  169. package/dist/xml.js.map +1 -1
  170. package/package.json +1 -1
  171. package/src/assert-never.ts +4 -0
  172. package/src/attempt-state-constants.ts +5 -0
  173. package/src/attempt-state.ts +245 -0
  174. package/src/catalog.ts +1 -1
  175. package/src/content-text.ts +3 -0
  176. package/src/custom-operators.ts +12 -0
  177. package/src/delivery-redaction.ts +1 -1
  178. package/src/index.ts +2 -0
  179. package/src/parser-custom-interactions.ts +1 -1
  180. package/src/parser-declarations.ts +4 -3
  181. package/src/parser-item-metadata.ts +8 -7
  182. package/src/parser-processing.ts +2 -2
  183. package/src/parser-values.ts +36 -0
  184. package/src/processing-evaluator-boolean.ts +48 -0
  185. package/src/processing-evaluator-collection.ts +50 -0
  186. package/src/processing-evaluator-comparison.ts +106 -0
  187. package/src/processing-evaluator-custom.ts +20 -0
  188. package/src/processing-evaluator-geometry.ts +26 -0
  189. package/src/processing-evaluator-numeric.ts +155 -0
  190. package/src/processing-evaluator-string.ts +60 -0
  191. package/src/processing-evaluator-variable.ts +81 -0
  192. package/src/processing-evaluator.ts +173 -0
  193. package/src/processing-expression-children.ts +69 -0
  194. package/src/processing-mapping.ts +153 -0
  195. package/src/processing-operators.ts +184 -0
  196. package/src/processing-random.ts +16 -0
  197. package/src/processing-state.ts +69 -0
  198. package/src/processing-templates.ts +12 -0
  199. package/src/processing-values.ts +162 -0
  200. package/src/processing-variables.ts +83 -0
  201. package/src/serializer-processing-expressions.ts +2 -12
  202. package/src/serializer-processing-xml.ts +2 -2
  203. package/src/serializer-processing.ts +3 -0
  204. package/src/session.ts +128 -2006
  205. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  206. package/src/shared-vocabulary-generated-families.ts +1 -1
  207. package/src/shared-vocabulary-support.ts +3 -3
  208. package/src/shared-vocabulary.ts +3 -2
  209. package/src/support-evidence.ts +107 -0
  210. package/src/support.ts +59 -50
  211. package/src/trusted-item-session.ts +6 -4
  212. package/src/tts.ts +29 -15
  213. package/src/validation-geometry.ts +204 -0
  214. package/src/validation-interactions.ts +831 -0
  215. package/src/validation-primitives.ts +63 -0
  216. package/src/validation-processing.ts +785 -0
  217. package/src/validation-shared-vocabulary-layout.ts +117 -0
  218. package/src/validation.ts +44 -2077
  219. package/src/xml.ts +1 -1
@@ -0,0 +1,81 @@
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 { mapOrMatchResponse, scoreAreaMapping } from "./processing-mapping.js";
5
+ import { isNullResponse, qtiMatchValues } from "./processing-values.js";
6
+ import {
7
+ defaultValueForIdentifier,
8
+ getResponseDeclaration,
9
+ resolveVariableValue,
10
+ } from "./processing-variables.js";
11
+
12
+ type VariableExpression = Extract<
13
+ QtiProcessingExpression,
14
+ {
15
+ type:
16
+ | "isNull"
17
+ | "matchCorrect"
18
+ | "mapResponse"
19
+ | "mapResponsePoint"
20
+ | "correct"
21
+ | "default"
22
+ | "variable";
23
+ }
24
+ >;
25
+
26
+ export function evaluateVariableExpression(
27
+ expression: VariableExpression,
28
+ context: EvaluationContext,
29
+ ): QtiValue {
30
+ switch (expression.type) {
31
+ case "isNull":
32
+ return isNullResponse(context.responses[expression.identifier] ?? null);
33
+ case "matchCorrect": {
34
+ const declaration = getResponseDeclaration(context.document, expression.correctIdentifier);
35
+ return declaration
36
+ ? qtiMatchValues(
37
+ context.responses[expression.identifier] ?? null,
38
+ context.correctResponses[expression.correctIdentifier] ?? null,
39
+ declaration.cardinality === "ordered",
40
+ )
41
+ : false;
42
+ }
43
+ case "mapResponse": {
44
+ const declaration = getResponseDeclaration(context.document, expression.identifier);
45
+ return declaration
46
+ ? mapOrMatchResponse(
47
+ declaration,
48
+ context.responses[expression.identifier] ?? null,
49
+ context.correctResponses[expression.identifier] ?? null,
50
+ )
51
+ : 0;
52
+ }
53
+ case "mapResponsePoint": {
54
+ const declaration = getResponseDeclaration(context.document, expression.identifier);
55
+ return declaration?.areaMapping
56
+ ? scoreAreaMapping(
57
+ context.responses[expression.identifier] ?? null,
58
+ declaration.areaMapping,
59
+ )
60
+ : 0;
61
+ }
62
+ case "correct":
63
+ return context.correctResponses[expression.identifier] ?? null;
64
+ case "default":
65
+ return defaultValueForIdentifier(context.document, expression.identifier);
66
+ case "variable":
67
+ return (
68
+ resolveVariableValue(
69
+ context.document,
70
+ expression.identifier,
71
+ context.responses,
72
+ context.outcomes,
73
+ context.templateValues,
74
+ ) ??
75
+ context.undeclaredResponseValue(expression.identifier) ??
76
+ null
77
+ );
78
+ default:
79
+ return assertNever(expression);
80
+ }
81
+ }
@@ -0,0 +1,173 @@
1
+ import type { QtiDocument, QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import type { QtiCustomOperatorRegistry } from "./custom-operators.js";
4
+ import { evaluateBooleanExpression } from "./processing-evaluator-boolean.js";
5
+ import {
6
+ evaluateCollectionExpression,
7
+ evaluateRepeatExpression,
8
+ } from "./processing-evaluator-collection.js";
9
+ import { evaluateComparisonExpression } from "./processing-evaluator-comparison.js";
10
+ import { evaluateCustomOperatorExpression } from "./processing-evaluator-custom.js";
11
+ import { evaluateGeometryExpression } from "./processing-evaluator-geometry.js";
12
+ import { evaluateNumericExpression } from "./processing-evaluator-numeric.js";
13
+ import { evaluateStringExpression } from "./processing-evaluator-string.js";
14
+ import { evaluateVariableExpression } from "./processing-evaluator-variable.js";
15
+ import { isRecordValue, numericValueOrNull } from "./processing-values.js";
16
+
17
+ export interface EvaluationContext {
18
+ document: QtiDocument;
19
+ responses: Record<string, QtiValue>;
20
+ outcomes: Record<string, QtiValue>;
21
+ templateValues: Record<string, QtiValue>;
22
+ correctResponses: Record<string, QtiValue>;
23
+ allowedUndeclaredResponseIdentifiers: ReadonlySet<string>;
24
+ random: () => number;
25
+ customOperators: QtiCustomOperatorRegistry;
26
+ evaluate(expression: QtiProcessingExpression): QtiValue;
27
+ indexValue(identifierOrInteger: string): number | undefined;
28
+ numericOperands(expressions: QtiProcessingExpression[]): number[] | null;
29
+ undeclaredResponseValue(identifier: string): QtiValue | undefined;
30
+ }
31
+
32
+ export interface EvaluationOptions {
33
+ allowedUndeclaredResponseIdentifiers?: ReadonlySet<string> | readonly string[] | undefined;
34
+ }
35
+
36
+ export function createEvaluationContext(
37
+ document: QtiDocument,
38
+ responses: Record<string, QtiValue>,
39
+ outcomes: Record<string, QtiValue>,
40
+ templateValues: Record<string, QtiValue>,
41
+ correctResponses: Record<string, QtiValue>,
42
+ random: () => number,
43
+ customOperators: QtiCustomOperatorRegistry,
44
+ options: EvaluationOptions = {},
45
+ ): EvaluationContext {
46
+ const allowedUndeclaredResponseIdentifiers =
47
+ options.allowedUndeclaredResponseIdentifiers instanceof Set
48
+ ? options.allowedUndeclaredResponseIdentifiers
49
+ : new Set(options.allowedUndeclaredResponseIdentifiers ?? []);
50
+ const context: EvaluationContext = {
51
+ document,
52
+ responses,
53
+ outcomes,
54
+ templateValues,
55
+ correctResponses,
56
+ allowedUndeclaredResponseIdentifiers,
57
+ random,
58
+ customOperators,
59
+ evaluate(expression) {
60
+ return evaluateProcessingExpression(expression, context);
61
+ },
62
+ indexValue(identifierOrInteger) {
63
+ const parsed = Number(identifierOrInteger);
64
+ if (Number.isInteger(parsed)) return parsed;
65
+ const value = outcomes[identifierOrInteger] ?? templateValues[identifierOrInteger] ?? null;
66
+ const numeric = numericValueOrNull(value);
67
+ return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
68
+ },
69
+ numericOperands(expressions) {
70
+ const numericValues: number[] = [];
71
+ for (const expression of expressions) {
72
+ const value = context.evaluate(expression);
73
+ if (value === null || isRecordValue(value)) return null;
74
+ const values = Array.isArray(value) ? value : [value];
75
+ for (const item of values) {
76
+ const numeric = numericValueOrNull(item);
77
+ if (numeric === null) return null;
78
+ numericValues.push(numeric);
79
+ }
80
+ }
81
+ return numericValues;
82
+ },
83
+ undeclaredResponseValue(identifier) {
84
+ return allowedUndeclaredResponseIdentifiers.has(identifier) && identifier in responses
85
+ ? (responses[identifier] ?? null)
86
+ : undefined;
87
+ },
88
+ };
89
+ return context;
90
+ }
91
+
92
+ export function evaluateProcessingExpression(
93
+ expression: QtiProcessingExpression,
94
+ context: EvaluationContext,
95
+ ): QtiValue {
96
+ switch (expression.type) {
97
+ case "baseValue":
98
+ return expression.value;
99
+ case "null":
100
+ return null;
101
+ case "randomInteger": {
102
+ const step = expression.step > 0 ? expression.step : 1;
103
+ const count = Math.floor((expression.max - expression.min) / step) + 1;
104
+ return expression.min + Math.floor(context.random() * count) * step;
105
+ }
106
+ case "randomFloat":
107
+ return expression.min + context.random() * (expression.max - expression.min);
108
+ case "random":
109
+ if (expression.values.length === 0) return null;
110
+ return context.evaluate(
111
+ expression.values[Math.floor(context.random() * expression.values.length)]!,
112
+ );
113
+ case "isNull":
114
+ case "matchCorrect":
115
+ case "mapResponse":
116
+ case "mapResponsePoint":
117
+ case "correct":
118
+ case "default":
119
+ case "variable":
120
+ return evaluateVariableExpression(expression, context);
121
+ case "multiple":
122
+ case "ordered":
123
+ case "index":
124
+ case "containerSize":
125
+ return evaluateCollectionExpression(expression, context);
126
+ case "sum":
127
+ case "product":
128
+ case "min":
129
+ case "max":
130
+ case "subtract":
131
+ case "divide":
132
+ case "power":
133
+ case "integerDivide":
134
+ case "integerModulus":
135
+ case "round":
136
+ case "roundTo":
137
+ case "truncate":
138
+ case "integerToFloat":
139
+ case "gcd":
140
+ case "lcm":
141
+ case "mathConstant":
142
+ case "mathOperator":
143
+ case "statsOperator":
144
+ return evaluateNumericExpression(expression, context);
145
+ case "and":
146
+ case "anyN":
147
+ case "or":
148
+ case "not":
149
+ return evaluateBooleanExpression(expression, context);
150
+ case "match":
151
+ case "equal":
152
+ case "equalRounded":
153
+ case "numericCompare":
154
+ case "durationCompare":
155
+ case "member":
156
+ case "delete":
157
+ case "contains":
158
+ return evaluateComparisonExpression(expression, context);
159
+ case "stringMatch":
160
+ case "substring":
161
+ case "patternMatch":
162
+ case "fieldValue":
163
+ return evaluateStringExpression(expression, context);
164
+ case "inside":
165
+ return evaluateGeometryExpression(expression, context);
166
+ case "repeat":
167
+ return evaluateRepeatExpression(expression, context);
168
+ case "customOperator":
169
+ return evaluateCustomOperatorExpression(expression, context);
170
+ default:
171
+ return assertNever(expression);
172
+ }
173
+ }
@@ -0,0 +1,69 @@
1
+ import type { QtiProcessingExpression } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+
4
+ export function expressionChildren(expression: QtiProcessingExpression): QtiProcessingExpression[] {
5
+ switch (expression.type) {
6
+ case "random":
7
+ return expression.values;
8
+ case "multiple":
9
+ case "ordered":
10
+ case "sum":
11
+ case "product":
12
+ case "min":
13
+ case "max":
14
+ case "and":
15
+ case "anyN":
16
+ case "or":
17
+ case "gcd":
18
+ case "lcm":
19
+ case "mathOperator":
20
+ case "repeat":
21
+ case "customOperator":
22
+ return expression.expressions;
23
+ case "subtract":
24
+ case "divide":
25
+ case "power":
26
+ case "integerDivide":
27
+ case "integerModulus":
28
+ case "match":
29
+ case "equal":
30
+ case "equalRounded":
31
+ case "numericCompare":
32
+ case "durationCompare":
33
+ case "stringMatch":
34
+ case "substring":
35
+ return [expression.left, expression.right];
36
+ case "not":
37
+ case "round":
38
+ case "roundTo":
39
+ case "truncate":
40
+ case "integerToFloat":
41
+ case "index":
42
+ case "containerSize":
43
+ case "patternMatch":
44
+ case "fieldValue":
45
+ case "inside":
46
+ case "statsOperator":
47
+ return [expression.expression];
48
+ case "member":
49
+ case "delete":
50
+ return [expression.value, expression.collection];
51
+ case "contains":
52
+ return [expression.collection, expression.values];
53
+ case "baseValue":
54
+ case "correct":
55
+ case "default":
56
+ case "isNull":
57
+ case "mapResponse":
58
+ case "mapResponsePoint":
59
+ case "matchCorrect":
60
+ case "mathConstant":
61
+ case "null":
62
+ case "randomFloat":
63
+ case "randomInteger":
64
+ case "variable":
65
+ return [];
66
+ default:
67
+ return assertNever(expression);
68
+ }
69
+ }
@@ -0,0 +1,153 @@
1
+ import type { QtiDocument, QtiResponseDeclaration, QtiValue } from "./types.js";
2
+ import { qtiScalarToString, qtiValueToStringList } from "./value-format.js";
3
+ import { isRecordValue, numericValue, valuesEqual } from "./processing-values.js";
4
+
5
+ export function lookupOutcomeValue(
6
+ document: QtiDocument,
7
+ identifier: string,
8
+ value: QtiValue,
9
+ ): QtiValue {
10
+ const declaration = document.item.outcomeDeclarations.find(
11
+ (outcome) => outcome.identifier === identifier,
12
+ );
13
+ const lookupTable = declaration?.lookupTable;
14
+ if (!lookupTable) return null;
15
+ if (value === null) return lookupTable.defaultValue;
16
+ const numeric = numericValue(value);
17
+ if (lookupTable.type === "match") {
18
+ return (
19
+ lookupTable.entries.find((entry) => entry.sourceValue === numeric)?.targetValue ??
20
+ lookupTable.defaultValue
21
+ );
22
+ }
23
+ const entry = [...lookupTable.entries]
24
+ .toSorted((left, right) => left.sourceValue - right.sourceValue)
25
+ .find(
26
+ (candidate) =>
27
+ numeric < candidate.sourceValue ||
28
+ (candidate.includeBoundary !== false && numeric === candidate.sourceValue),
29
+ );
30
+ return entry?.targetValue ?? lookupTable.defaultValue;
31
+ }
32
+
33
+ export function mapOrMatchResponse(
34
+ declaration: QtiResponseDeclaration,
35
+ response: QtiValue,
36
+ correctResponse: QtiValue,
37
+ ): number {
38
+ if (declaration.areaMapping) return scoreAreaMapping(response, declaration.areaMapping);
39
+ if (declaration.mapping) return scoreMapping(response, declaration.mapping);
40
+ return valuesEqual(response, correctResponse, declaration.cardinality === "ordered") ? 1 : 0;
41
+ }
42
+
43
+ export function scoreAreaMapping(
44
+ response: QtiValue,
45
+ areaMapping: NonNullable<QtiResponseDeclaration["areaMapping"]>,
46
+ ): number {
47
+ const points = Array.isArray(response)
48
+ ? response.map(qtiScalarToString)
49
+ : response === null
50
+ ? []
51
+ : qtiValueToStringList(response);
52
+ let score = 0;
53
+ for (const point of points) {
54
+ const parsed = parsePoint(point);
55
+ if (!parsed) {
56
+ score += areaMapping.defaultValue;
57
+ continue;
58
+ }
59
+ const entry = areaMapping.entries.find((candidate) => pointInsideArea(parsed, candidate));
60
+ score += entry?.mappedValue ?? areaMapping.defaultValue;
61
+ }
62
+ return clampMappedScore(score, areaMapping.attributes);
63
+ }
64
+
65
+ export function parsePoint(value: string): { x: number; y: number } | undefined {
66
+ const [x, y] = value
67
+ .trim()
68
+ .split(/[,\s]+/)
69
+ .map((part) => Number(part));
70
+ if (x === undefined || y === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
71
+ return undefined;
72
+ }
73
+ return { x, y };
74
+ }
75
+
76
+ export function pointInsideArea(
77
+ point: { x: number; y: number },
78
+ entry: NonNullable<QtiResponseDeclaration["areaMapping"]>["entries"][number],
79
+ ): boolean {
80
+ if (entry.shape === "circle") {
81
+ const [cx, cy, radius] = entry.coords;
82
+ if (cx === undefined || cy === undefined || radius === undefined) return false;
83
+ return Math.hypot(point.x - cx, point.y - cy) <= radius;
84
+ }
85
+
86
+ if (entry.shape === "rect") {
87
+ const [left, top, right, bottom] = entry.coords;
88
+ if (left === undefined || top === undefined || right === undefined || bottom === undefined) {
89
+ return false;
90
+ }
91
+ return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
92
+ }
93
+
94
+ if (entry.shape === "poly") {
95
+ return pointInsidePolygon(point, entry.coords);
96
+ }
97
+
98
+ return false;
99
+ }
100
+
101
+ function pointInsidePolygon(point: { x: number; y: number }, coords: number[]): boolean {
102
+ if (coords.length < 6 || coords.length % 2 !== 0) return false;
103
+ let inside = false;
104
+ for (let index = 0, previous = coords.length - 2; index < coords.length; index += 2) {
105
+ const xi = coords[index]!;
106
+ const yi = coords[index + 1]!;
107
+ const xj = coords[previous]!;
108
+ const yj = coords[previous + 1]!;
109
+ const intersects =
110
+ yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
111
+ if (intersects) inside = !inside;
112
+ previous = index;
113
+ }
114
+ return inside;
115
+ }
116
+
117
+ function scoreMapping(
118
+ response: QtiValue,
119
+ mapping: NonNullable<QtiResponseDeclaration["mapping"]>,
120
+ ): number {
121
+ const values = Object.fromEntries(
122
+ mapping.entries
123
+ .filter((entry) => entry.mapKey !== undefined)
124
+ .map((entry) => [entry.mapKey!, entry.mappedValue]),
125
+ );
126
+ if (Array.isArray(response)) {
127
+ const score = response.reduce<number>(
128
+ (sum, value) => sum + (values[String(value)] ?? mapping.defaultValue),
129
+ 0,
130
+ );
131
+ return clampMappedScore(score, mapping.attributes);
132
+ }
133
+ const score =
134
+ response === null || isRecordValue(response)
135
+ ? 0
136
+ : (values[String(response)] ?? mapping.defaultValue);
137
+ return clampMappedScore(score, mapping.attributes);
138
+ }
139
+
140
+ function clampMappedScore(score: number, attributes: Record<string, string>): number {
141
+ const lower = numericBound(attributes["lower-bound"]);
142
+ const upper = numericBound(attributes["upper-bound"]);
143
+ let clamped = score;
144
+ if (lower !== undefined) clamped = Math.max(clamped, lower);
145
+ if (upper !== undefined) clamped = Math.min(clamped, upper);
146
+ return clamped;
147
+ }
148
+
149
+ function numericBound(value: string | undefined): number | undefined {
150
+ if (value === undefined) return undefined;
151
+ const numberValue = Number(value);
152
+ return Number.isFinite(numberValue) ? numberValue : undefined;
153
+ }
@@ -0,0 +1,184 @@
1
+ import type { QtiValue } from "./types.js";
2
+
3
+ export const MATH_OPERATOR_NAMES = [
4
+ "abs",
5
+ "acos",
6
+ "acot",
7
+ "acsc",
8
+ "asec",
9
+ "asin",
10
+ "atan",
11
+ "atan2",
12
+ "ceil",
13
+ "cos",
14
+ "cosh",
15
+ "cot",
16
+ "coth",
17
+ "csc",
18
+ "csch",
19
+ "exp",
20
+ "floor",
21
+ "ln",
22
+ "log",
23
+ "sec",
24
+ "sech",
25
+ "signum",
26
+ "sin",
27
+ "sinh",
28
+ "tan",
29
+ "tanh",
30
+ "toDegrees",
31
+ "toRadians",
32
+ ] as const;
33
+
34
+ export const STATS_OPERATOR_NAMES = [
35
+ "mean",
36
+ "sampleVariance",
37
+ "sampleSD",
38
+ "popVariance",
39
+ "popSD",
40
+ ] as const;
41
+
42
+ export function roundToDecimalPlaces(value: number, figures: number): number {
43
+ const factor = 10 ** figures;
44
+ return Math.round(value * factor) / factor;
45
+ }
46
+
47
+ export function roundToSignificantFigures(value: number, figures: number): number {
48
+ if (value === 0 || figures <= 0) return 0;
49
+ const factor = 10 ** (figures - 1 - Math.floor(Math.log10(Math.abs(value))));
50
+ return Math.round(value * factor) / factor;
51
+ }
52
+
53
+ export function roundWithMode(value: number, roundingMode: string, figures: number): number | null {
54
+ if (!Number.isFinite(value)) return null;
55
+ if (roundingMode === "decimalPlaces") return roundToDecimalPlaces(value, figures);
56
+ if (roundingMode === "significantFigures") return roundToSignificantFigures(value, figures);
57
+ return null;
58
+ }
59
+
60
+ export function generalizedGcd(values: number[]): number {
61
+ let result = 0;
62
+ for (const value of values) {
63
+ result = gcd(result, Math.abs(value));
64
+ }
65
+ return result;
66
+ }
67
+
68
+ export function generalizedLcm(values: number[]): number {
69
+ if (values.some((value) => value === 0)) return 0;
70
+ return values.reduce((result, value) => lcm(result, Math.abs(value)), 1);
71
+ }
72
+
73
+ function gcd(left: number, right: number): number {
74
+ let a = Math.abs(left);
75
+ let b = Math.abs(right);
76
+ while (b !== 0) {
77
+ const next = a % b;
78
+ a = b;
79
+ b = next;
80
+ }
81
+ return a;
82
+ }
83
+
84
+ function lcm(left: number, right: number): number {
85
+ return Math.abs(left * right) / gcd(left, right);
86
+ }
87
+
88
+ export function mathOperatorValue(name: string, values: number[]): QtiValue {
89
+ const [first, second] = values;
90
+ if (first === undefined) return null;
91
+ switch (name) {
92
+ case "abs":
93
+ return finiteOrNull(Math.abs(first));
94
+ case "acos":
95
+ return Math.abs(first) > 1 ? null : finiteOrNull(Math.acos(first));
96
+ case "acot":
97
+ return finiteOrNull(Math.PI / 2 - Math.atan(1 / first));
98
+ case "acsc":
99
+ return Math.abs(first) < 1 ? null : finiteOrNull(Math.PI / 2 - Math.asin(1 / first));
100
+ case "asec":
101
+ return Math.abs(first) < 1 ? null : finiteOrNull(Math.PI / 2 - Math.acos(1 / first));
102
+ case "asin":
103
+ return Math.abs(first) > 1 ? null : finiteOrNull(Math.asin(first));
104
+ case "atan":
105
+ return finiteOrNull(Math.atan(first));
106
+ case "atan2":
107
+ return second === undefined ? null : finiteOrNull(Math.atan2(first, second));
108
+ case "ceil":
109
+ return finiteOrNull(Math.ceil(first));
110
+ case "cos":
111
+ return finiteOrNull(Math.cos(first));
112
+ case "cosh":
113
+ return finiteOrNull(Math.cosh(first));
114
+ case "cot":
115
+ return finiteOrNull(1 / Math.tan(first));
116
+ case "coth":
117
+ return finiteOrNull(1 / Math.tanh(first));
118
+ case "csc":
119
+ return finiteOrNull(1 / Math.sin(first));
120
+ case "csch":
121
+ return finiteOrNull(1 / Math.sinh(first));
122
+ case "exp":
123
+ return finiteOrNull(Math.exp(first));
124
+ case "floor":
125
+ return finiteOrNull(Math.floor(first));
126
+ case "ln":
127
+ return first < 0 ? null : finiteOrNull(Math.log(first));
128
+ case "log":
129
+ return first < 0 ? null : finiteOrNull(Math.log10(first));
130
+ case "sec":
131
+ return finiteOrNull(1 / Math.cos(first));
132
+ case "sech":
133
+ return finiteOrNull(1 / Math.cosh(first));
134
+ case "signum":
135
+ return finiteOrNull(Math.sign(first));
136
+ case "sin":
137
+ return finiteOrNull(Math.sin(first));
138
+ case "sinh":
139
+ return finiteOrNull(Math.sinh(first));
140
+ case "tan":
141
+ return finiteOrNull(Math.tan(first));
142
+ case "tanh":
143
+ return finiteOrNull(Math.tanh(first));
144
+ case "toDegrees":
145
+ return finiteOrNull((first * 180) / Math.PI);
146
+ case "toRadians":
147
+ return finiteOrNull((first * Math.PI) / 180);
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
153
+ export function statsOperatorValue(name: string, values: number[]): QtiValue {
154
+ if (values.length === 0) return 0;
155
+ const meanValue = mean(values);
156
+ const squareDiffs = values.map((value) => (value - meanValue) ** 2);
157
+ switch (name) {
158
+ case "mean":
159
+ return meanValue;
160
+ case "sampleVariance":
161
+ return meanWithDivisor(squareDiffs, values.length > 1 ? values.length - 1 : 1);
162
+ case "sampleSD":
163
+ return Math.sqrt(meanWithDivisor(squareDiffs, values.length > 1 ? values.length - 1 : 1));
164
+ case "popVariance":
165
+ return mean(squareDiffs);
166
+ case "popSD":
167
+ return Math.sqrt(mean(squareDiffs));
168
+ default:
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function mean(values: number[]): number {
174
+ return meanWithDivisor(values, values.length);
175
+ }
176
+
177
+ function meanWithDivisor(values: number[], divisor: number): number {
178
+ if (values.length === 0) return 0;
179
+ return values.reduce((sum, value) => sum + value, 0) / divisor;
180
+ }
181
+
182
+ function finiteOrNull(value: number): QtiValue {
183
+ return Number.isFinite(value) ? value : null;
184
+ }
@@ -0,0 +1,16 @@
1
+ export function seededRandom(seed: string | number): () => number {
2
+ let state = typeof seed === "number" ? seed >>> 0 : hashSeed(seed);
3
+ return () => {
4
+ state = (1664525 * state + 1013904223) >>> 0;
5
+ return state / 0x100000000;
6
+ };
7
+ }
8
+
9
+ function hashSeed(seed: string): number {
10
+ let hash = 2166136261;
11
+ for (let index = 0; index < seed.length; index += 1) {
12
+ hash ^= seed.charCodeAt(index);
13
+ hash = Math.imul(hash, 16777619);
14
+ }
15
+ return hash >>> 0;
16
+ }