@longsightgroup/qti3-core 0.2.1 → 0.3.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.
package/dist/session.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { qtiScalarToString, qtiValueToString, qtiValueToStringList } from "./value-format.js";
1
2
  const COMPLETION_STATUS = "completionStatus";
2
3
  const COMPLETION_NOT_ATTEMPTED = "not_attempted";
3
4
  const COMPLETION_UNKNOWN = "unknown";
@@ -14,9 +15,10 @@ export function visibleModalFeedback(item, outcomes) {
14
15
  if (feedback.showHide === "hide")
15
16
  return false;
16
17
  const outcome = outcomes[feedback.outcomeIdentifier];
17
- if (Array.isArray(outcome))
18
- return outcome.includes(feedback.identifier);
19
- return String(outcome ?? "") === feedback.identifier;
18
+ const outcomeValue = outcome === undefined ? null : outcome;
19
+ if (Array.isArray(outcomeValue))
20
+ return outcomeValue.includes(feedback.identifier);
21
+ return qtiValueToString(outcomeValue) === feedback.identifier;
20
22
  });
21
23
  }
22
24
  export function createItemSession(document, priorState, options = {}) {
@@ -27,6 +29,7 @@ export function createItemSession(document, priorState, options = {}) {
27
29
  const priorInteractionStates = clonePortableCustomStateRecord(priorState?.interactionStates ?? {});
28
30
  let validationMessages = cloneDiagnostics(priorState?.validationMessages ?? []);
29
31
  const responses = {};
32
+ const responseDefaults = {};
30
33
  const outcomes = {};
31
34
  const templateValues = {};
32
35
  const interactionStates = {};
@@ -40,8 +43,8 @@ export function createItemSession(document, priorState, options = {}) {
40
43
  .filter((identifier) => Boolean(identifier)));
41
44
  for (const declaration of document.item.responseDeclarations) {
42
45
  correctResponses[declaration.identifier] = cloneValue(declaration.correctResponse);
43
- if (declaration.defaultValue !== null && responses[declaration.identifier] === undefined) {
44
- responses[declaration.identifier] = cloneValue(declaration.defaultValue);
46
+ if (declaration.defaultValue !== null) {
47
+ responseDefaults[declaration.identifier] = cloneValue(declaration.defaultValue);
45
48
  }
46
49
  }
47
50
  for (const declaration of document.item.templateDeclarations) {
@@ -52,12 +55,13 @@ export function createItemSession(document, priorState, options = {}) {
52
55
  }
53
56
  outcomes[COMPLETION_STATUS] = COMPLETION_NOT_ATTEMPTED;
54
57
  const baseResponses = cloneValueRecord(responses);
58
+ const baseResponseDefaults = cloneValueRecord(responseDefaults);
55
59
  const baseOutcomes = cloneValueRecord(outcomes);
56
- applyTemplateProcessing(document, templateValues, responses, outcomes, correctResponses, random, customOperators, new Set(), baseResponses, baseOutcomes);
60
+ applyTemplateProcessing(document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, new Set(), baseResponses, baseResponseDefaults, baseOutcomes);
57
61
  if (priorState) {
58
62
  Object.assign(templateValues, priorTemplateValues);
59
63
  resetCorrectResponses(document, correctResponses);
60
- applyTemplateProcessing(document, templateValues, responses, outcomes, correctResponses, random, customOperators, new Set(Object.keys(priorTemplateValues)), baseResponses, baseOutcomes);
64
+ applyTemplateProcessing(document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, new Set(Object.keys(priorTemplateValues)), baseResponses, baseResponseDefaults, baseOutcomes);
61
65
  }
62
66
  const defaultOutcomes = cloneValueRecord(outcomes);
63
67
  Object.assign(responses, priorResponses);
@@ -90,7 +94,7 @@ export function createItemSession(document, priorState, options = {}) {
90
94
  },
91
95
  score() {
92
96
  const diagnostics = [];
93
- if (document.item.adaptive || status !== "initialized" || Object.keys(responses).length > 0) {
97
+ if (document.item.adaptive || status !== "initialized") {
94
98
  startAttempt();
95
99
  }
96
100
  const completionStatus = outcomes[COMPLETION_STATUS] ?? COMPLETION_NOT_ATTEMPTED;
@@ -110,6 +114,10 @@ export function createItemSession(document, priorState, options = {}) {
110
114
  },
111
115
  };
112
116
  function startAttempt() {
117
+ for (const [identifier, value] of Object.entries(responseDefaults)) {
118
+ if (responses[identifier] === undefined)
119
+ responses[identifier] = cloneValue(value);
120
+ }
113
121
  if (status === "initialized" || status === "suspended")
114
122
  status = "interacting";
115
123
  if (outcomes[COMPLETION_STATUS] === COMPLETION_NOT_ATTEMPTED) {
@@ -172,7 +180,7 @@ function assertCompatiblePriorState(document, priorState) {
172
180
  completionStatus !== COMPLETION_UNKNOWN &&
173
181
  completionStatus !== COMPLETION_COMPLETED &&
174
182
  completionStatus !== "incomplete") {
175
- throw new Error(`Cannot restore unsupported completionStatus ${String(completionStatus)}.`);
183
+ throw new Error(`Cannot restore unsupported completionStatus ${qtiValueToString(completionStatus)}.`);
176
184
  }
177
185
  }
178
186
  function assertKnownStateIdentifiers(kind, record, allowed) {
@@ -281,12 +289,12 @@ function attemptStateErrors(value) {
281
289
  }
282
290
  return errors;
283
291
  }
284
- function applyTemplateProcessing(document, templateValues, responses, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers = new Set(), baseResponses = cloneValueRecord(responses), baseOutcomes = cloneValueRecord(outcomes)) {
292
+ function applyTemplateProcessing(document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers = new Set(), baseResponses = cloneValueRecord(responses), baseResponseDefaults = cloneValueRecord(responseDefaults), baseOutcomes = cloneValueRecord(outcomes)) {
285
293
  const rules = document.item.templateProcessing?.rules ?? [];
286
294
  let restarts = 0;
287
295
  for (let index = 0; index < rules.length; index += 1) {
288
296
  const rule = rules[index];
289
- const shouldExit = applyTemplateRule(rule, document, templateValues, responses, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers);
297
+ const shouldExit = applyTemplateRule(rule, document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers);
290
298
  if (shouldExit)
291
299
  return;
292
300
  if (rule.type === "templateConstraint") {
@@ -294,6 +302,7 @@ function applyTemplateProcessing(document, templateValues, responses, outcomes,
294
302
  if (!satisfied) {
295
303
  resetTemplateValues(document, templateValues);
296
304
  resetRecord(responses, cloneValueRecord(baseResponses));
305
+ resetRecord(responseDefaults, cloneValueRecord(baseResponseDefaults));
297
306
  resetRecord(outcomes, cloneValueRecord(baseOutcomes));
298
307
  resetCorrectResponses(document, correctResponses);
299
308
  restarts += 1;
@@ -316,7 +325,7 @@ function resetCorrectResponses(document, correctResponses) {
316
325
  correctResponses[declaration.identifier] = cloneValue(declaration.correctResponse);
317
326
  }
318
327
  }
319
- function applyTemplateRule(rule, document, templateValues, responses, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers) {
328
+ function applyTemplateRule(rule, document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers) {
320
329
  if (rule.type === "exitTemplate")
321
330
  return true;
322
331
  if (rule.type === "templateConstraint")
@@ -335,7 +344,7 @@ function applyTemplateRule(rule, document, templateValues, responses, outcomes,
335
344
  }
336
345
  branch ??= rule.elseRules;
337
346
  for (const branchRule of branch) {
338
- const shouldExit = applyTemplateRule(branchRule, document, templateValues, responses, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers);
347
+ const shouldExit = applyTemplateRule(branchRule, document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers);
339
348
  if (shouldExit)
340
349
  return true;
341
350
  }
@@ -352,7 +361,12 @@ function applyTemplateRule(rule, document, templateValues, responses, outcomes,
352
361
  const responseDeclaration = getResponseDeclaration(document, rule.identifier);
353
362
  if (responseDeclaration) {
354
363
  const normalized = normalizeValueForCardinality(value, responseDeclaration.cardinality);
355
- responses[rule.identifier] = normalized;
364
+ if (normalized === null) {
365
+ delete responseDefaults[rule.identifier];
366
+ }
367
+ else {
368
+ responseDefaults[rule.identifier] = normalized;
369
+ }
356
370
  return false;
357
371
  }
358
372
  const outcomeDeclaration = document.item.outcomeDeclarations.find((declaration) => declaration.identifier === rule.identifier);
@@ -372,29 +386,37 @@ function applyResponseProcessing(document, responses, outcomes, templateValues,
372
386
  applyResponseRules(processing.rules, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
373
387
  return;
374
388
  }
375
- const template = processing?.template ?? "";
376
- if (template.includes("map_response")) {
377
- let score = 0;
378
- for (const declaration of document.item.responseDeclarations) {
379
- score += mapOrMatchResponse(declaration, responses[declaration.identifier] ?? null, correctResponses[declaration.identifier] ?? null);
380
- }
381
- outcomes.SCORE = score;
389
+ const templateKind = responseProcessingTemplateKind(processing?.template);
390
+ if (templateKind === "unsupported") {
382
391
  return;
383
392
  }
384
- let score = 0;
385
- let scored = false;
386
- for (const declaration of document.item.responseDeclarations) {
387
- const response = responses[declaration.identifier] ?? null;
388
- const correctResponse = correctResponses[declaration.identifier] ?? null;
389
- if (correctResponse !== null) {
390
- score += valuesEqual(response, correctResponse, declaration.cardinality === "ordered")
391
- ? 1
392
- : 0;
393
- scored = true;
394
- }
393
+ if (templateKind === "mapResponse" || templateKind === "mapResponsePoint") {
394
+ const declaration = getResponseDeclaration(document, "RESPONSE");
395
+ outcomes.SCORE = declaration
396
+ ? mapOrMatchResponse(declaration, responses.RESPONSE ?? null, correctResponses.RESPONSE ?? null)
397
+ : 0;
398
+ return;
395
399
  }
396
- if (scored)
397
- outcomes.SCORE = score;
400
+ if (templateKind === "matchCorrect") {
401
+ const declaration = getResponseDeclaration(document, "RESPONSE");
402
+ const matches = declaration
403
+ ? qtiMatchValues(responses.RESPONSE ?? null, correctResponses.RESPONSE ?? null, declaration.cardinality === "ordered")
404
+ : null;
405
+ outcomes.SCORE = matches === true ? 1 : 0;
406
+ }
407
+ }
408
+ function responseProcessingTemplateKind(template) {
409
+ if (!template)
410
+ return undefined;
411
+ const path = template.split(/[?#]/, 1)[0] ?? "";
412
+ const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
413
+ if (name === "match_correct")
414
+ return "matchCorrect";
415
+ if (name === "map_response")
416
+ return "mapResponse";
417
+ if (name === "map_response_point")
418
+ return "mapResponsePoint";
419
+ return "unsupported";
398
420
  }
399
421
  function applyResponseRules(rules, document, responses, outcomes, templateValues, correctResponses, random, customOperators) {
400
422
  for (const rule of rules) {
@@ -450,11 +472,13 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
450
472
  if (expression.type === "matchCorrect") {
451
473
  const declaration = getResponseDeclaration(document, expression.correctIdentifier);
452
474
  return declaration
453
- ? valuesEqual(responses[expression.identifier] ?? null, correctResponses[expression.correctIdentifier] ?? null, declaration.cardinality === "ordered")
475
+ ? qtiMatchValues(responses[expression.identifier] ?? null, correctResponses[expression.correctIdentifier] ?? null, declaration.cardinality === "ordered")
454
476
  : false;
455
477
  }
456
478
  if (expression.type === "match") {
457
- return valuesEqual(evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators), evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators), expressionIsOrdered(expression.left, document) ||
479
+ const left = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
480
+ const right = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
481
+ return qtiMatchValues(left, right, expressionIsOrdered(expression.left, document) ||
458
482
  expressionIsOrdered(expression.right, document));
459
483
  }
460
484
  if (expression.type === "mapResponse") {
@@ -510,37 +534,40 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
510
534
  return valueContainer(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)).length;
511
535
  }
512
536
  if (expression.type === "sum") {
513
- return expression.expressions.reduce((sum, item) => sum +
514
- numericValue(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)), 0);
537
+ const values = evaluateNumericOperands(expression.expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
538
+ return values ? values.reduce((sum, value) => sum + value, 0) : null;
515
539
  }
516
540
  if (expression.type === "product") {
517
- return expression.expressions.reduce((product, item) => product *
518
- numericValue(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)), 1);
541
+ const values = evaluateNumericOperands(expression.expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
542
+ return values ? values.reduce((product, value) => product * value, 1) : null;
519
543
  }
520
544
  if (expression.type === "min" || expression.type === "max") {
521
- const values = expression.expressions.flatMap((item) => valueContainer(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
522
- if (values.length === 0)
545
+ const values = evaluateNumericOperands(expression.expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
546
+ if (!values || values.length === 0)
523
547
  return null;
524
- const numericValues = values.map((value) => numericValue(value));
525
- return expression.type === "min" ? Math.min(...numericValues) : Math.max(...numericValues);
548
+ return expression.type === "min" ? Math.min(...values) : Math.max(...values);
526
549
  }
527
550
  if (expression.type === "subtract") {
528
- return (numericValue(evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators)) -
529
- numericValue(evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
551
+ const values = evaluateNumericOperands([expression.left, expression.right], document, responses, outcomes, templateValues, correctResponses, random, customOperators);
552
+ return values && values.length === 2 ? values[0] - values[1] : null;
530
553
  }
531
554
  if (expression.type === "divide") {
532
555
  const dividendValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
533
556
  const divisorValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
534
557
  if (dividendValue === null || divisorValue === null)
535
558
  return null;
536
- const divisor = numericValue(divisorValue);
537
- if (divisor === 0)
559
+ const dividend = numericValueOrNull(dividendValue);
560
+ const divisor = numericValueOrNull(divisorValue);
561
+ if (dividend === null || divisor === null || divisor === 0)
538
562
  return null;
539
- const quotient = numericValue(dividendValue) / divisor;
563
+ const quotient = dividend / divisor;
540
564
  return Number.isFinite(quotient) ? quotient : null;
541
565
  }
542
566
  if (expression.type === "power") {
543
- const value = Math.pow(numericValue(evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators)), numericValue(evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
567
+ const values = evaluateNumericOperands([expression.left, expression.right], document, responses, outcomes, templateValues, correctResponses, random, customOperators);
568
+ if (!values || values.length !== 2)
569
+ return null;
570
+ const value = Math.pow(values[0], values[1]);
544
571
  return Number.isFinite(value) ? value : null;
545
572
  }
546
573
  if (expression.type === "integerDivide") {
@@ -548,40 +575,53 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
548
575
  const divisorValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
549
576
  if (dividendValue === null || divisorValue === null)
550
577
  return null;
551
- const divisor = numericValue(divisorValue);
552
- if (divisor === 0)
578
+ const dividend = numericValueOrNull(dividendValue);
579
+ const divisor = numericValueOrNull(divisorValue);
580
+ if (dividend === null || divisor === null || divisor === 0)
553
581
  return null;
554
- return Math.floor(numericValue(dividendValue) / divisor);
582
+ return Math.floor(dividend / divisor);
555
583
  }
556
584
  if (expression.type === "integerModulus") {
557
585
  const dividendValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
558
586
  const divisorValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
559
587
  if (dividendValue === null || divisorValue === null)
560
588
  return null;
561
- const divisor = numericValue(divisorValue);
562
- if (divisor === 0)
589
+ const dividend = numericValueOrNull(dividendValue);
590
+ const divisor = numericValueOrNull(divisorValue);
591
+ if (dividend === null || divisor === null || divisor === 0)
563
592
  return null;
564
- const dividend = numericValue(dividendValue);
565
593
  return dividend - Math.floor(dividend / divisor) * divisor;
566
594
  }
567
595
  if (expression.type === "round") {
568
- return Math.round(numericValue(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
596
+ const value = numericValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
597
+ return value === null ? null : Math.round(value);
569
598
  }
570
599
  if (expression.type === "roundTo") {
571
- const value = numericValue(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
600
+ const value = numericValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
601
+ if (value === null)
602
+ return null;
572
603
  return expression.roundingMode === "decimalPlaces"
573
604
  ? roundToDecimalPlaces(value, expression.figures)
574
605
  : roundToSignificantFigures(value, expression.figures);
575
606
  }
576
607
  if (expression.type === "truncate") {
577
- return Math.trunc(numericValue(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
608
+ const value = numericValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
609
+ return value === null ? null : Math.trunc(value);
578
610
  }
579
611
  if (expression.type === "integerToFloat") {
580
612
  const value = evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
581
- return value === null ? null : numericValue(value);
613
+ return numericValueOrNull(value);
582
614
  }
583
615
  if (expression.type === "and") {
584
- return expression.expressions.every((item) => booleanValue(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
616
+ let sawNull = false;
617
+ for (const item of expression.expressions) {
618
+ const value = booleanValueOrNull(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
619
+ if (value === false)
620
+ return false;
621
+ if (value === null)
622
+ sawNull = true;
623
+ }
624
+ return sawNull ? null : true;
585
625
  }
586
626
  if (expression.type === "anyN") {
587
627
  const min = indexValue(expression.min, outcomes, templateValues) ?? 0;
@@ -596,21 +636,36 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
596
636
  return null;
597
637
  }
598
638
  if (expression.type === "or") {
599
- return expression.expressions.some((item) => booleanValue(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
639
+ let sawNull = false;
640
+ for (const item of expression.expressions) {
641
+ const value = booleanValueOrNull(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
642
+ if (value === true)
643
+ return true;
644
+ if (value === null)
645
+ sawNull = true;
646
+ }
647
+ return sawNull ? null : false;
600
648
  }
601
649
  if (expression.type === "not") {
602
- return !booleanValue(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
650
+ const value = booleanValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
651
+ return value === null ? null : !value;
603
652
  }
604
653
  if (expression.type === "equal") {
605
- return valuesEqual(evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators), evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
654
+ const left = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
655
+ const right = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
656
+ return left === null || right === null ? null : valuesEqual(left, right);
606
657
  }
607
658
  if (expression.type === "equalRounded") {
608
659
  const left = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
609
660
  const right = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
610
661
  if (left === null || right === null)
611
662
  return null;
612
- const roundedLeft = roundWithMode(numericValue(left), expression.roundingMode, expression.figures);
613
- const roundedRight = roundWithMode(numericValue(right), expression.roundingMode, expression.figures);
663
+ const leftNumber = numericValueOrNull(left);
664
+ const rightNumber = numericValueOrNull(right);
665
+ if (leftNumber === null || rightNumber === null)
666
+ return null;
667
+ const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
668
+ const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
614
669
  return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
615
670
  }
616
671
  if (expression.type === "numericCompare") {
@@ -618,8 +673,10 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
618
673
  const rightValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
619
674
  if (leftValue === null || rightValue === null)
620
675
  return null;
621
- const left = numericValue(leftValue);
622
- const right = numericValue(rightValue);
676
+ const left = numericValueOrNull(leftValue);
677
+ const right = numericValueOrNull(rightValue);
678
+ if (left === null || right === null)
679
+ return null;
623
680
  if (expression.operator === "lt")
624
681
  return left < right;
625
682
  if (expression.operator === "lte")
@@ -652,7 +709,7 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
652
709
  templateValues[expression.pattern] ??
653
710
  expression.pattern;
654
711
  try {
655
- return new RegExp(String(patternValue)).test(String(value));
712
+ return new RegExp(typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue)).test(qtiValueToString(value));
656
713
  }
657
714
  catch {
658
715
  return null;
@@ -665,20 +722,29 @@ function evaluateValue(expression, document, responses, outcomes = {}, templateV
665
722
  if (expression.type === "member") {
666
723
  const value = evaluateValue(expression.value, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
667
724
  const collection = evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
725
+ if (value === null || collection === null)
726
+ return null;
668
727
  const values = valueContainer(collection);
669
- return value === null ? null : values.some((item) => valuesEqual(item, value));
728
+ return values.some((item) => valuesEqual(item, value));
670
729
  }
671
730
  if (expression.type === "delete") {
672
731
  const value = evaluateValue(expression.value, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
673
- const collection = valueContainer(evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
674
- if (value === null || collection.length === 0)
732
+ const collectionValue = evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
733
+ if (value === null || collectionValue === null)
734
+ return null;
735
+ const collection = valueContainer(collectionValue);
736
+ if (collection.length === 0)
675
737
  return null;
676
738
  const filtered = collection.filter((item) => !valuesEqual(item, value));
677
739
  return filtered.length > 0 ? filtered : null;
678
740
  }
679
741
  if (expression.type === "contains") {
680
- const collection = valueContainer(evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
681
- const values = valueContainer(evaluateValue(expression.values, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
742
+ const collectionValue = evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
743
+ const valuesValue = evaluateValue(expression.values, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
744
+ if (collectionValue === null || valuesValue === null)
745
+ return null;
746
+ const collection = valueContainer(collectionValue);
747
+ const values = valueContainer(valuesValue);
682
748
  if (collection.length === 0 || values.length === 0)
683
749
  return null;
684
750
  return containsValues(collection, values);
@@ -817,7 +883,11 @@ function mapOrMatchResponse(declaration, response, correctResponse) {
817
883
  return valuesEqual(response, correctResponse, declaration.cardinality === "ordered") ? 1 : 0;
818
884
  }
819
885
  function scoreAreaMapping(response, areaMapping) {
820
- const points = Array.isArray(response) ? response : response === null ? [] : [String(response)];
886
+ const points = Array.isArray(response)
887
+ ? response.map(qtiScalarToString)
888
+ : response === null
889
+ ? []
890
+ : qtiValueToStringList(response);
821
891
  let score = 0;
822
892
  for (const point of points) {
823
893
  const parsed = parsePoint(String(point));
@@ -924,7 +994,9 @@ function scoreMapping(response, mapping) {
924
994
  const score = response.reduce((sum, value) => sum + (values[String(value)] ?? mapping.defaultValue), 0);
925
995
  return clampMappedScore(score, mapping.attributes);
926
996
  }
927
- const score = typeof response === "string" ? (values[response] ?? mapping.defaultValue) : 0;
997
+ const score = response === null || isRecordValue(response)
998
+ ? 0
999
+ : (values[String(response)] ?? mapping.defaultValue);
928
1000
  return clampMappedScore(score, mapping.attributes);
929
1001
  }
930
1002
  function clampMappedScore(score, attributes) {
@@ -966,6 +1038,11 @@ function valuesEqual(actual, expected, ordered = false) {
966
1038
  }
967
1039
  return scalarValuesEqual(actual, expected);
968
1040
  }
1041
+ function qtiMatchValues(actual, expected, ordered = false) {
1042
+ if (actual === null || expected === null)
1043
+ return null;
1044
+ return valuesEqual(actual, expected, ordered);
1045
+ }
969
1046
  function scalarValuesEqual(actual, expected) {
970
1047
  if (typeof actual === "boolean" && typeof expected === "string") {
971
1048
  return String(actual) === expected;
@@ -1058,8 +1135,24 @@ function indexValue(n, outcomes, templateValues) {
1058
1135
  if (Number.isInteger(parsed))
1059
1136
  return parsed;
1060
1137
  const value = outcomes[n] ?? templateValues[n] ?? null;
1061
- const numeric = numericValue(value);
1062
- return Number.isInteger(numeric) ? numeric : undefined;
1138
+ const numeric = numericValueOrNull(value);
1139
+ return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
1140
+ }
1141
+ function evaluateNumericOperands(expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators) {
1142
+ const numericValues = [];
1143
+ for (const expression of expressions) {
1144
+ const value = evaluateValue(expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
1145
+ if (value === null || isRecordValue(value))
1146
+ return null;
1147
+ const values = Array.isArray(value) ? value : [value];
1148
+ for (const item of values) {
1149
+ const numeric = numericValueOrNull(item);
1150
+ if (numeric === null)
1151
+ return null;
1152
+ numericValues.push(numeric);
1153
+ }
1154
+ }
1155
+ return numericValues;
1063
1156
  }
1064
1157
  function containsValues(collection, values) {
1065
1158
  const remaining = [...collection];
@@ -1083,6 +1176,17 @@ function numericValue(value) {
1083
1176
  return Number(value);
1084
1177
  return 0;
1085
1178
  }
1179
+ function numericValueOrNull(value) {
1180
+ if (typeof value === "number")
1181
+ return Number.isFinite(value) ? value : null;
1182
+ if (typeof value === "boolean")
1183
+ return value ? 1 : 0;
1184
+ if (typeof value === "string") {
1185
+ const parsed = Number(value);
1186
+ return Number.isFinite(parsed) ? parsed : null;
1187
+ }
1188
+ return null;
1189
+ }
1086
1190
  function durationSeconds(value) {
1087
1191
  if (value === null || Array.isArray(value) || isRecordValue(value))
1088
1192
  return null;
@@ -1117,6 +1221,11 @@ function booleanValue(value) {
1117
1221
  return value.length > 0;
1118
1222
  return false;
1119
1223
  }
1224
+ function booleanValueOrNull(value) {
1225
+ if (value === null)
1226
+ return null;
1227
+ return booleanValue(value);
1228
+ }
1120
1229
  function roundToDecimalPlaces(value, figures) {
1121
1230
  const factor = 10 ** figures;
1122
1231
  return Math.round(value * factor) / factor;
@@ -1258,8 +1367,10 @@ function finiteOrNull(value) {
1258
1367
  return Number.isFinite(value) ? value : null;
1259
1368
  }
1260
1369
  function stringMatch(left, right, caseSensitive, substring) {
1261
- let actual = String(left ?? "");
1262
- let expected = String(right ?? "");
1370
+ if (left === null || right === null)
1371
+ return null;
1372
+ let actual = qtiValueToString(left);
1373
+ let expected = qtiValueToString(right);
1263
1374
  if (!caseSensitive) {
1264
1375
  actual = actual.toLocaleLowerCase();
1265
1376
  expected = expected.toLocaleLowerCase();