@qti-components/processing 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/components/index.d.ts +4 -0
  2. package/dist/components/index.js +4 -0
  3. package/dist/components/index.js.map +1 -1
  4. package/dist/components/qti-container-size/qti-container-size.d.ts +2 -0
  5. package/dist/components/qti-container-size/qti-container-size.js +3 -0
  6. package/dist/components/qti-container-size/qti-container-size.js.map +1 -1
  7. package/dist/components/qti-contains/qti-contains.d.ts +24 -3
  8. package/dist/components/qti-contains/qti-contains.js +82 -30
  9. package/dist/components/qti-contains/qti-contains.js.map +1 -1
  10. package/dist/components/qti-default/qti-default.js +0 -1
  11. package/dist/components/qti-default/qti-default.js.map +1 -1
  12. package/dist/components/qti-divide/qti-divide.d.ts +3 -0
  13. package/dist/components/qti-divide/qti-divide.js +4 -0
  14. package/dist/components/qti-divide/qti-divide.js.map +1 -1
  15. package/dist/components/qti-equal/qti-equal.d.ts +12 -1
  16. package/dist/components/qti-equal/qti-equal.js +51 -7
  17. package/dist/components/qti-equal/qti-equal.js.map +1 -1
  18. package/dist/components/qti-equal-rounded/qti-equal-rounded.js +9 -1
  19. package/dist/components/qti-equal-rounded/qti-equal-rounded.js.map +1 -1
  20. package/dist/components/qti-exit-response/qti-exit-response.d.ts +17 -0
  21. package/dist/components/qti-exit-response/qti-exit-response.js +16 -0
  22. package/dist/components/qti-exit-response/qti-exit-response.js.map +1 -0
  23. package/dist/components/qti-exit-test/qti-exit-test.d.ts +16 -0
  24. package/dist/components/qti-exit-test/qti-exit-test.js +15 -0
  25. package/dist/components/qti-exit-test/qti-exit-test.js.map +1 -0
  26. package/dist/components/qti-field-value/qti-field-value.d.ts +7 -1
  27. package/dist/components/qti-field-value/qti-field-value.js +15 -12
  28. package/dist/components/qti-field-value/qti-field-value.js.map +1 -1
  29. package/dist/components/qti-gte/qti-gte.js +1 -1
  30. package/dist/components/qti-gte/qti-gte.js.map +1 -1
  31. package/dist/components/qti-integer-divide/qti-integer-divide.d.ts +2 -0
  32. package/dist/components/qti-integer-divide/qti-integer-divide.js +3 -0
  33. package/dist/components/qti-integer-divide/qti-integer-divide.js.map +1 -1
  34. package/dist/components/qti-integer-modulus/qti-integer-modulus.d.ts +3 -1
  35. package/dist/components/qti-integer-modulus/qti-integer-modulus.js +9 -2
  36. package/dist/components/qti-integer-modulus/qti-integer-modulus.js.map +1 -1
  37. package/dist/components/qti-integer-to-float/qti-integer-to-float.d.ts +2 -0
  38. package/dist/components/qti-integer-to-float/qti-integer-to-float.js +3 -0
  39. package/dist/components/qti-integer-to-float/qti-integer-to-float.js.map +1 -1
  40. package/dist/components/qti-is-null/qti-is-null.js +4 -1
  41. package/dist/components/qti-is-null/qti-is-null.js.map +1 -1
  42. package/dist/components/qti-lookup-outcome-value/qti-lookup-outcome-value.d.ts +2 -1
  43. package/dist/components/qti-lookup-outcome-value/qti-lookup-outcome-value.js +29 -5
  44. package/dist/components/qti-lookup-outcome-value/qti-lookup-outcome-value.js.map +1 -1
  45. package/dist/components/qti-lte/qti-lte.js +1 -1
  46. package/dist/components/qti-lte/qti-lte.js.map +1 -1
  47. package/dist/components/qti-map-response-point/qti-map-response-point.js +6 -0
  48. package/dist/components/qti-map-response-point/qti-map-response-point.js.map +1 -1
  49. package/dist/components/qti-math-constant/qti-math-constant.d.ts +20 -0
  50. package/dist/components/qti-math-constant/qti-math-constant.js +40 -0
  51. package/dist/components/qti-math-constant/qti-math-constant.js.map +1 -0
  52. package/dist/components/qti-math-operator/qti-math-operator.d.ts +6 -0
  53. package/dist/components/qti-math-operator/qti-math-operator.js +43 -1
  54. package/dist/components/qti-math-operator/qti-math-operator.js.map +1 -1
  55. package/dist/components/qti-multiple/qti-multiple.js +0 -2
  56. package/dist/components/qti-multiple/qti-multiple.js.map +1 -1
  57. package/dist/components/qti-pattern-match/qti-pattern-match.d.ts +1 -0
  58. package/dist/components/qti-pattern-match/qti-pattern-match.js +28 -4
  59. package/dist/components/qti-pattern-match/qti-pattern-match.js.map +1 -1
  60. package/dist/components/qti-power/qti-power.d.ts +2 -0
  61. package/dist/components/qti-power/qti-power.js +3 -0
  62. package/dist/components/qti-power/qti-power.js.map +1 -1
  63. package/dist/components/qti-random/qti-random.js +0 -1
  64. package/dist/components/qti-random/qti-random.js.map +1 -1
  65. package/dist/components/qti-repeat/qti-repeat.d.ts +0 -8
  66. package/dist/components/qti-repeat/qti-repeat.js +15 -2
  67. package/dist/components/qti-repeat/qti-repeat.js.map +1 -1
  68. package/dist/components/qti-response-processing-fragment/qti-response-processing-fragment.d.ts +19 -0
  69. package/dist/components/qti-response-processing-fragment/qti-response-processing-fragment.js +19 -0
  70. package/dist/components/qti-response-processing-fragment/qti-response-processing-fragment.js.map +1 -0
  71. package/dist/components/qti-round/qti-round.d.ts +2 -0
  72. package/dist/components/qti-round/qti-round.js +3 -0
  73. package/dist/components/qti-round/qti-round.js.map +1 -1
  74. package/dist/components/qti-round-to/qti-round-to.d.ts +2 -0
  75. package/dist/components/qti-round-to/qti-round-to.js +3 -0
  76. package/dist/components/qti-round-to/qti-round-to.js.map +1 -1
  77. package/dist/components/qti-stats-operator/qti-stats-operator.d.ts +3 -1
  78. package/dist/components/qti-stats-operator/qti-stats-operator.js +29 -7
  79. package/dist/components/qti-stats-operator/qti-stats-operator.js.map +1 -1
  80. package/dist/components/qti-truncate/qti-truncate.d.ts +2 -0
  81. package/dist/components/qti-truncate/qti-truncate.js +3 -0
  82. package/dist/components/qti-truncate/qti-truncate.js.map +1 -1
  83. package/dist/elements.d.ts +17 -1
  84. package/dist/elements.js +9 -1
  85. package/dist/elements.js.map +1 -1
  86. package/package.json +4 -4
@@ -0,0 +1,16 @@
1
+ import { QtiExitResponseSignal } from '@qti-components/base';
2
+ import { QtiRule } from '../qti-rule/qti-rule';
3
+ /**
4
+ * @summary The qti-exit-response rule ends the response processing of an attempt.
5
+ * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.exit-response
6
+ *
7
+ * No rule after it runs, at any depth. The outcomes already set are kept: a
8
+ * `qti-set-outcome-value` writes as it is processed, so everything before the
9
+ * exit has landed by the time this throws.
10
+ */
11
+ export class QtiExitResponse extends QtiRule {
12
+ process() {
13
+ throw new QtiExitResponseSignal();
14
+ }
15
+ }
16
+ //# sourceMappingURL=qti-exit-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-exit-response.js","sourceRoot":"","sources":["../../../src/components/qti-exit-response/qti-exit-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,OAAO,eAAgB,SAAQ,OAAO;IAC1B,OAAO;QACrB,MAAM,IAAI,qBAAqB,EAAE,CAAC;IACpC,CAAC;CACF","sourcesContent":["import { QtiExitResponseSignal } from '@qti-components/base';\n\nimport { QtiRule } from '../qti-rule/qti-rule';\n\n/**\n * @summary The qti-exit-response rule ends the response processing of an attempt.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.exit-response\n *\n * No rule after it runs, at any depth. The outcomes already set are kept: a\n * `qti-set-outcome-value` writes as it is processed, so everything before the\n * exit has landed by the time this throws.\n */\nexport class QtiExitResponse extends QtiRule {\n public override process(): never {\n throw new QtiExitResponseSignal();\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-exit-response': QtiExitResponse;\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import { QtiRule } from '../qti-rule/qti-rule';
2
+ /**
3
+ * @summary The qti-exit-test rule ends the outcome processing of a test.
4
+ * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.exit-test
5
+ *
6
+ * The test-level counterpart of `qti-exit-response`. No rule after it runs, at
7
+ * any depth, and the test outcomes already set are kept.
8
+ */
9
+ export declare class QtiExitTest extends QtiRule {
10
+ process(): never;
11
+ }
12
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ 'qti-exit-test': QtiExitTest;
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import { QtiExitTestSignal } from '@qti-components/base';
2
+ import { QtiRule } from '../qti-rule/qti-rule';
3
+ /**
4
+ * @summary The qti-exit-test rule ends the outcome processing of a test.
5
+ * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.exit-test
6
+ *
7
+ * The test-level counterpart of `qti-exit-response`. No rule after it runs, at
8
+ * any depth, and the test outcomes already set are kept.
9
+ */
10
+ export class QtiExitTest extends QtiRule {
11
+ process() {
12
+ throw new QtiExitTestSignal();
13
+ }
14
+ }
15
+ //# sourceMappingURL=qti-exit-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-exit-test.js","sourceRoot":"","sources":["../../../src/components/qti-exit-test/qti-exit-test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,OAAO,WAAY,SAAQ,OAAO;IACtB,OAAO;QACrB,MAAM,IAAI,iBAAiB,EAAE,CAAC;IAChC,CAAC;CACF","sourcesContent":["import { QtiExitTestSignal } from '@qti-components/base';\n\nimport { QtiRule } from '../qti-rule/qti-rule';\n\n/**\n * @summary The qti-exit-test rule ends the outcome processing of a test.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.exit-test\n *\n * The test-level counterpart of `qti-exit-response`. No rule after it runs, at\n * any depth, and the test outcomes already set are kept.\n */\nexport class QtiExitTest extends QtiRule {\n public override process(): never {\n throw new QtiExitTestSignal();\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-exit-test': QtiExitTest;\n }\n}\n"]}
@@ -7,6 +7,12 @@ import { QtiExpression } from '@qti-components/base';
7
7
  * Returns the value of the specified field.
8
8
  * Special cases: Returns NULL if sub-expression is NULL or field does not exist.
9
9
  */
10
- export declare class QtiFieldValue extends QtiExpression<string | string[]> {
10
+ export declare class QtiFieldValue extends QtiExpression<string | string[] | null> {
11
+ /**
12
+ * Every failure here is NULL rather than a thrown error. `qti-is-null` over a
13
+ * `qti-field-value` is the specified way to ask whether a record carries a
14
+ * field at all, so a missing one has to be an ordinary value — throwing
15
+ * aborted the whole response processing run instead.
16
+ */
11
17
  getResult(): any;
12
18
  }
@@ -8,27 +8,30 @@ import { QtiExpression } from '@qti-components/base';
8
8
  * Special cases: Returns NULL if sub-expression is NULL or field does not exist.
9
9
  */
10
10
  export class QtiFieldValue extends QtiExpression {
11
+ /**
12
+ * Every failure here is NULL rather than a thrown error. `qti-is-null` over a
13
+ * `qti-field-value` is the specified way to ask whether a record carries a
14
+ * field at all, so a missing one has to be an ordinary value — throwing
15
+ * aborted the whole response processing run instead.
16
+ */
11
17
  getResult() {
12
18
  const fieldIdentifier = this.getAttribute('field-identifier');
13
19
  if (!fieldIdentifier) {
14
- throw new Error('field-identifier attribute is required');
20
+ console.error('qti-field-value requires a field-identifier attribute');
21
+ return null;
15
22
  }
16
- // Get the result from the child expression (should be a record)
17
- const childElements = Array.from(this.children);
18
- if (childElements.length !== 1) {
19
- throw new Error('qti-field-value must have exactly one child expression');
23
+ if (this.children.length !== 1) {
24
+ console.error('qti-field-value must have exactly one child expression');
25
+ return null;
20
26
  }
21
27
  const variable = this.getVariables()[0];
22
28
  // Check if the result is a record/object
23
- if (variable.baseType !== 'record' || variable.value === null) {
24
- throw new Error('qti-field-value child expression must return a record');
29
+ if (!variable || variable.baseType !== 'record' || variable.value === null || variable.value === undefined) {
30
+ console.warn('qti-field-value child expression must return a record');
31
+ return null;
25
32
  }
26
- // Return the field value
27
33
  const fieldValue = variable.value[fieldIdentifier];
28
- if (fieldValue === undefined) {
29
- throw new Error(`Field "${fieldIdentifier}" not found in record`);
30
- }
31
- return fieldValue;
34
+ return fieldValue === undefined ? null : fieldValue;
32
35
  }
33
36
  }
34
37
  //# sourceMappingURL=qti-field-value.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qti-field-value.js","sourceRoot":"","sources":["../../../src/components/qti-field-value/qti-field-value.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,aAAc,SAAQ,aAAgC;IACjD,SAAS;QACvB,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,CAAC;QAE9D,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;QAC5D,CAAC;QAED,gEAAgE;QAChE,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAyB,CAAC;QAExE,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QAED,MAAM,QAAQ,GAAI,IAAI,CAAC,YAAY,EAAyB,CAAC,CAAC,CAAC,CAAC;QAEhE,yCAAyC;QACzC,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC3E,CAAC;QAED,yBAAyB;QACzB,MAAM,UAAU,GAAI,QAAQ,CAAC,KAA6B,CAAC,eAAe,CAAC,CAAC;QAE5E,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,UAAU,eAAe,uBAAuB,CAAC,CAAC;QACpE,CAAC;QAED,OAAO,UAAU,CAAC;IACpB,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-field-value operator retrieves a specific field from a record.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#fieldValue\n *\n * Takes a sub-expression with record cardinality. 'field-identifier' identifies the field.\n * Returns the value of the specified field.\n * Special cases: Returns NULL if sub-expression is NULL or field does not exist.\n */\nexport class QtiFieldValue extends QtiExpression<string | string[]> {\n public override getResult() {\n const fieldIdentifier = this.getAttribute('field-identifier');\n\n if (!fieldIdentifier) {\n throw new Error('field-identifier attribute is required');\n }\n\n // Get the result from the child expression (should be a record)\n const childElements = Array.from(this.children) as QtiExpression<any>[];\n\n if (childElements.length !== 1) {\n throw new Error('qti-field-value must have exactly one child expression');\n }\n\n const variable = (this.getVariables() as ResponseVariable[])[0];\n\n // Check if the result is a record/object\n if (variable.baseType !== 'record' || variable.value === null) {\n throw new Error('qti-field-value child expression must return a record');\n }\n\n // Return the field value\n const fieldValue = (variable.value as Record<string, any>)[fieldIdentifier];\n\n if (fieldValue === undefined) {\n throw new Error(`Field \"${fieldIdentifier}\" not found in record`);\n }\n\n return fieldValue;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-field-value.js","sourceRoot":"","sources":["../../../src/components/qti-field-value/qti-field-value.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,aAAc,SAAQ,aAAuC;IACxE;;;;;OAKG;IACa,SAAS;QACvB,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,CAAC;QAE9D,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;YACxE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAI,IAAI,CAAC,YAAY,EAAyB,CAAC,CAAC,CAAC,CAAC;QAEhE,yCAAyC;QACzC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC3G,OAAO,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAC;YACtE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,UAAU,GAAI,QAAQ,CAAC,KAA6B,CAAC,eAAe,CAAC,CAAC;QAE5E,OAAO,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC;IACtD,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-field-value operator retrieves a specific field from a record.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#fieldValue\n *\n * Takes a sub-expression with record cardinality. 'field-identifier' identifies the field.\n * Returns the value of the specified field.\n * Special cases: Returns NULL if sub-expression is NULL or field does not exist.\n */\nexport class QtiFieldValue extends QtiExpression<string | string[] | null> {\n /**\n * Every failure here is NULL rather than a thrown error. `qti-is-null` over a\n * `qti-field-value` is the specified way to ask whether a record carries a\n * field at all, so a missing one has to be an ordinary value — throwing\n * aborted the whole response processing run instead.\n */\n public override getResult() {\n const fieldIdentifier = this.getAttribute('field-identifier');\n\n if (!fieldIdentifier) {\n console.error('qti-field-value requires a field-identifier attribute');\n return null;\n }\n\n if (this.children.length !== 1) {\n console.error('qti-field-value must have exactly one child expression');\n return null;\n }\n\n const variable = (this.getVariables() as ResponseVariable[])[0];\n\n // Check if the result is a record/object\n if (!variable || variable.baseType !== 'record' || variable.value === null || variable.value === undefined) {\n console.warn('qti-field-value child expression must return a record');\n return null;\n }\n\n const fieldValue = (variable.value as Record<string, any>)[fieldIdentifier];\n\n return fieldValue === undefined ? null : fieldValue;\n }\n}\n"]}
@@ -16,7 +16,7 @@ export class QtiGte extends QtiConditionExpression {
16
16
  return null;
17
17
  }
18
18
  }
19
- console.log('unexpected number of children in qte');
19
+ console.error('unexpected number of children in qti gte');
20
20
  return null;
21
21
  }
22
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"qti-gte.js","sourceRoot":"","sources":["../../../src/components/qti-gte/qti-gte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAI9D,oFAAoF;AACpF,0DAA0D;AAC1D,MAAM,OAAO,MAAO,SAAQ,sBAAsB;IAChC,SAAS;QACvB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,IACE,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC;gBAC9D,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,EAC9D,CAAC;gBACD,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;gBAC/D,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { QtiConditionExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n// PK: For the contains we assume the expressions to calculate are all directedPairs\n// I don't know it this in QTI is always the case however?\nexport class QtiGte extends QtiConditionExpression {\n public override getResult(): boolean {\n if (this.children.length === 2) {\n const values = this.getVariables() as ResponseVariable[];\n const value1 = values[0];\n const value2 = values[1];\n if (\n (value1.baseType === 'integer' || value1.baseType === 'float') &&\n (value2.baseType === 'integer' || value2.baseType === 'float')\n ) {\n return +value1.value >= +value2.value;\n } else {\n console.error('unexpected baseType or cardinality in qti gte');\n return null;\n }\n }\n console.log('unexpected number of children in qte');\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-gte.js","sourceRoot":"","sources":["../../../src/components/qti-gte/qti-gte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAI9D,oFAAoF;AACpF,0DAA0D;AAC1D,MAAM,OAAO,MAAO,SAAQ,sBAAsB;IAChC,SAAS;QACvB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,IACE,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC;gBAC9D,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,EAC9D,CAAC;gBACD,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;gBAC/D,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { QtiConditionExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n// PK: For the contains we assume the expressions to calculate are all directedPairs\n// I don't know it this in QTI is always the case however?\nexport class QtiGte extends QtiConditionExpression {\n public override getResult(): boolean {\n if (this.children.length === 2) {\n const values = this.getVariables() as ResponseVariable[];\n const value1 = values[0];\n const value2 = values[1];\n if (\n (value1.baseType === 'integer' || value1.baseType === 'float') &&\n (value2.baseType === 'integer' || value2.baseType === 'float')\n ) {\n return +value1.value >= +value2.value;\n } else {\n console.error('unexpected baseType or cardinality in qti gte');\n return null;\n }\n }\n console.error('unexpected number of children in qti gte');\n return null;\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { QtiExpression } from '@qti-components/base';
2
+ import type { BaseType } from '@qti-components/base';
2
3
  /**
3
4
  * @summary The qti-integer-divide operator performs integer division rounded down.
4
5
  * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerDivide
@@ -9,5 +10,6 @@ import { QtiExpression } from '@qti-components/base';
9
10
  */
10
11
  export declare class QtiIntegerDivide extends QtiExpression<number | null> {
11
12
  #private;
13
+ get resultBaseType(): BaseType;
12
14
  getResult(): number | null;
13
15
  }
@@ -8,6 +8,9 @@ import { QtiExpression } from '@qti-components/base';
8
8
  * Special cases: Returns NULL if any argument is NULL or if divisor is 0.
9
9
  */
10
10
  export class QtiIntegerDivide extends QtiExpression {
11
+ get resultBaseType() {
12
+ return 'integer';
13
+ }
11
14
  getResult() {
12
15
  const values = this.#collectIntegerValues(this.getVariables());
13
16
  if (values.length !== 2) {
@@ -1 +1 @@
1
- {"version":3,"file":"qti-integer-divide.js","sourceRoot":"","sources":["../../../src/components/qti-integer-divide/qti-integer-divide.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAiB,SAAQ,aAA4B;IAChD,SAAS;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,YAAY,EAAwB,CAAC,CAAC;QAErF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;YACtE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,qBAAqB,CAAC,SAA6B;QACjD,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACpC,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;gBAC/D,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvE,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;gBAChE,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC5D,OAAO,EAAE,CAAC;YACZ,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;gBAC5D,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-integer-divide operator performs integer division rounded down.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerDivide\n *\n * Takes 2 sub-expressions with base-type integer and single cardinality.\n * Returns result of (x / y) rounded down toward negative infinity.\n * Special cases: Returns NULL if any argument is NULL or if divisor is 0.\n */\nexport class QtiIntegerDivide extends QtiExpression<number | null> {\n public override getResult(): number | null {\n const values = this.#collectIntegerValues(this.getVariables() as ResponseVariable[]);\n\n if (values.length !== 2) {\n console.error('qti-integer-divide requires exactly 2 integer values');\n return null;\n }\n\n if (values[1] === 0) {\n console.error('qti-integer-divide: division by zero');\n return null;\n }\n\n return Math.floor(values[0] / values[1]);\n }\n\n #collectIntegerValues(variables: ResponseVariable[]): number[] {\n const values: number[] = [];\n\n for (const variable of variables) {\n if (variable.baseType !== 'integer') {\n console.error('qti-integer-divide requires integer base-type');\n return [];\n }\n if (variable.cardinality !== 'single' || Array.isArray(variable.value)) {\n console.error('qti-integer-divide requires single cardinality');\n return [];\n }\n if (variable.value === null || variable.value === undefined) {\n return [];\n }\n\n const value = Number.parseInt(variable.value as string, 10);\n if (Number.isNaN(value)) {\n console.error('qti-integer-divide requires integer values');\n return [];\n }\n values.push(value);\n }\n\n return values;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-integer-divide.js","sourceRoot":"","sources":["../../../src/components/qti-integer-divide/qti-integer-divide.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAiB,SAAQ,aAA4B;IAChE,IAAoB,cAAc;QAChC,OAAO,SAAS,CAAC;IACnB,CAAC;IAEe,SAAS;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,YAAY,EAAwB,CAAC,CAAC;QAErF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;YACtE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,qBAAqB,CAAC,SAA6B;QACjD,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACpC,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;gBAC/D,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvE,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;gBAChE,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC5D,OAAO,EAAE,CAAC;YACZ,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;gBAC5D,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { BaseType, ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-integer-divide operator performs integer division rounded down.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerDivide\n *\n * Takes 2 sub-expressions with base-type integer and single cardinality.\n * Returns result of (x / y) rounded down toward negative infinity.\n * Special cases: Returns NULL if any argument is NULL or if divisor is 0.\n */\nexport class QtiIntegerDivide extends QtiExpression<number | null> {\n public override get resultBaseType(): BaseType {\n return 'integer';\n }\n\n public override getResult(): number | null {\n const values = this.#collectIntegerValues(this.getVariables() as ResponseVariable[]);\n\n if (values.length !== 2) {\n console.error('qti-integer-divide requires exactly 2 integer values');\n return null;\n }\n\n if (values[1] === 0) {\n console.error('qti-integer-divide: division by zero');\n return null;\n }\n\n return Math.floor(values[0] / values[1]);\n }\n\n #collectIntegerValues(variables: ResponseVariable[]): number[] {\n const values: number[] = [];\n\n for (const variable of variables) {\n if (variable.baseType !== 'integer') {\n console.error('qti-integer-divide requires integer base-type');\n return [];\n }\n if (variable.cardinality !== 'single' || Array.isArray(variable.value)) {\n console.error('qti-integer-divide requires single cardinality');\n return [];\n }\n if (variable.value === null || variable.value === undefined) {\n return [];\n }\n\n const value = Number.parseInt(variable.value as string, 10);\n if (Number.isNaN(value)) {\n console.error('qti-integer-divide requires integer values');\n return [];\n }\n values.push(value);\n }\n\n return values;\n }\n}\n"]}
@@ -1,13 +1,15 @@
1
1
  import { QtiExpression } from '@qti-components/base';
2
+ import type { BaseType } from '@qti-components/base';
2
3
  /**
3
4
  * @summary The qti-integer-modulus operator returns the remainder of integer division.
4
5
  * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerModulus
5
6
  *
6
7
  * Takes 2 sub-expressions with base-type integer and single cardinality.
7
- * Returns the remainder (x % y).
8
+ * Returns the remainder that goes with qti-integer-divide, which rounds down.
8
9
  * Special cases: Returns NULL if any argument is NULL or if divisor is 0.
9
10
  */
10
11
  export declare class QtiIntegerModulus extends QtiExpression<number | null> {
11
12
  #private;
13
+ get resultBaseType(): BaseType;
12
14
  getResult(): number | null;
13
15
  }
@@ -4,10 +4,13 @@ import { QtiExpression } from '@qti-components/base';
4
4
  * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerModulus
5
5
  *
6
6
  * Takes 2 sub-expressions with base-type integer and single cardinality.
7
- * Returns the remainder (x % y).
7
+ * Returns the remainder that goes with qti-integer-divide, which rounds down.
8
8
  * Special cases: Returns NULL if any argument is NULL or if divisor is 0.
9
9
  */
10
10
  export class QtiIntegerModulus extends QtiExpression {
11
+ get resultBaseType() {
12
+ return 'integer';
13
+ }
11
14
  getResult() {
12
15
  const values = this.#collectIntegerValues(this.getVariables());
13
16
  if (values.length !== 2) {
@@ -18,7 +21,11 @@ export class QtiIntegerModulus extends QtiExpression {
18
21
  console.error('qti-integer-modulus: division by zero');
19
22
  return null;
20
23
  }
21
- return values[0] % values[1];
24
+ // The remainder has to pair with qti-integer-divide, which rounds *down*
25
+ // (Math.floor). JavaScript's % truncates towards zero instead, so for a
26
+ // negative operand the two disagreed: -7 divide 3 gave -3 while -7 % 3 gave
27
+ // -1, where floored division leaves a remainder of 2.
28
+ return values[0] - Math.floor(values[0] / values[1]) * values[1];
22
29
  }
23
30
  #collectIntegerValues(variables) {
24
31
  const values = [];
@@ -1 +1 @@
1
- {"version":3,"file":"qti-integer-modulus.js","sourceRoot":"","sources":["../../../src/components/qti-integer-modulus/qti-integer-modulus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,aAA4B;IACjD,SAAS;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,YAAY,EAAwB,CAAC,CAAC;QAErF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;IAED,qBAAqB,CAAC,SAA6B;QACjD,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACpC,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;gBAChE,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvE,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;gBACjE,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC5D,OAAO,EAAE,CAAC;YACZ,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;gBAC7D,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-integer-modulus operator returns the remainder of integer division.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerModulus\n *\n * Takes 2 sub-expressions with base-type integer and single cardinality.\n * Returns the remainder (x % y).\n * Special cases: Returns NULL if any argument is NULL or if divisor is 0.\n */\nexport class QtiIntegerModulus extends QtiExpression<number | null> {\n public override getResult(): number | null {\n const values = this.#collectIntegerValues(this.getVariables() as ResponseVariable[]);\n\n if (values.length !== 2) {\n console.error('qti-integer-modulus requires exactly 2 integer values');\n return null;\n }\n\n if (values[1] === 0) {\n console.error('qti-integer-modulus: division by zero');\n return null;\n }\n\n return values[0] % values[1];\n }\n\n #collectIntegerValues(variables: ResponseVariable[]): number[] {\n const values: number[] = [];\n\n for (const variable of variables) {\n if (variable.baseType !== 'integer') {\n console.error('qti-integer-modulus requires integer base-type');\n return [];\n }\n if (variable.cardinality !== 'single' || Array.isArray(variable.value)) {\n console.error('qti-integer-modulus requires single cardinality');\n return [];\n }\n if (variable.value === null || variable.value === undefined) {\n return [];\n }\n\n const value = Number.parseInt(variable.value as string, 10);\n if (Number.isNaN(value)) {\n console.error('qti-integer-modulus requires integer values');\n return [];\n }\n values.push(value);\n }\n\n return values;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-integer-modulus.js","sourceRoot":"","sources":["../../../src/components/qti-integer-modulus/qti-integer-modulus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,aAA4B;IACjE,IAAoB,cAAc;QAChC,OAAO,SAAS,CAAC;IACnB,CAAC;IAEe,SAAS;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,YAAY,EAAwB,CAAC,CAAC;QAErF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,yEAAyE;QACzE,wEAAwE;QACxE,4EAA4E;QAC5E,sDAAsD;QACtD,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,qBAAqB,CAAC,SAA6B;QACjD,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACpC,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;gBAChE,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvE,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;gBACjE,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC5D,OAAO,EAAE,CAAC;YACZ,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;gBAC7D,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { BaseType, ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-integer-modulus operator returns the remainder of integer division.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerModulus\n *\n * Takes 2 sub-expressions with base-type integer and single cardinality.\n * Returns the remainder that goes with qti-integer-divide, which rounds down.\n * Special cases: Returns NULL if any argument is NULL or if divisor is 0.\n */\nexport class QtiIntegerModulus extends QtiExpression<number | null> {\n public override get resultBaseType(): BaseType {\n return 'integer';\n }\n\n public override getResult(): number | null {\n const values = this.#collectIntegerValues(this.getVariables() as ResponseVariable[]);\n\n if (values.length !== 2) {\n console.error('qti-integer-modulus requires exactly 2 integer values');\n return null;\n }\n\n if (values[1] === 0) {\n console.error('qti-integer-modulus: division by zero');\n return null;\n }\n\n // The remainder has to pair with qti-integer-divide, which rounds *down*\n // (Math.floor). JavaScript's % truncates towards zero instead, so for a\n // negative operand the two disagreed: -7 divide 3 gave -3 while -7 % 3 gave\n // -1, where floored division leaves a remainder of 2.\n return values[0] - Math.floor(values[0] / values[1]) * values[1];\n }\n\n #collectIntegerValues(variables: ResponseVariable[]): number[] {\n const values: number[] = [];\n\n for (const variable of variables) {\n if (variable.baseType !== 'integer') {\n console.error('qti-integer-modulus requires integer base-type');\n return [];\n }\n if (variable.cardinality !== 'single' || Array.isArray(variable.value)) {\n console.error('qti-integer-modulus requires single cardinality');\n return [];\n }\n if (variable.value === null || variable.value === undefined) {\n return [];\n }\n\n const value = Number.parseInt(variable.value as string, 10);\n if (Number.isNaN(value)) {\n console.error('qti-integer-modulus requires integer values');\n return [];\n }\n values.push(value);\n }\n\n return values;\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { QtiExpression } from '@qti-components/base';
2
+ import type { BaseType } from '@qti-components/base';
2
3
  /**
3
4
  * @summary The qti-integer-to-float operator converts an integer to a float.
4
5
  * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerToFloat
@@ -8,5 +9,6 @@ import { QtiExpression } from '@qti-components/base';
8
9
  * Special cases: Returns NULL if sub-expression is NULL.
9
10
  */
10
11
  export declare class QtiIntegerToFloat extends QtiExpression<number | null> {
12
+ get resultBaseType(): BaseType;
11
13
  getResult(): number | null;
12
14
  }
@@ -8,6 +8,9 @@ import { QtiExpression } from '@qti-components/base';
8
8
  * Special cases: Returns NULL if sub-expression is NULL.
9
9
  */
10
10
  export class QtiIntegerToFloat extends QtiExpression {
11
+ get resultBaseType() {
12
+ return 'float';
13
+ }
11
14
  getResult() {
12
15
  const variables = this.getVariables();
13
16
  if (variables.length !== 1) {
@@ -1 +1 @@
1
- {"version":3,"file":"qti-integer-to-float.js","sourceRoot":"","sources":["../../../src/components/qti-integer-to-float/qti-integer-to-float.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,aAA4B;IACjD,SAAS;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;QAE5D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;YAC5E,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACjE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACvE,OAAO,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;YAChE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-integer-to-float operator converts an integer to a float.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerToFloat\n *\n * Takes 1 sub-expression with base-type integer and single cardinality.\n * Returns a value of base-type float with the same numeric value.\n * Special cases: Returns NULL if sub-expression is NULL.\n */\nexport class QtiIntegerToFloat extends QtiExpression<number | null> {\n public override getResult(): number | null {\n const variables = this.getVariables() as ResponseVariable[];\n\n if (variables.length !== 1) {\n console.error('qti-integer-to-float requires exactly one child expression');\n return null;\n }\n\n const variable = variables[0];\n if (variable.baseType !== 'integer') {\n console.error('qti-integer-to-float requires integer base-type');\n return null;\n }\n if (variable.cardinality !== 'single' || Array.isArray(variable.value)) {\n console.error('qti-integer-to-float requires single cardinality');\n return null;\n }\n if (variable.value === null || variable.value === undefined) {\n return null;\n }\n\n const value = Number.parseInt(variable.value as string, 10);\n if (Number.isNaN(value)) {\n console.error('qti-integer-to-float requires an integer value');\n return null;\n }\n\n return value;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-integer-to-float.js","sourceRoot":"","sources":["../../../src/components/qti-integer-to-float/qti-integer-to-float.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,aAA4B;IACjE,IAAoB,cAAc;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IAEe,SAAS;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;QAE5D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;YAC5E,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACjE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,QAAQ,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACvE,OAAO,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;YAChE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { BaseType, ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-integer-to-float operator converts an integer to a float.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#integerToFloat\n *\n * Takes 1 sub-expression with base-type integer and single cardinality.\n * Returns a value of base-type float with the same numeric value.\n * Special cases: Returns NULL if sub-expression is NULL.\n */\nexport class QtiIntegerToFloat extends QtiExpression<number | null> {\n public override get resultBaseType(): BaseType {\n return 'float';\n }\n\n public override getResult(): number | null {\n const variables = this.getVariables() as ResponseVariable[];\n\n if (variables.length !== 1) {\n console.error('qti-integer-to-float requires exactly one child expression');\n return null;\n }\n\n const variable = variables[0];\n if (variable.baseType !== 'integer') {\n console.error('qti-integer-to-float requires integer base-type');\n return null;\n }\n if (variable.cardinality !== 'single' || Array.isArray(variable.value)) {\n console.error('qti-integer-to-float requires single cardinality');\n return null;\n }\n if (variable.value === null || variable.value === undefined) {\n return null;\n }\n\n const value = Number.parseInt(variable.value as string, 10);\n if (Number.isNaN(value)) {\n console.error('qti-integer-to-float requires an integer value');\n return null;\n }\n\n return value;\n }\n}\n"]}
@@ -11,7 +11,10 @@ export class QtiIsNull extends QtiExpression {
11
11
  getResult() {
12
12
  if (this.children.length === 1) {
13
13
  const variables = this.getVariables();
14
- if (!variables) {
14
+ // An empty list is an operand that resolved to nothing at all, which is
15
+ // as NULL as a value can get. `!variables` never caught that, because an
16
+ // empty array is truthy — and the read below then threw.
17
+ if (!variables?.length) {
15
18
  return true;
16
19
  }
17
20
  const value = variables[0].value;
@@ -1 +1 @@
1
- {"version":3,"file":"qti-is-null.js","sourceRoot":"","sources":["../../../src/components/qti-is-null/qti-is-null.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,SAAU,SAAQ,aAAsB;IACnC,SAAS;QACvB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;YAC5D,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACjC,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC;QAC7D,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC3D,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-is-null operator checks if a variable or expression is missing its value.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#isNull\n *\n * Takes 1 sub-expression with any base-type or cardinality.\n * Returns true if the sub-expression is NULL, false otherwise.\n * Special cases: Empty containers and empty strings are treated as NULL.\n */\nexport class QtiIsNull extends QtiExpression<boolean> {\n public override getResult(): boolean {\n if (this.children.length === 1) {\n const variables = this.getVariables() as ResponseVariable[];\n if (!variables) {\n return true;\n }\n const value = variables[0].value;\n return value == null || value == undefined || value === '';\n }\n console.error('unexpected number of children in qti Null');\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-is-null.js","sourceRoot":"","sources":["../../../src/components/qti-is-null/qti-is-null.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,SAAU,SAAQ,aAAsB;IACnC,SAAS;QACvB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;YAC5D,wEAAwE;YACxE,yEAAyE;YACzE,yDAAyD;YACzD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;gBACvB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACjC,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC;QAC7D,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC3D,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { QtiExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n/**\n * @summary The qti-is-null operator checks if a variable or expression is missing its value.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/info/index.html#isNull\n *\n * Takes 1 sub-expression with any base-type or cardinality.\n * Returns true if the sub-expression is NULL, false otherwise.\n * Special cases: Empty containers and empty strings are treated as NULL.\n */\nexport class QtiIsNull extends QtiExpression<boolean> {\n public override getResult(): boolean {\n if (this.children.length === 1) {\n const variables = this.getVariables() as ResponseVariable[];\n // An empty list is an operand that resolved to nothing at all, which is\n // as NULL as a value can get. `!variables` never caught that, because an\n // empty array is truthy — and the read below then threw.\n if (!variables?.length) {\n return true;\n }\n const value = variables[0].value;\n return value == null || value == undefined || value === '';\n }\n console.error('unexpected number of children in qti Null');\n return null;\n }\n}\n"]}
@@ -7,10 +7,11 @@ import type { ItemContext } from '@qti-components/base';
7
7
  * with the outcome's declaration.
8
8
  */
9
9
  export declare class QtiLookupOutcomeValue extends QtiRule {
10
+ #private;
10
11
  identifier: string;
11
12
  protected context?: ItemContext;
12
13
  get childExpression(): QtiExpression<string>;
13
- process(): number;
14
+ process(): number | string | null;
14
15
  }
15
16
  declare global {
16
17
  interface HTMLElementTagNameMap {
@@ -20,14 +20,27 @@ export class QtiLookupOutcomeValue extends QtiRule {
20
20
  }
21
21
  process() {
22
22
  const identifier = this.getAttribute('identifier');
23
- const outcomeVariable = this.context.variables.find(v => v.identifier === identifier) || null;
23
+ const outcomeVariable = this.context?.variables.find(v => v.identifier === identifier) || null;
24
+ if (!outcomeVariable) {
25
+ console.warn(`lookupOutcomeValue: no outcome declaration for "${identifier}"`);
26
+ return null;
27
+ }
28
+ const sourceValue = parseFloat(this.childExpression.calculate());
29
+ // A declaration carries one table or the other. The match table is checked
30
+ // first because its targets need no numeric interpretation.
24
31
  let value;
25
- if (outcomeVariable.interpolationTable) {
26
- value = outcomeVariable.interpolationTable.get(parseFloat(this.childExpression.calculate()));
32
+ if (outcomeVariable.matchTable?.size) {
33
+ value = outcomeVariable.matchTable.get(sourceValue);
34
+ }
35
+ else if (outcomeVariable.interpolationTable) {
36
+ value = this.#interpolate(sourceValue, outcomeVariable.interpolationTable);
27
37
  }
28
38
  if (value === null || value === undefined) {
29
- console.warn('lookupOutcomeValue: value is null or undefined');
30
- return 0;
39
+ // Nothing matched, so there is no value to write. The outcome keeps the
40
+ // default its declaration gave it rather than being overwritten with a
41
+ // score the table never specified.
42
+ console.warn(`lookupOutcomeValue: no table entry matches ${sourceValue} for "${identifier}"`);
43
+ return null;
31
44
  }
32
45
  this.dispatchEvent(new CustomEvent('qti-set-outcome-value', {
33
46
  bubbles: true,
@@ -39,6 +52,17 @@ export class QtiLookupOutcomeValue extends QtiRule {
39
52
  }));
40
53
  return value;
41
54
  }
55
+ /**
56
+ * An interpolation table entry's `source-value` is the *lower bound* of a
57
+ * range, not a value to match exactly, and the entries are tested in document
58
+ * order — the first whose bound the source value clears wins. Tables are
59
+ * therefore authored from the highest bound down.
60
+ */
61
+ #interpolate(sourceValue, entries) {
62
+ if (Number.isNaN(sourceValue))
63
+ return undefined;
64
+ return entries.find(entry => entry.includeBoundary ? sourceValue >= entry.sourceValue : sourceValue > entry.sourceValue)?.targetValue;
65
+ }
42
66
  }
43
67
  __decorate([
44
68
  property({ type: String })
@@ -1 +1 @@
1
- {"version":3,"file":"qti-lookup-outcome-value.js","sourceRoot":"","sources":["../../../src/components/qti-lookup-outcome-value/qti-lookup-outcome-value.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAM/C;;;;GAIG;AACH,MAAM,OAAO,qBAAsB,SAAQ,OAAO;IAOhD,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,iBAA0C,CAAC;IACzD,CAAC;IAEe,OAAO;QACrB,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAEnD,MAAM,eAAe,GACnB,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,IAAI,IAAI,CAAC;QAExE,IAAI,KAAK,CAAC;QACV,IAAI,eAAe,CAAC,kBAAkB,EAAE,CAAC;YACvC,KAAK,GAAG,eAAe,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAC/F,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC1C,OAAO,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;YAC/D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAA0D,uBAAuB,EAAE;YAChG,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE;gBACN,iBAAiB,EAAE,IAAI,CAAC,UAAU;gBAClC,KAAK,EAAE,8BAA8B,CAAC,KAAK,CAAC;aAC7C;SACF,CAAC,CACH,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AApC6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;yDAAoB;AAIrC;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAClD,KAAK,EAAE;sDACwB","sourcesContent":["import { property, state } from 'lit/decorators.js';\nimport { consume } from '@lit/context';\n\nimport { convertNumberToUniversalFormat } from '@qti-components/base';\nimport { itemContext } from '@qti-components/base';\n\nimport { QtiRule } from '../qti-rule/qti-rule';\n\nimport type { QtiExpression } from '@qti-components/base';\nimport type { OutcomeVariable } from '@qti-components/base';\nimport type { ItemContext } from '@qti-components/base';\n\n/**\n * The lookupOutcomeValue rule sets the value of an outcome variable to the value obtained\n * by looking up the value of the associated expression in the lookupTable associated\n * with the outcome's declaration.\n */\nexport class QtiLookupOutcomeValue extends QtiRule {\n @property({ type: String }) identifier: string;\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n protected context?: ItemContext;\n\n get childExpression(): QtiExpression<string> {\n return this.firstElementChild as QtiExpression<string>;\n }\n\n public override process(): number {\n const identifier = this.getAttribute('identifier');\n\n const outcomeVariable: OutcomeVariable | null =\n this.context.variables.find(v => v.identifier === identifier) || null;\n\n let value;\n if (outcomeVariable.interpolationTable) {\n value = outcomeVariable.interpolationTable.get(parseFloat(this.childExpression.calculate()));\n }\n if (value === null || value === undefined) {\n console.warn('lookupOutcomeValue: value is null or undefined');\n return 0;\n }\n this.dispatchEvent(\n new CustomEvent<{ outcomeIdentifier: string; value: string | string[] }>('qti-set-outcome-value', {\n bubbles: true,\n composed: true,\n detail: {\n outcomeIdentifier: this.identifier,\n value: convertNumberToUniversalFormat(value)\n }\n })\n );\n return value;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-lookup-outcome-value': QtiLookupOutcomeValue;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-lookup-outcome-value.js","sourceRoot":"","sources":["../../../src/components/qti-lookup-outcome-value/qti-lookup-outcome-value.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAM/C;;;;GAIG;AACH,MAAM,OAAO,qBAAsB,SAAQ,OAAO;IAOhD,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,iBAA0C,CAAC;IACzD,CAAC;IAEe,OAAO;QACrB,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAEnD,MAAM,eAAe,GACnB,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,IAAI,IAAI,CAAC;QAEzE,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,mDAAmD,UAAU,GAAG,CAAC,CAAC;YAC/E,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC,CAAC;QAEjE,2EAA2E;QAC3E,4DAA4D;QAC5D,IAAI,KAAkC,CAAC;QACvC,IAAI,eAAe,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC;YACrC,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACtD,CAAC;aAAM,IAAI,eAAe,CAAC,kBAAkB,EAAE,CAAC;YAC9C,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,eAAe,CAAC,kBAAkB,CAAC,CAAC;QAC7E,CAAC;QAED,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC1C,wEAAwE;YACxE,uEAAuE;YACvE,mCAAmC;YACnC,OAAO,CAAC,IAAI,CAAC,8CAA8C,WAAW,SAAS,UAAU,GAAG,CAAC,CAAC;YAC9F,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAA0D,uBAAuB,EAAE;YAChG,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE;gBACN,iBAAiB,EAAE,IAAI,CAAC,UAAU;gBAClC,KAAK,EAAE,8BAA8B,CAAC,KAAK,CAAC;aAC7C;SACF,CAAC,CACH,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,WAAmB,EAAE,OAAkC;QAClE,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC;YAAE,OAAO,SAAS,CAAC;QAEhD,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAC1B,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAC3F,EAAE,WAAW,CAAC;IACjB,CAAC;CACF;AAjE6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;yDAAoB;AAIrC;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAClD,KAAK,EAAE;sDACwB","sourcesContent":["import { property, state } from 'lit/decorators.js';\nimport { consume } from '@lit/context';\n\nimport { convertNumberToUniversalFormat } from '@qti-components/base';\nimport { itemContext } from '@qti-components/base';\n\nimport { QtiRule } from '../qti-rule/qti-rule';\n\nimport type { QtiExpression } from '@qti-components/base';\nimport type { InterpolationTableEntry, OutcomeVariable } from '@qti-components/base';\nimport type { ItemContext } from '@qti-components/base';\n\n/**\n * The lookupOutcomeValue rule sets the value of an outcome variable to the value obtained\n * by looking up the value of the associated expression in the lookupTable associated\n * with the outcome's declaration.\n */\nexport class QtiLookupOutcomeValue extends QtiRule {\n @property({ type: String }) identifier: string;\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n protected context?: ItemContext;\n\n get childExpression(): QtiExpression<string> {\n return this.firstElementChild as QtiExpression<string>;\n }\n\n public override process(): number | string | null {\n const identifier = this.getAttribute('identifier');\n\n const outcomeVariable: OutcomeVariable | null =\n this.context?.variables.find(v => v.identifier === identifier) || null;\n\n if (!outcomeVariable) {\n console.warn(`lookupOutcomeValue: no outcome declaration for \"${identifier}\"`);\n return null;\n }\n\n const sourceValue = parseFloat(this.childExpression.calculate());\n\n // A declaration carries one table or the other. The match table is checked\n // first because its targets need no numeric interpretation.\n let value: number | string | undefined;\n if (outcomeVariable.matchTable?.size) {\n value = outcomeVariable.matchTable.get(sourceValue);\n } else if (outcomeVariable.interpolationTable) {\n value = this.#interpolate(sourceValue, outcomeVariable.interpolationTable);\n }\n\n if (value === null || value === undefined) {\n // Nothing matched, so there is no value to write. The outcome keeps the\n // default its declaration gave it rather than being overwritten with a\n // score the table never specified.\n console.warn(`lookupOutcomeValue: no table entry matches ${sourceValue} for \"${identifier}\"`);\n return null;\n }\n this.dispatchEvent(\n new CustomEvent<{ outcomeIdentifier: string; value: string | string[] }>('qti-set-outcome-value', {\n bubbles: true,\n composed: true,\n detail: {\n outcomeIdentifier: this.identifier,\n value: convertNumberToUniversalFormat(value)\n }\n })\n );\n return value;\n }\n\n /**\n * An interpolation table entry's `source-value` is the *lower bound* of a\n * range, not a value to match exactly, and the entries are tested in document\n * order — the first whose bound the source value clears wins. Tables are\n * therefore authored from the highest bound down.\n */\n #interpolate(sourceValue: number, entries: InterpolationTableEntry[]): number | undefined {\n if (Number.isNaN(sourceValue)) return undefined;\n\n return entries.find(entry =>\n entry.includeBoundary ? sourceValue >= entry.sourceValue : sourceValue > entry.sourceValue\n )?.targetValue;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-lookup-outcome-value': QtiLookupOutcomeValue;\n }\n}\n"]}
@@ -16,7 +16,7 @@ export class QtiLte extends QtiConditionExpression {
16
16
  return null;
17
17
  }
18
18
  }
19
- console.log('unexpected number of children in lte');
19
+ console.error('unexpected number of children in qti lte');
20
20
  return null;
21
21
  }
22
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"qti-lte.js","sourceRoot":"","sources":["../../../src/components/qti-lte/qti-lte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAI9D,oFAAoF;AACpF,0DAA0D;AAC1D,MAAM,OAAO,MAAO,SAAQ,sBAAsB;IAChC,SAAS;QACvB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,IACE,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC;gBAC9D,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,EAC9D,CAAC;gBACD,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;gBAC/D,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { QtiConditionExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n// PK: For the contains we assume the expressions to calculate are all directedPairs\n// I don't know it this in QTI is always the case however?\nexport class QtiLte extends QtiConditionExpression {\n public override getResult(): boolean {\n if (this.children.length === 2) {\n const values = this.getVariables() as ResponseVariable[];\n const value1 = values[0];\n const value2 = values[1];\n if (\n (value1.baseType === 'integer' || value1.baseType === 'float') &&\n (value2.baseType === 'integer' || value2.baseType === 'float')\n ) {\n return +value1.value <= +value2.value;\n } else {\n console.error('unexpected baseType or cardinality in qti lte');\n return null;\n }\n }\n console.log('unexpected number of children in lte');\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-lte.js","sourceRoot":"","sources":["../../../src/components/qti-lte/qti-lte.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAI9D,oFAAoF;AACpF,0DAA0D;AAC1D,MAAM,OAAO,MAAO,SAAQ,sBAAsB;IAChC,SAAS;QACvB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAwB,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,IACE,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC;gBAC9D,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,EAC9D,CAAC;gBACD,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;gBAC/D,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { QtiConditionExpression } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\n\n// PK: For the contains we assume the expressions to calculate are all directedPairs\n// I don't know it this in QTI is always the case however?\nexport class QtiLte extends QtiConditionExpression {\n public override getResult(): boolean {\n if (this.children.length === 2) {\n const values = this.getVariables() as ResponseVariable[];\n const value1 = values[0];\n const value2 = values[1];\n if (\n (value1.baseType === 'integer' || value1.baseType === 'float') &&\n (value2.baseType === 'integer' || value2.baseType === 'float')\n ) {\n return +value1.value <= +value2.value;\n } else {\n console.error('unexpected baseType or cardinality in qti lte');\n return null;\n }\n }\n console.error('unexpected number of children in qti lte');\n return null;\n }\n}\n"]}
@@ -36,6 +36,12 @@ export class QtiMapResponsePoint extends QtiExpression {
36
36
  if (isPointInArea) {
37
37
  result += entry.mappedValue ?? 0;
38
38
  mappedAreas.add(entry.coords);
39
+ // First match wins: "each area is tested in turn, with those listed first taking priority
40
+ // in the case where areas overlap and a point falls in the intersection" (QTI 3.0 §7.4).
41
+ // Without this the point would also score every later area it happens to fall in — which
42
+ // only became reachable once `shape="default"`, an area covering the whole image, started
43
+ // matching (Citolab/qti-components#83).
44
+ break;
39
45
  }
40
46
  }
41
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"qti-map-response-point.js","sourceRoot":"","sources":["../../../src/components/qti-map-response-point/qti-map-response-point.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAKrD,MAAM,OAAO,mBAAoB,SAAQ,aAAqB;IAG5C,SAAS;QACvB,MAAM,QAAQ,GAAqB,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC;QACtG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,kBAAkB,CAAC,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,WAAW,GAAmB,QAAQ,CAAC,WAAW,CAAC;QACzD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,uCAAuC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,kBAAkB,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC9F,IAAI,CAAC,kBAAkB,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3D,OAAO,CAAC,IAAI,CAAC,uCAAuC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,qDAAqD;QACrD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;QAEtC,KAAK,MAAM,iBAAiB,IAAI,kBAAkB,EAAE,CAAC;YACnD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,cAAc,EAAE,CAAC;gBAC/C,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;oBAClC,SAAS,CAAC,2CAA2C;gBACvD,CAAC;gBACD,MAAM,aAAa,GAAG,aAAa,CAAC,aAAa,CAC/C,iBAAiB,EACjB,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,EAChC,QAAQ,CAAC,QAAQ,CAClB,CAAC;gBACF,IAAI,aAAa,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC;oBACjC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;QAED,sDAAsD;QACtD,IAAI,WAAW,CAAC,IAAI,GAAG,kBAAkB,CAAC,MAAM,EAAE,CAAC;YACjD,MAAM,IAAI,WAAW,CAAC,YAAY,CAAC;QACrC,CAAC;QAED,0BAA0B;QAC1B,IAAI,WAAW,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YACnC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,WAAW,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YACnC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AA1D6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;uDAAoB","sourcesContent":["import { property } from 'lit/decorators.js';\n\nimport { QtiExpression } from '@qti-components/base';\nimport { ScoringHelper } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\nimport type { QtiAreaMapping } from '@qti-components/base';\n\nexport class QtiMapResponsePoint extends QtiExpression<number> {\n @property({ type: String }) identifier: string;\n\n public override getResult(): number {\n const response: ResponseVariable = this.context.variables.find(r => r.identifier === this.identifier);\n if (!response) {\n console.warn(`Response ${this.identifier} cannot be found`);\n return null;\n }\n\n const areaMapping: QtiAreaMapping = response.areaMapping;\n if (!areaMapping) {\n console.warn(`Area mapping not found for response ${this.identifier}`);\n return null;\n }\n\n const candidateResponses = !Array.isArray(response.value) ? [response.value] : response.value;\n if (!candidateResponses || candidateResponses.length === 0) {\n console.warn(`No candidate responses for response ${this.identifier}`);\n return null;\n }\n\n let result = 0;\n\n // Keep track of areas that have already been matched\n const mappedAreas = new Set<string>();\n\n for (const candidateResponse of candidateResponses) {\n for (const entry of areaMapping.areaMapEntries) {\n if (mappedAreas.has(entry.coords)) {\n continue; // Skip areas that have already been mapped\n }\n const isPointInArea = ScoringHelper.isPointInArea(\n candidateResponse,\n `${entry.shape},${entry.coords}`,\n response.baseType\n );\n if (isPointInArea) {\n result += entry.mappedValue ?? 0;\n mappedAreas.add(entry.coords);\n }\n }\n }\n\n // Add default value for unmatched candidate responses\n if (mappedAreas.size < candidateResponses.length) {\n result += areaMapping.defaultValue;\n }\n\n // Apply bounds if defined\n if (areaMapping.lowerBound != null) {\n result = Math.max(areaMapping.lowerBound, result);\n }\n if (areaMapping.upperBound != null) {\n result = Math.min(areaMapping.upperBound, result);\n }\n\n return result;\n }\n}\n"]}
1
+ {"version":3,"file":"qti-map-response-point.js","sourceRoot":"","sources":["../../../src/components/qti-map-response-point/qti-map-response-point.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAKrD,MAAM,OAAO,mBAAoB,SAAQ,aAAqB;IAG5C,SAAS;QACvB,MAAM,QAAQ,GAAqB,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC;QACtG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,kBAAkB,CAAC,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,WAAW,GAAmB,QAAQ,CAAC,WAAW,CAAC;QACzD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,uCAAuC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,kBAAkB,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC9F,IAAI,CAAC,kBAAkB,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3D,OAAO,CAAC,IAAI,CAAC,uCAAuC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,qDAAqD;QACrD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;QAEtC,KAAK,MAAM,iBAAiB,IAAI,kBAAkB,EAAE,CAAC;YACnD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,cAAc,EAAE,CAAC;gBAC/C,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;oBAClC,SAAS,CAAC,2CAA2C;gBACvD,CAAC;gBACD,MAAM,aAAa,GAAG,aAAa,CAAC,aAAa,CAC/C,iBAAiB,EACjB,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,EAChC,QAAQ,CAAC,QAAQ,CAClB,CAAC;gBACF,IAAI,aAAa,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC;oBACjC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;oBAC9B,0FAA0F;oBAC1F,yFAAyF;oBACzF,yFAAyF;oBACzF,0FAA0F;oBAC1F,wCAAwC;oBACxC,MAAM;gBACR,CAAC;YACH,CAAC;QACH,CAAC;QAED,sDAAsD;QACtD,IAAI,WAAW,CAAC,IAAI,GAAG,kBAAkB,CAAC,MAAM,EAAE,CAAC;YACjD,MAAM,IAAI,WAAW,CAAC,YAAY,CAAC;QACrC,CAAC;QAED,0BAA0B;QAC1B,IAAI,WAAW,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YACnC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,WAAW,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YACnC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAhE6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;uDAAoB","sourcesContent":["import { property } from 'lit/decorators.js';\n\nimport { QtiExpression } from '@qti-components/base';\nimport { ScoringHelper } from '@qti-components/base';\n\nimport type { ResponseVariable } from '@qti-components/base';\nimport type { QtiAreaMapping } from '@qti-components/base';\n\nexport class QtiMapResponsePoint extends QtiExpression<number> {\n @property({ type: String }) identifier: string;\n\n public override getResult(): number {\n const response: ResponseVariable = this.context.variables.find(r => r.identifier === this.identifier);\n if (!response) {\n console.warn(`Response ${this.identifier} cannot be found`);\n return null;\n }\n\n const areaMapping: QtiAreaMapping = response.areaMapping;\n if (!areaMapping) {\n console.warn(`Area mapping not found for response ${this.identifier}`);\n return null;\n }\n\n const candidateResponses = !Array.isArray(response.value) ? [response.value] : response.value;\n if (!candidateResponses || candidateResponses.length === 0) {\n console.warn(`No candidate responses for response ${this.identifier}`);\n return null;\n }\n\n let result = 0;\n\n // Keep track of areas that have already been matched\n const mappedAreas = new Set<string>();\n\n for (const candidateResponse of candidateResponses) {\n for (const entry of areaMapping.areaMapEntries) {\n if (mappedAreas.has(entry.coords)) {\n continue; // Skip areas that have already been mapped\n }\n const isPointInArea = ScoringHelper.isPointInArea(\n candidateResponse,\n `${entry.shape},${entry.coords}`,\n response.baseType\n );\n if (isPointInArea) {\n result += entry.mappedValue ?? 0;\n mappedAreas.add(entry.coords);\n // First match wins: \"each area is tested in turn, with those listed first taking priority\n // in the case where areas overlap and a point falls in the intersection\" (QTI 3.0 §7.4).\n // Without this the point would also score every later area it happens to fall in — which\n // only became reachable once `shape=\"default\"`, an area covering the whole image, started\n // matching (Citolab/qti-components#83).\n break;\n }\n }\n }\n\n // Add default value for unmatched candidate responses\n if (mappedAreas.size < candidateResponses.length) {\n result += areaMapping.defaultValue;\n }\n\n // Apply bounds if defined\n if (areaMapping.lowerBound != null) {\n result = Math.max(areaMapping.lowerBound, result);\n }\n if (areaMapping.upperBound != null) {\n result = Math.min(areaMapping.upperBound, result);\n }\n\n return result;\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { QtiExpression } from '@qti-components/base';
2
+ import type { BaseType } from '@qti-components/base';
3
+ /**
4
+ * @summary The qti-math-constant expression returns a mathematical constant.
5
+ * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.math-constant
6
+ *
7
+ * Takes no sub-expressions. The 'name' attribute selects the constant.
8
+ * Returns a single float.
9
+ * Special cases: Returns NULL for a name outside the vocabulary.
10
+ */
11
+ export declare class QtiMathConstant extends QtiExpression<number | null> {
12
+ name: string;
13
+ get resultBaseType(): BaseType;
14
+ getResult(): number | null;
15
+ }
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ 'qti-math-constant': QtiMathConstant;
19
+ }
20
+ }
@@ -0,0 +1,40 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { property } from 'lit/decorators.js';
8
+ import { QtiExpression } from '@qti-components/base';
9
+ /**
10
+ * @summary The qti-math-constant expression returns a mathematical constant.
11
+ * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.math-constant
12
+ *
13
+ * Takes no sub-expressions. The 'name' attribute selects the constant.
14
+ * Returns a single float.
15
+ * Special cases: Returns NULL for a name outside the vocabulary.
16
+ */
17
+ export class QtiMathConstant extends QtiExpression {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.name = '';
21
+ }
22
+ get resultBaseType() {
23
+ return 'float';
24
+ }
25
+ getResult() {
26
+ switch (this.name.toLowerCase()) {
27
+ case 'pi':
28
+ return Math.PI;
29
+ case 'e':
30
+ return Math.E;
31
+ default:
32
+ console.error(`qti-math-constant: unknown constant "${this.name}"`);
33
+ return null;
34
+ }
35
+ }
36
+ }
37
+ __decorate([
38
+ property({ type: String })
39
+ ], QtiMathConstant.prototype, "name", void 0);
40
+ //# sourceMappingURL=qti-math-constant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-math-constant.js","sourceRoot":"","sources":["../../../src/components/qti-math-constant/qti-math-constant.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIrD;;;;;;;GAOG;AACH,MAAM,OAAO,eAAgB,SAAQ,aAA4B;IAAjE;;QAC8B,SAAI,GAAW,EAAE,CAAC;IAiBhD,CAAC;IAfC,IAAoB,cAAc;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IAEe,SAAS;QACvB,QAAQ,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAChC,KAAK,IAAI;gBACP,OAAO,IAAI,CAAC,EAAE,CAAC;YACjB,KAAK,GAAG;gBACN,OAAO,IAAI,CAAC,CAAC,CAAC;YAChB;gBACE,OAAO,CAAC,KAAK,CAAC,wCAAwC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;gBACpE,OAAO,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;CACF;AAjB6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CAAmB","sourcesContent":["import { property } from 'lit/decorators.js';\n\nimport { QtiExpression } from '@qti-components/base';\n\nimport type { BaseType } from '@qti-components/base';\n\n/**\n * @summary The qti-math-constant expression returns a mathematical constant.\n * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.math-constant\n *\n * Takes no sub-expressions. The 'name' attribute selects the constant.\n * Returns a single float.\n * Special cases: Returns NULL for a name outside the vocabulary.\n */\nexport class QtiMathConstant extends QtiExpression<number | null> {\n @property({ type: String }) name: string = '';\n\n public override get resultBaseType(): BaseType {\n return 'float';\n }\n\n public override getResult(): number | null {\n switch (this.name.toLowerCase()) {\n case 'pi':\n return Math.PI;\n case 'e':\n return Math.E;\n default:\n console.error(`qti-math-constant: unknown constant \"${this.name}\"`);\n return null;\n }\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-math-constant': QtiMathConstant;\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { QtiExpression } from '@qti-components/base';
2
+ import type { BaseType } from '@qti-components/base';
2
3
  /**
3
4
  * @summary The qti-math-operator performs mathematical operations on numerical values.
4
5
  * @documentation https://www.imsglobal.org/spec/qti/v3p0/impl#h.math-operator
@@ -10,5 +11,10 @@ import { QtiExpression } from '@qti-components/base';
10
11
  export declare class QtiMathOperator extends QtiExpression<number | null> {
11
12
  #private;
12
13
  name: string;
14
+ /**
15
+ * Float for every function except the three that produce a whole number:
16
+ * `floor`, `ceil` and `signum`.
17
+ */
18
+ get resultBaseType(): BaseType;
13
19
  getResult(): number | null;
14
20
  }