@m2c2kit/assessment-color-shapes 0.8.32 → 0.8.34

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/index.js CHANGED
@@ -11,6 +11,72 @@ class M2Error extends Error {
11
11
  }
12
12
  }
13
13
  }
14
+ const CHAIN_REGISTRY = /* @__PURE__ */ new Map();
15
+ let CHAIN_COUNTER = 0;
16
+ function getChainOps(id) {
17
+ return CHAIN_REGISTRY.get(id);
18
+ }
19
+ function clearChainOps(id) {
20
+ CHAIN_REGISTRY.delete(id);
21
+ }
22
+ function makeChainFn() {
23
+ const fn = ((dataCalc) => {
24
+ if (!dataCalc) return void 0;
25
+ let current = dataCalc;
26
+ for (const op of fn.ops) {
27
+ const method = current[op.name];
28
+ if (typeof method !== "function") {
29
+ throw new M2Error(
30
+ `chain: method ${op.name} does not exist on DataCalc`
31
+ );
32
+ }
33
+ const res = method.apply(current, op.args);
34
+ if (res instanceof DataCalc) {
35
+ current = res;
36
+ continue;
37
+ }
38
+ return res;
39
+ }
40
+ return void 0;
41
+ });
42
+ fn.ops = [];
43
+ const record = (name, args) => {
44
+ fn.ops.push({ name, args });
45
+ return fn;
46
+ };
47
+ fn.arrange = (...variables) => record("arrange", variables);
48
+ fn.slice = (start, end) => record("slice", [start, end]);
49
+ fn.pull = (variable) => record("pull", [variable]);
50
+ fn.filter = (predicate) => record("filter", [predicate]);
51
+ fn.mutate = (mutations) => record("mutate", [mutations]);
52
+ fn.select = (...variables) => record("select", variables);
53
+ fn.groupBy = (...groups) => record("groupBy", groups);
54
+ fn.ungroup = () => record("ungroup", []);
55
+ fn.rename = (renames) => record("rename", [renames]);
56
+ fn.distinct = () => record("distinct", []);
57
+ Object.defineProperty(fn, "length", {
58
+ configurable: true,
59
+ get: function() {
60
+ try {
61
+ const id = `c${++CHAIN_COUNTER}`;
62
+ CHAIN_REGISTRY.set(
63
+ id,
64
+ (fn.ops || []).map((o) => ({ ...o }))
65
+ );
66
+ return `__CHAIN_EXPR__[${id}]`;
67
+ } catch (err) {
68
+ return void 0;
69
+ }
70
+ }
71
+ });
72
+ return fn;
73
+ }
74
+ function arrange(...variables) {
75
+ return makeChainFn().arrange(...variables);
76
+ }
77
+ function filter(predicate) {
78
+ return makeChainFn().filter(predicate);
79
+ }
14
80
  class DataCalc {
15
81
  /**
16
82
  * A class for transformation and calculation of m2c2kit data.
@@ -34,6 +100,7 @@ class DataCalc {
34
100
  */
35
101
  constructor(data, options) {
36
102
  this._groups = new Array();
103
+ this._warnings = false;
37
104
  if (!Array.isArray(data)) {
38
105
  throw new M2Error(
39
106
  "DataCalc constructor expects an array of observations as first argument"
@@ -63,6 +130,9 @@ class DataCalc {
63
130
  if (options?.groups) {
64
131
  this._groups = Array.from(options.groups);
65
132
  }
133
+ if (options?.warnings) {
134
+ this._warnings = true;
135
+ }
66
136
  }
67
137
  /**
68
138
  * Returns the groups in the data.
@@ -89,7 +159,8 @@ class DataCalc {
89
159
  * Returns a single variable from the data.
90
160
  *
91
161
  * @remarks If the variable length is 1, the value is returned. If the
92
- * variable has length > 1, an array of values is returned.
162
+ * variable has length > 1, an array of values is returned. If an empty
163
+ * dataset is provided, `null` is returned and a warning is logged.
93
164
  *
94
165
  * @param variable - Name of variable to pull from the data
95
166
  * @returns the value of the variable
@@ -105,9 +176,11 @@ class DataCalc {
105
176
  */
106
177
  pull(variable) {
107
178
  if (this._observations.length === 0) {
108
- console.warn(
109
- `DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
110
- );
179
+ if (this._warnings) {
180
+ console.warn(
181
+ `DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
182
+ );
183
+ }
111
184
  return null;
112
185
  }
113
186
  this.verifyObservationsContainVariable(variable);
@@ -167,7 +240,7 @@ class DataCalc {
167
240
  this._observations.filter(
168
241
  predicate
169
242
  ),
170
- { groups: this._groups }
243
+ { groups: this._groups, warnings: this._warnings }
171
244
  );
172
245
  }
173
246
  /**
@@ -176,7 +249,8 @@ class DataCalc {
176
249
  * @remarks This is used with the `summarize()` method to calculate summaries
177
250
  * by group.
178
251
  *
179
- * @param groups - variable names to group by
252
+ * @param groups - variable names to group by. Grouping variables must be
253
+ * primitive values (string, number, boolean).
180
254
  * @returns A new `DataCalc` object with the observations grouped by one or
181
255
  * more variables
182
256
  *
@@ -197,6 +271,16 @@ class DataCalc {
197
271
  groupBy(...groups) {
198
272
  groups.forEach((group) => {
199
273
  this.verifyObservationsContainVariable(group);
274
+ for (let i = 0; i < this._observations.length; i++) {
275
+ const val = this._observations[i][group];
276
+ if (val === null) continue;
277
+ const t = typeof val;
278
+ if (t !== "number" && t !== "string" && t !== "boolean") {
279
+ throw new M2Error(
280
+ `groupBy(): variable "${group}" contains non-primitive value at index ${i} (type=${t}). Only number, string, boolean, or null are allowed for grouping.`
281
+ );
282
+ }
283
+ }
200
284
  });
201
285
  return new DataCalc(this._observations, { groups });
202
286
  }
@@ -250,7 +334,10 @@ class DataCalc {
250
334
  }
251
335
  return newObservation;
252
336
  });
253
- return new DataCalc(newObservations, { groups: this._groups });
337
+ return new DataCalc(newObservations, {
338
+ groups: this._groups,
339
+ warnings: this._warnings
340
+ });
254
341
  }
255
342
  /**
256
343
  * Calculates summaries of the data.
@@ -287,6 +374,31 @@ class DataCalc {
287
374
  * );
288
375
  * // [ { filteredTotalC: 10 } ]
289
376
  * ```
377
+ *
378
+ * @remarks Within a `summarize()` call, regular arithmetic operations
379
+ * (+, -, *, /, etc.) must not be used directly on `DataCalc` objects
380
+ * or summary functions. This is not allowed:
381
+ *
382
+ * @example
383
+ * ```js
384
+ * dc.summarize({
385
+ * meanA: mean("a"),
386
+ * // The below will cause an error because it attempts to add a number
387
+ * // to a DataCalc object, which is not supported
388
+ * totalBPlus5: sum("b") + 5
389
+ * })
390
+ * ```
391
+ *
392
+ * Instead, use the `DataCalc` helper functions such as `add` to perform
393
+ * calculations within summary results. For example, to add 5 to the sum
394
+ * of "b", you can do:
395
+ * @example
396
+ * ```js
397
+ * dc.summarize({
398
+ * meanA: mean("a"),
399
+ * totalBPlus5: sum("b").add(5)
400
+ * })
401
+ * ```
290
402
  */
291
403
  summarize(summarizations) {
292
404
  if (this._groups.length === 0) {
@@ -299,20 +411,103 @@ class DataCalc {
299
411
  summarizeOperation.parameters,
300
412
  summarizeOperation.options
301
413
  );
414
+ } else if (typeof value === "function") {
415
+ try {
416
+ const res = value(this);
417
+ if (typeof res === "function") {
418
+ throw new M2Error(
419
+ `summarize(): lazy callback for ${newVariable} returned a function; expected a value or array of values.`
420
+ );
421
+ }
422
+ if (res instanceof DataCalc) {
423
+ throw new M2Error(
424
+ `summarize(): lazy callback for ${newVariable} returned a DataCalc; expected a value or array of values.`
425
+ );
426
+ }
427
+ obs[newVariable] = res;
428
+ } catch (err) {
429
+ throw new M2Error(
430
+ `summarize(): lazy callback for ${newVariable} threw an error: ${err && err.message ? err.message : String(err)}`
431
+ );
432
+ }
302
433
  } else {
434
+ if (typeof value === "string") {
435
+ const re = /__CHAIN_EXPR__\[(.*?)\]/g;
436
+ const matches = Array.from(value.matchAll(re));
437
+ if (matches.length > 0) {
438
+ let sum2 = 0;
439
+ for (const m of matches) {
440
+ const payload = m[1];
441
+ let ops = getChainOps(payload);
442
+ if (!ops) {
443
+ try {
444
+ ops = JSON.parse(decodeURIComponent(payload));
445
+ } catch (err) {
446
+ throw new M2Error(
447
+ `summarize(): failed to parse chain payload for ${newVariable}`
448
+ );
449
+ }
450
+ }
451
+ let current = this;
452
+ let evaluated = void 0;
453
+ if (!ops) {
454
+ throw new M2Error(
455
+ `summarize(): empty chain ops for ${newVariable}`
456
+ );
457
+ }
458
+ for (const op of ops) {
459
+ const method = current[op.name];
460
+ if (typeof method !== "function") {
461
+ throw new M2Error(
462
+ `summarize(): chain method ${op.name} does not exist on DataCalc`
463
+ );
464
+ }
465
+ const res = method.apply(current, op.args);
466
+ if (res instanceof DataCalc) {
467
+ current = res;
468
+ continue;
469
+ }
470
+ if (Array.isArray(res)) {
471
+ evaluated = res.length;
472
+ break;
473
+ }
474
+ if (typeof res === "boolean") {
475
+ evaluated = res ? 1 : 0;
476
+ break;
477
+ }
478
+ evaluated = typeof res === "number" ? res : Number(res);
479
+ break;
480
+ }
481
+ if (evaluated === void 0) {
482
+ evaluated = current instanceof DataCalc ? current.length : 0;
483
+ }
484
+ sum2 += evaluated;
485
+ }
486
+ let leftover = value;
487
+ for (const m of matches) leftover = leftover.replace(m[0], "");
488
+ const leftoverNum = Number(leftover);
489
+ if (!Number.isNaN(leftoverNum)) sum2 += leftoverNum;
490
+ obs[newVariable] = sum2;
491
+ for (const m of matches) {
492
+ clearChainOps(m[1]);
493
+ }
494
+ continue;
495
+ }
496
+ }
303
497
  obs[newVariable] = value;
304
498
  }
305
499
  }
306
- return new DataCalc([obs], { groups: this._groups });
500
+ return new DataCalc([obs], {
501
+ groups: this._groups,
502
+ warnings: this._warnings
503
+ });
307
504
  }
308
505
  return this.summarizeByGroups(summarizations);
309
506
  }
310
507
  summarizeByGroups(summarizations) {
311
508
  const groupMap = /* @__PURE__ */ new Map();
312
509
  this._observations.forEach((obs) => {
313
- const groupKey = this._groups.map(
314
- (g) => typeof obs[g] === "object" ? JSON.stringify(obs[g]) : obs[g]
315
- ).join("|");
510
+ const groupKey = this._groups.map((g) => String(obs[g])).join("|");
316
511
  if (!groupMap.has(groupKey)) {
317
512
  groupMap.set(groupKey, []);
318
513
  }
@@ -330,21 +525,17 @@ class DataCalc {
330
525
  const summaryObj = {};
331
526
  this._groups.forEach((group, i) => {
332
527
  const valueStr = groupValues[i];
333
- const originalType = typeof firstObs[group];
334
- if (originalType === "number") {
335
- summaryObj[group] = Number(valueStr);
336
- } else if (originalType === "boolean") {
337
- summaryObj[group] = valueStr === "true";
338
- } else if (valueStr.startsWith("{") || valueStr.startsWith("[")) {
339
- try {
340
- summaryObj[group] = JSON.parse(valueStr);
341
- } catch {
342
- throw new M2Error(
343
- `Failed to parse group value ${valueStr} as JSON for group ${group}`
344
- );
345
- }
528
+ if (firstObs[group] === null) {
529
+ summaryObj[group] = null;
346
530
  } else {
347
- summaryObj[group] = valueStr;
531
+ const originalType = typeof firstObs[group];
532
+ if (originalType === "number") {
533
+ summaryObj[group] = Number(valueStr);
534
+ } else if (originalType === "boolean") {
535
+ summaryObj[group] = valueStr === "true";
536
+ } else {
537
+ summaryObj[group] = valueStr;
538
+ }
348
539
  }
349
540
  });
350
541
  const groupDataCalc = new DataCalc(groupObs);
@@ -356,13 +547,98 @@ class DataCalc {
356
547
  summarizeOperation.parameters,
357
548
  summarizeOperation.options
358
549
  );
550
+ } else if (typeof value === "function") {
551
+ try {
552
+ const res = value(groupDataCalc);
553
+ if (typeof res === "function") {
554
+ throw new M2Error(
555
+ `summarize(): lazy callback for ${newVariable} returned a function; expected a value or array of values.`
556
+ );
557
+ }
558
+ if (res instanceof DataCalc) {
559
+ throw new M2Error(
560
+ `summarize(): lazy callback for ${newVariable} returned a DataCalc; expected a value or array of values.`
561
+ );
562
+ }
563
+ summaryObj[newVariable] = res;
564
+ } catch (err) {
565
+ throw new M2Error(
566
+ `summarize(): lazy callback for ${newVariable} threw an error: ${err && err.message ? err.message : String(err)}`
567
+ );
568
+ }
359
569
  } else {
570
+ if (typeof value === "string") {
571
+ const re = /__CHAIN_EXPR__\[(.*?)\]/g;
572
+ const matches = Array.from(value.matchAll(re));
573
+ if (matches.length > 0) {
574
+ let sum2 = 0;
575
+ for (const m of matches) {
576
+ const payload = m[1];
577
+ let ops = getChainOps(payload);
578
+ if (!ops) {
579
+ try {
580
+ ops = JSON.parse(decodeURIComponent(payload));
581
+ } catch (err) {
582
+ throw new M2Error(
583
+ `summarize(): failed to parse chain payload for ${newVariable}`
584
+ );
585
+ }
586
+ }
587
+ let current = groupDataCalc;
588
+ let evaluated = void 0;
589
+ if (!ops) {
590
+ throw new M2Error(
591
+ `summarize(): empty chain ops for ${newVariable}`
592
+ );
593
+ }
594
+ for (const op of ops) {
595
+ const method = current[op.name];
596
+ if (typeof method !== "function") {
597
+ throw new M2Error(
598
+ `summarize(): chain method ${op.name} does not exist on DataCalc`
599
+ );
600
+ }
601
+ const res = method.apply(current, op.args);
602
+ if (res instanceof DataCalc) {
603
+ current = res;
604
+ continue;
605
+ }
606
+ if (Array.isArray(res)) {
607
+ evaluated = res.length;
608
+ break;
609
+ }
610
+ if (typeof res === "boolean") {
611
+ evaluated = res ? 1 : 0;
612
+ break;
613
+ }
614
+ evaluated = typeof res === "number" ? res : Number(res);
615
+ break;
616
+ }
617
+ if (evaluated === void 0) {
618
+ evaluated = current instanceof DataCalc ? current.length : 0;
619
+ }
620
+ sum2 += evaluated;
621
+ }
622
+ let leftover = value;
623
+ for (const m of matches) leftover = leftover.replace(m[0], "");
624
+ const leftoverNum = Number(leftover);
625
+ if (!Number.isNaN(leftoverNum)) sum2 += leftoverNum;
626
+ summaryObj[newVariable] = sum2;
627
+ for (const m of matches) {
628
+ clearChainOps(m[1]);
629
+ }
630
+ continue;
631
+ }
632
+ }
360
633
  summaryObj[newVariable] = value;
361
634
  }
362
635
  }
363
636
  summarizedObservations.push(summaryObj);
364
637
  });
365
- return new DataCalc(summarizedObservations, { groups: this._groups });
638
+ return new DataCalc(summarizedObservations, {
639
+ groups: this._groups,
640
+ warnings: this._warnings
641
+ });
366
642
  }
367
643
  /**
368
644
  * Selects specific variables to keep in the dataset.
@@ -415,7 +691,10 @@ class DataCalc {
415
691
  }
416
692
  return newObservation;
417
693
  });
418
- return new DataCalc(newObservations, { groups: this._groups });
694
+ return new DataCalc(newObservations, {
695
+ groups: this._groups,
696
+ warnings: this._warnings
697
+ });
419
698
  }
420
699
  /**
421
700
  * Arranges (sorts) the observations based on one or more variables.
@@ -467,7 +746,10 @@ class DataCalc {
467
746
  }
468
747
  return 0;
469
748
  });
470
- return new DataCalc(sortedObservations, { groups: this._groups });
749
+ return new DataCalc(sortedObservations, {
750
+ groups: this._groups,
751
+ warnings: this._warnings
752
+ });
471
753
  }
472
754
  /**
473
755
  * Keeps only unique/distinct observations.
@@ -496,11 +778,19 @@ class DataCalc {
496
778
  seen.add(key);
497
779
  return true;
498
780
  });
499
- return new DataCalc(uniqueObs, { groups: this._groups });
781
+ return new DataCalc(uniqueObs, {
782
+ groups: this._groups,
783
+ warnings: this._warnings
784
+ });
500
785
  }
501
786
  /**
502
787
  * Renames variables in the observations.
503
788
  *
789
+ * @remarks If a target name in `renames` already exists in the dataset (and is not
790
+ * simply the source name being renamed), the rename will overwrite the existing
791
+ * variable. When warnings are enabled, a warning will be logged to make users aware
792
+ * of the potential overwrite.
793
+ *
504
794
  * @param renames - Object mapping new variable names to old variable names
505
795
  * @returns A new DataCalc object with renamed variables
506
796
  *
@@ -522,21 +812,37 @@ class DataCalc {
522
812
  Object.values(renames).forEach((oldName) => {
523
813
  this.verifyObservationsContainVariable(oldName);
524
814
  });
815
+ if (this._observations.length > 0) {
816
+ const existingKeys = new Set(Object.keys(this._observations[0]));
817
+ const oldNames = new Set(Object.values(renames));
818
+ const collisions = Object.keys(renames).filter(
819
+ (n2) => existingKeys.has(n2) && !oldNames.has(n2)
820
+ );
821
+ if (collisions.length > 0 && this._warnings) {
822
+ console.warn(
823
+ `DataCalc.rename(): renaming will overwrite existing variables: ${collisions.join(", ")}`
824
+ );
825
+ }
826
+ }
525
827
  const newObservations = this._observations.map((observation) => {
526
828
  const newObservation = {};
829
+ const newNames = new Set(Object.keys(renames));
527
830
  for (const [key, value] of Object.entries(observation)) {
528
831
  const newKey = Object.entries(renames).find(
529
832
  ([, old]) => old === key
530
833
  )?.[0];
531
834
  if (newKey) {
532
835
  newObservation[newKey] = value;
533
- } else if (!Object.values(renames).includes(key)) {
836
+ } else if (!newNames.has(key)) {
534
837
  newObservation[key] = value;
535
838
  }
536
839
  }
537
840
  return newObservation;
538
841
  });
539
- return new DataCalc(newObservations, { groups: this._groups });
842
+ return new DataCalc(newObservations, {
843
+ groups: this._groups,
844
+ warnings: this._warnings
845
+ });
540
846
  }
541
847
  /**
542
848
  * Performs an inner join with another DataCalc object.
@@ -876,7 +1182,10 @@ class DataCalc {
876
1182
  }
877
1183
  let sliced;
878
1184
  if (start >= this._observations.length) {
879
- return new DataCalc([], { groups: this._groups });
1185
+ return new DataCalc([], {
1186
+ groups: this._groups,
1187
+ warnings: this._warnings
1188
+ });
880
1189
  }
881
1190
  if (end === void 0) {
882
1191
  const index = start < 0 ? this._observations.length + start : start;
@@ -884,7 +1193,10 @@ class DataCalc {
884
1193
  } else {
885
1194
  sliced = this._observations.slice(start, end);
886
1195
  }
887
- return new DataCalc(sliced, { groups: this._groups });
1196
+ return new DataCalc(sliced, {
1197
+ groups: this._groups,
1198
+ warnings: this._warnings
1199
+ });
888
1200
  }
889
1201
  /**
890
1202
  * Combines observations from two DataCalc objects by rows.
@@ -1046,6 +1358,26 @@ class DataCalc {
1046
1358
  * @returns a deep copy of the object
1047
1359
  */
1048
1360
  deepCopy(source, map = /* @__PURE__ */ new WeakMap()) {
1361
+ const sClone = globalThis.structuredClone;
1362
+ if (typeof sClone === "function") {
1363
+ try {
1364
+ const proto = source && typeof source === "object" ? Object.getPrototypeOf(source) : null;
1365
+ const isPlain = Array.isArray(source) || proto === Object.prototype || proto === null;
1366
+ if (isPlain) {
1367
+ const descriptors = Object.getOwnPropertyDescriptors(
1368
+ source
1369
+ );
1370
+ const hasAccessors = Object.values(descriptors).some(
1371
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1372
+ (d) => typeof d.get === "function" || typeof d.set === "function"
1373
+ );
1374
+ if (!hasAccessors) {
1375
+ return sClone(source);
1376
+ }
1377
+ }
1378
+ } catch {
1379
+ }
1380
+ }
1049
1381
  if (source === null || typeof source !== "object") {
1050
1382
  return source;
1051
1383
  }
@@ -1064,8 +1396,9 @@ class DataCalc {
1064
1396
  key
1065
1397
  );
1066
1398
  if (descriptor) {
1399
+ const { get, set, ...descWithoutAccessors } = descriptor;
1067
1400
  Object.defineProperty(copy, key, {
1068
- ...descriptor,
1401
+ ...descWithoutAccessors,
1069
1402
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1070
1403
  value: this.deepCopy(source[key], map)
1071
1404
  });
@@ -1085,7 +1418,613 @@ class DataCalc {
1085
1418
  return keys.some((key) => obs[key] === null || obs[key] === void 0);
1086
1419
  }
1087
1420
  }
1088
- console.log("\u26AA @m2c2kit/data-calc version 0.8.5 (d289d12c)");
1421
+ const PRECEDENCE = {
1422
+ "+": 1,
1423
+ "-": 1,
1424
+ "*": 2,
1425
+ "/": 2,
1426
+ "^": 3
1427
+ };
1428
+ class SummarizeOperation {
1429
+ constructor(leafFn, parameters, options, tokens) {
1430
+ this.leafFn = leafFn;
1431
+ this.parameters = parameters;
1432
+ this.options = options;
1433
+ if (tokens && tokens.length > 0) {
1434
+ this.tokens = tokens.slice();
1435
+ } else if (leafFn) {
1436
+ this.tokens = [{ t: "operand", v: this }];
1437
+ } else {
1438
+ this.tokens = [];
1439
+ }
1440
+ this.summarizeFunction = (dc) => {
1441
+ return this.evaluateAsValue(dc);
1442
+ };
1443
+ }
1444
+ // Factory for creating a leaf SummarizeOperation (use in helpers)
1445
+ static leaf(leafFn, parameters, options) {
1446
+ return new SummarizeOperation(leafFn, parameters, options);
1447
+ }
1448
+ // clone with new token stream (immutable-ish)
1449
+ cloneWithTokens(newTokens) {
1450
+ return new SummarizeOperation(void 0, void 0, void 0, newTokens);
1451
+ }
1452
+ // append operator + operand (operand can be number or SummarizeOperation)
1453
+ appendOp(op, operand) {
1454
+ const newTokens = this.tokens.slice();
1455
+ newTokens.push({ t: "op", v: op });
1456
+ newTokens.push({ t: "operand", v: operand });
1457
+ return this.cloneWithTokens(newTokens);
1458
+ }
1459
+ /**
1460
+ * Append addition to this expression.
1461
+ *
1462
+ * @param x - A numeric literal or another `SummarizeOperation` to add to this expression
1463
+ * @returns A new `SummarizeOperation` representing `(this + x)`
1464
+ *
1465
+ * @example
1466
+ * ```js
1467
+ * const d = [
1468
+ * { a: 1, b: 2, c: 3 },
1469
+ * { a: 0, b: 8, c: 3 },
1470
+ * { a: 9, b: 4, c: 7 },
1471
+ * ];
1472
+ * const dc = new DataCalc(d);
1473
+ * console.log(
1474
+ * dc.summarize({
1475
+ * result: mean("a").add(10),
1476
+ * }).observations
1477
+ * );
1478
+ * // [ { result: 13.33333 } ]
1479
+ * ```
1480
+ */
1481
+ add(x) {
1482
+ return this.appendOp("+", x);
1483
+ }
1484
+ /**
1485
+ * Append subtraction to this expression.
1486
+ *
1487
+ * @param x - A numeric literal or another `SummarizeOperation` to subtract from this expression
1488
+ * @returns A new `SummarizeOperation` representing `(this - x)`
1489
+ *
1490
+ * @example
1491
+ * ```js
1492
+ * const d = [
1493
+ * { a: 1, b: 2, c: 3 },
1494
+ * { a: 0, b: 8, c: 3 },
1495
+ * { a: 9, b: 4, c: 7 },
1496
+ * ];
1497
+ * const dc = new DataCalc(d);
1498
+ * console.log(
1499
+ * dc.summarize({
1500
+ * result: mean("a").sub(10),
1501
+ * }).observations
1502
+ * );
1503
+ * // [ { result: -6.6667 } ]
1504
+ * ```
1505
+ */
1506
+ sub(x) {
1507
+ return this.appendOp("-", x);
1508
+ }
1509
+ /**
1510
+ * Append multiplication to this expression. Multiplication has higher
1511
+ * precedence than addition/subtraction.
1512
+ *
1513
+ * @param x - A numeric literal or another `SummarizeOperation` to multiply with this expression
1514
+ * @returns A new `SummarizeOperation` representing `(this * x)`
1515
+ *
1516
+ * @example
1517
+ * ```js
1518
+ * const d = [
1519
+ * { a: 1, b: 2, c: 3 },
1520
+ * { a: 0, b: 8, c: 3 },
1521
+ * { a: 9, b: 4, c: 7 },
1522
+ * ];
1523
+ * const dc = new DataCalc(d);
1524
+ * console.log(
1525
+ * dc.summarize({
1526
+ * result: mean("a").mul(10),
1527
+ * }).observations
1528
+ * );
1529
+ * // [ { result: 33.3333 } ]
1530
+ * ```
1531
+ */
1532
+ mul(x) {
1533
+ return this.appendOp("*", x);
1534
+ }
1535
+ /**
1536
+ * Append division to this expression.
1537
+ *
1538
+ * @param x - A numeric literal or another `SummarizeOperation` to divide this expression by
1539
+ * @returns A new `SummarizeOperation` representing `(this / x)`
1540
+ *
1541
+ * @example
1542
+ * ```js
1543
+ * const d = [
1544
+ * { a: 1, b: 2, c: 3 },
1545
+ * { a: 0, b: 8, c: 3 },
1546
+ * { a: 9, b: 4, c: 7 },
1547
+ * ];
1548
+ * const dc = new DataCalc(d);
1549
+ * console.log(
1550
+ * dc.summarize({
1551
+ * result: mean("a").div(10),
1552
+ * }).observations
1553
+ * );
1554
+ * // [ { result: .3333 } ]
1555
+ * ```
1556
+ */
1557
+ div(x) {
1558
+ return this.appendOp("/", x);
1559
+ }
1560
+ /**
1561
+ * Append exponentiation (power) to this expression.
1562
+ *
1563
+ * Note: exponentiation uses right-associative semantics (a ^ b ^ c -> a ^ (b ^ c)).
1564
+ *
1565
+ * @param x - A numeric literal or another `SummarizeOperation` used as the exponent
1566
+ * @returns A new `SummarizeOperation` representing `(this ^ x)`
1567
+ *
1568
+ * @example
1569
+ * ```js
1570
+ * const d = [
1571
+ * { a: 1, b: 2, c: 3 },
1572
+ * { a: 0, b: 8, c: 3 },
1573
+ * { a: 9, b: 4, c: 7 },
1574
+ * ];
1575
+ * const dc = new DataCalc(d);
1576
+ * console.log(
1577
+ * dc.summarize({
1578
+ * result: mean("a").pow(2),
1579
+ * }).observations
1580
+ * );
1581
+ * // [ { result: 11.1111 } ]
1582
+ * ```
1583
+ */
1584
+ pow(x) {
1585
+ return this.appendOp("^", x);
1586
+ }
1587
+ // Evaluate an operand token to a number (returns NaN for non-numeric)
1588
+ evaluateOperandToNumber(opd, dc) {
1589
+ if (!this._usedChainIds) this._usedChainIds = /* @__PURE__ */ new Set();
1590
+ if (typeof opd === "number") return opd;
1591
+ if (typeof opd === "string") {
1592
+ const re = /^__CHAIN_EXPR__\[(.*?)\]$/;
1593
+ const m = re.exec(opd);
1594
+ if (m) {
1595
+ const payload = m[1];
1596
+ let ops = getChainOps(payload);
1597
+ if (ops) this._usedChainIds?.add(payload);
1598
+ if (!ops) {
1599
+ try {
1600
+ ops = JSON.parse(decodeURIComponent(payload));
1601
+ } catch {
1602
+ ops = void 0;
1603
+ }
1604
+ }
1605
+ if (!ops) return NaN;
1606
+ let current = dc;
1607
+ let evaluated = void 0;
1608
+ for (const op of ops) {
1609
+ const method = current[op.name];
1610
+ if (typeof method !== "function") return NaN;
1611
+ const res = method.apply(current, op.args);
1612
+ if (res instanceof DataCalc) {
1613
+ current = res;
1614
+ continue;
1615
+ }
1616
+ if (Array.isArray(res)) {
1617
+ evaluated = res.length;
1618
+ break;
1619
+ }
1620
+ if (typeof res === "boolean") {
1621
+ const coerce = this.options && typeof this.options.coerceBooleans === "boolean" ? this.options.coerceBooleans : true;
1622
+ if (coerce) {
1623
+ evaluated = res ? 1 : 0;
1624
+ break;
1625
+ } else {
1626
+ return NaN;
1627
+ }
1628
+ }
1629
+ evaluated = typeof res === "number" ? res : Number(res);
1630
+ break;
1631
+ }
1632
+ if (evaluated === void 0)
1633
+ evaluated = current instanceof DataCalc ? current.length : NaN;
1634
+ return typeof evaluated === "number" && !isNaN(evaluated) ? evaluated : NaN;
1635
+ }
1636
+ }
1637
+ const params = Array.isArray(opd.parameters) ? opd.parameters : opd.parameters === void 0 ? void 0 : [opd.parameters];
1638
+ const raw = opd.leafFn ? opd.leafFn(dc, params, opd.options) : opd.evaluateAsValue(dc);
1639
+ if (raw === null || raw === void 0) return NaN;
1640
+ const num = Number(raw);
1641
+ return typeof num === "number" && !isNaN(num) ? num : NaN;
1642
+ }
1643
+ // Evaluate a flat token stream (no nested handling required here; nested SummarizeOperation
1644
+ // operands will evaluate themselves recursively)
1645
+ evaluateFlatTokens(tokens, dc) {
1646
+ const operands = [];
1647
+ const ops = [];
1648
+ for (const tk of tokens) {
1649
+ if (tk.t === "operand") operands.push(tk.v);
1650
+ else ops.push(tk.v);
1651
+ }
1652
+ if (operands.length === 0) return NaN;
1653
+ while (ops.length > 0) {
1654
+ let bestIdx = 0;
1655
+ let bestPrec = PRECEDENCE[ops[0]] ?? 0;
1656
+ for (let i = 1; i < ops.length; i++) {
1657
+ const p = PRECEDENCE[ops[i]] ?? 0;
1658
+ if (p > bestPrec || p === bestPrec && ops[i] === "^") {
1659
+ bestPrec = p;
1660
+ bestIdx = i;
1661
+ }
1662
+ }
1663
+ const op = ops.splice(bestIdx, 1)[0];
1664
+ const left = operands.splice(bestIdx, 1)[0];
1665
+ const right = operands.splice(bestIdx, 1)[0];
1666
+ const a = this.evaluateOperandToNumber(left, dc);
1667
+ const b = this.evaluateOperandToNumber(right, dc);
1668
+ let res2 = NaN;
1669
+ if (op === "+") res2 = a + b;
1670
+ else if (op === "-") res2 = a - b;
1671
+ else if (op === "*") res2 = a * b;
1672
+ else if (op === "/") res2 = b === 0 ? NaN : a / b;
1673
+ else if (op === "^") res2 = Math.pow(a, b);
1674
+ operands.splice(bestIdx, 0, res2);
1675
+ }
1676
+ const final = operands[0];
1677
+ if (typeof final === "number") {
1678
+ if (this._usedChainIds) {
1679
+ for (const id of this._usedChainIds) clearChainOps(id);
1680
+ this._usedChainIds = void 0;
1681
+ }
1682
+ return final;
1683
+ }
1684
+ const res = this.evaluateOperandToNumber(final, dc);
1685
+ if (this._usedChainIds) {
1686
+ for (const id of this._usedChainIds) clearChainOps(id);
1687
+ this._usedChainIds = void 0;
1688
+ }
1689
+ return res;
1690
+ }
1691
+ /**
1692
+ * Instance helper: return a grouped version of this operation so it becomes
1693
+ * a single operand in outer expressions. Equivalent to parens(this).
1694
+ */
1695
+ parens() {
1696
+ return parens(this);
1697
+ }
1698
+ // Top-level evaluation: handles the case where this instance is a leaf (leafFn present)
1699
+ // or an expression token stream.
1700
+ evaluateAsValue(dc) {
1701
+ if (this.leafFn) {
1702
+ const params = Array.isArray(this.parameters) ? this.parameters : this.parameters === void 0 ? void 0 : [this.parameters];
1703
+ const res = this.leafFn(dc, params, this.options);
1704
+ if (typeof res === "number" && Number.isNaN(res)) return null;
1705
+ return res;
1706
+ }
1707
+ if (!this.tokens || this.tokens.length === 0) return NaN;
1708
+ const val = this.evaluateFlatTokens(this.tokens, dc);
1709
+ if (typeof val === "number" && Number.isNaN(val)) return null;
1710
+ return val;
1711
+ }
1712
+ }
1713
+ function parens(op) {
1714
+ const tokens = [{ t: "operand", v: op }];
1715
+ return new SummarizeOperation(void 0, void 0, void 0, tokens);
1716
+ }
1717
+ const DEFAULT_SUMMARIZE_OPTIONS = {
1718
+ coerceBooleans: true,
1719
+ skipMissing: false
1720
+ };
1721
+ function applyDefaultOptions(options) {
1722
+ return { ...DEFAULT_SUMMARIZE_OPTIONS, ...options };
1723
+ }
1724
+ function resolveLazy(param, dataCalc, opName) {
1725
+ if (typeof param !== "function") {
1726
+ return param;
1727
+ }
1728
+ try {
1729
+ const res = param(dataCalc);
1730
+ if (typeof res === "function") {
1731
+ throw new M2Error(
1732
+ `${opName || "summarize()"}: lazy callback returned a function; expected a value or array of values.`
1733
+ );
1734
+ }
1735
+ if (res instanceof DataCalc) {
1736
+ throw new M2Error(
1737
+ `${opName || "summarize()"}: lazy callback returned a DataCalc instance; expected a value or array of values.`
1738
+ );
1739
+ }
1740
+ return res;
1741
+ } catch (err) {
1742
+ throw new M2Error(
1743
+ `${"summarize()"}: lazy callback threw an error: ${err && err.message ? err.message : String(err)}`
1744
+ );
1745
+ }
1746
+ }
1747
+ function processNumericValues(dataCalc, variable, options, collector, errorPrefix, initialState) {
1748
+ const mergedOptions = applyDefaultOptions(options);
1749
+ dataCalc.verifyObservationsContainVariable(variable);
1750
+ let count = 0;
1751
+ let state = initialState;
1752
+ let containsMissing = false;
1753
+ dataCalc.observations.forEach((o) => {
1754
+ if (dataCalc.isNonMissingNumeric(o[variable])) {
1755
+ state = collector(o[variable], state);
1756
+ count++;
1757
+ return;
1758
+ }
1759
+ if (typeof o[variable] === "boolean" && mergedOptions.coerceBooleans) {
1760
+ state = collector(o[variable] ? 1 : 0, state);
1761
+ count++;
1762
+ return;
1763
+ }
1764
+ if (dataCalc.isMissingNumeric(o[variable])) {
1765
+ containsMissing = true;
1766
+ return;
1767
+ }
1768
+ throw new M2Error(
1769
+ `${errorPrefix}: variable ${variable} has non-numeric value ${o[variable]} in this observation: ${JSON.stringify(o)}`
1770
+ );
1771
+ });
1772
+ return { state, count, containsMissing };
1773
+ }
1774
+ function processSingleValue(value, options, errorPrefix) {
1775
+ const mergedOptions = applyDefaultOptions(options);
1776
+ if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
1777
+ return { value, isMissing: false };
1778
+ } else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
1779
+ return { value: value ? 1 : 0, isMissing: false };
1780
+ } else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
1781
+ return { value: 0, isMissing: true };
1782
+ } else {
1783
+ throw new M2Error(`${errorPrefix}: has non-numeric value ${value}`);
1784
+ }
1785
+ }
1786
+ const varianceInternal = (dataCalc, params, options) => {
1787
+ let variableOrValues = params ? params[0] : void 0;
1788
+ variableOrValues = resolveLazy(variableOrValues, dataCalc);
1789
+ const mergedOptions = applyDefaultOptions(options);
1790
+ if (typeof variableOrValues === "string") {
1791
+ if (!dataCalc.variableExists(variableOrValues)) {
1792
+ return null;
1793
+ }
1794
+ const variable = variableOrValues;
1795
+ const meanResult = processNumericValues(
1796
+ dataCalc,
1797
+ variable,
1798
+ options,
1799
+ (value, sum2) => sum2 + value,
1800
+ "variance()",
1801
+ 0
1802
+ );
1803
+ if (meanResult.containsMissing && !mergedOptions.skipMissing) {
1804
+ return null;
1805
+ }
1806
+ if (meanResult.count <= 1) {
1807
+ return null;
1808
+ }
1809
+ const meanValue = meanResult.state / meanResult.count;
1810
+ const varianceResult = processNumericValues(
1811
+ dataCalc,
1812
+ variable,
1813
+ options,
1814
+ (value, sum2) => {
1815
+ const actualValue = typeof value === "boolean" && mergedOptions.coerceBooleans ? value ? 1 : 0 : value;
1816
+ return sum2 + Math.pow(actualValue - meanValue, 2);
1817
+ },
1818
+ "variance()",
1819
+ 0
1820
+ );
1821
+ return varianceResult.state / (meanResult.count - 1);
1822
+ } else if (Array.isArray(variableOrValues)) {
1823
+ const validValues = [];
1824
+ let containsMissing = false;
1825
+ for (const value of variableOrValues) {
1826
+ if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
1827
+ validValues.push(value);
1828
+ } else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
1829
+ validValues.push(value ? 1 : 0);
1830
+ } else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
1831
+ containsMissing = true;
1832
+ } else {
1833
+ throw new M2Error(`variance(): has non-numeric value ${value}`);
1834
+ }
1835
+ }
1836
+ if (containsMissing && !mergedOptions.skipMissing) {
1837
+ return null;
1838
+ }
1839
+ if (validValues.length <= 1) {
1840
+ return null;
1841
+ }
1842
+ const sum2 = validValues.reduce((acc, val) => acc + val, 0);
1843
+ const mean2 = sum2 / validValues.length;
1844
+ const sumSquaredDiffs = validValues.reduce(
1845
+ (acc, val) => acc + Math.pow(val - mean2, 2),
1846
+ 0
1847
+ );
1848
+ return sumSquaredDiffs / (validValues.length - 1);
1849
+ } else {
1850
+ return null;
1851
+ }
1852
+ };
1853
+ const medianInternal = (dataCalc, params, options) => {
1854
+ let variableOrValues = params ? params[0] : void 0;
1855
+ variableOrValues = resolveLazy(variableOrValues, dataCalc);
1856
+ const mergedOptions = applyDefaultOptions(options);
1857
+ if (typeof variableOrValues === "string") {
1858
+ if (!dataCalc.variableExists(variableOrValues)) {
1859
+ return null;
1860
+ }
1861
+ const variable = variableOrValues;
1862
+ dataCalc.verifyObservationsContainVariable(variable);
1863
+ const values = [];
1864
+ let containsMissing = false;
1865
+ dataCalc.observations.forEach((o) => {
1866
+ if (dataCalc.isNonMissingNumeric(o[variable])) {
1867
+ values.push(o[variable]);
1868
+ } else if (typeof o[variable] === "boolean" && mergedOptions.coerceBooleans) {
1869
+ values.push(o[variable] ? 1 : 0);
1870
+ } else if (dataCalc.isMissingNumeric(o[variable])) {
1871
+ containsMissing = true;
1872
+ } else {
1873
+ throw new M2Error(
1874
+ `median(): variable ${variable} has non-numeric value ${o[variable]} in this observation: ${JSON.stringify(o)}`
1875
+ );
1876
+ }
1877
+ });
1878
+ if (containsMissing && !mergedOptions.skipMissing) {
1879
+ return null;
1880
+ }
1881
+ if (values.length === 0) {
1882
+ return null;
1883
+ }
1884
+ values.sort((a, b) => a - b);
1885
+ const mid = Math.floor(values.length / 2);
1886
+ if (values.length % 2 === 0) {
1887
+ return (values[mid - 1] + values[mid]) / 2;
1888
+ } else {
1889
+ return values[mid];
1890
+ }
1891
+ } else if (Array.isArray(variableOrValues)) {
1892
+ const values = [];
1893
+ let containsMissing = false;
1894
+ for (const value of variableOrValues) {
1895
+ if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
1896
+ values.push(value);
1897
+ } else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
1898
+ values.push(value ? 1 : 0);
1899
+ } else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
1900
+ containsMissing = true;
1901
+ } else {
1902
+ throw new M2Error(`median(): has non-numeric value ${value}`);
1903
+ }
1904
+ }
1905
+ if (containsMissing && !mergedOptions.skipMissing) {
1906
+ return null;
1907
+ }
1908
+ if (values.length === 0) {
1909
+ return null;
1910
+ }
1911
+ values.sort((a, b) => a - b);
1912
+ const mid = Math.floor(values.length / 2);
1913
+ if (values.length % 2 === 0) {
1914
+ return (values[mid - 1] + values[mid]) / 2;
1915
+ } else {
1916
+ return values[mid];
1917
+ }
1918
+ } else {
1919
+ const result = processSingleValue(variableOrValues, options, "median()");
1920
+ if (result.isMissing && !mergedOptions.skipMissing) {
1921
+ return null;
1922
+ }
1923
+ return result.isMissing ? null : result.value;
1924
+ }
1925
+ };
1926
+ function median(variableOrValues, options) {
1927
+ return SummarizeOperation.leaf(medianInternal, [variableOrValues], options);
1928
+ }
1929
+ const sdInternal = (dataCalc, params, options) => {
1930
+ let variableOrValues = params ? params[0] : void 0;
1931
+ variableOrValues = resolveLazy(variableOrValues, dataCalc);
1932
+ if (typeof variableOrValues === "string") {
1933
+ if (!dataCalc.variableExists(variableOrValues)) {
1934
+ return null;
1935
+ }
1936
+ const varianceValue = varianceInternal(dataCalc, params, options);
1937
+ if (varianceValue === null) {
1938
+ return null;
1939
+ }
1940
+ return Math.sqrt(varianceValue);
1941
+ } else if (Array.isArray(variableOrValues)) {
1942
+ const newParams = params ? [...params] : [variableOrValues];
1943
+ const varianceValue = varianceInternal(dataCalc, newParams, options);
1944
+ if (varianceValue === null) {
1945
+ return null;
1946
+ }
1947
+ return Math.sqrt(varianceValue);
1948
+ } else {
1949
+ return null;
1950
+ }
1951
+ };
1952
+ function sd(variableOrValues, options) {
1953
+ return SummarizeOperation.leaf(sdInternal, [variableOrValues], options);
1954
+ }
1955
+ const scalarInternal = (_dataCalc, params, options) => {
1956
+ let v = params ? params[0] : void 0;
1957
+ if (typeof v === "function") v = v(_dataCalc);
1958
+ if (typeof v === "string") {
1959
+ const re = /^__CHAIN_EXPR__\[(.*?)\]$/;
1960
+ const m = re.exec(v);
1961
+ if (m) {
1962
+ const payload = m[1];
1963
+ let ops = getChainOps(payload);
1964
+ if (!ops) {
1965
+ try {
1966
+ ops = JSON.parse(decodeURIComponent(payload));
1967
+ } catch {
1968
+ ops = void 0;
1969
+ }
1970
+ }
1971
+ if (ops) {
1972
+ let current = _dataCalc;
1973
+ let evaluated = void 0;
1974
+ for (const op of ops) {
1975
+ const method = current[op.name];
1976
+ if (typeof method !== "function") {
1977
+ evaluated = NaN;
1978
+ break;
1979
+ }
1980
+ const res = method.apply(current, op.args);
1981
+ if (res instanceof DataCalc) {
1982
+ current = res;
1983
+ continue;
1984
+ }
1985
+ if (Array.isArray(res)) {
1986
+ evaluated = res.length;
1987
+ break;
1988
+ }
1989
+ if (typeof res === "boolean") {
1990
+ evaluated = res;
1991
+ break;
1992
+ }
1993
+ evaluated = typeof res === "number" ? res : Number(res);
1994
+ break;
1995
+ }
1996
+ if (evaluated === void 0)
1997
+ evaluated = current instanceof DataCalc ? current.length : NaN;
1998
+ if (!Number.isNaN(evaluated)) v = evaluated;
1999
+ else v = null;
2000
+ try {
2001
+ clearChainOps(payload);
2002
+ } catch {
2003
+ }
2004
+ }
2005
+ }
2006
+ }
2007
+ if (typeof v === "object" && v !== null && "summarizeFunction" in v) {
2008
+ try {
2009
+ const op = v;
2010
+ const paramsForOp = Array.isArray(op.parameters) ? op.parameters : op.parameters === void 0 ? void 0 : [op.parameters];
2011
+ const raw = op.summarizeFunction(
2012
+ _dataCalc,
2013
+ paramsForOp,
2014
+ op.options
2015
+ );
2016
+ v = raw;
2017
+ } catch {
2018
+ v = null;
2019
+ }
2020
+ }
2021
+ const result = processSingleValue(v, options, "scalar()");
2022
+ return result.isMissing ? null : result.value;
2023
+ };
2024
+ function scalar(value) {
2025
+ return SummarizeOperation.leaf(scalarInternal, [value], void 0);
2026
+ }
2027
+ console.log("\u26AA @m2c2kit/data-calc version 0.8.7 (38a5862e)");
1089
2028
 
1090
2029
  class ColorShapes extends Game {
1091
2030
  constructor() {
@@ -1198,6 +2137,14 @@ class ColorShapes extends Game {
1198
2137
  type: "boolean",
1199
2138
  default: false,
1200
2139
  description: "Should scoring data be generated? Default is false."
2140
+ },
2141
+ scoring_filter_response_time_duration_ms: {
2142
+ type: "array",
2143
+ items: {
2144
+ type: "number"
2145
+ },
2146
+ default: [100, 1e4],
2147
+ description: "When scoring, values of response_time_duration_ms less than the lower bound or greater than the upper bound are discarded. This array contains two numbers, the lower and upper bounds."
1201
2148
  }
1202
2149
  };
1203
2150
  const colorShapesTrialSchema = {
@@ -1330,6 +2277,14 @@ class ColorShapes extends Game {
1330
2277
  format: "date-time",
1331
2278
  description: "ISO 8601 timestamp at the end of the last trial. Null if no trials were completed."
1332
2279
  },
2280
+ response_time_filter_lower_bound: {
2281
+ type: "number",
2282
+ description: "Response times less than this lower bound were discarded when calculating filtered response times."
2283
+ },
2284
+ response_time_filter_upper_bound: {
2285
+ type: "number",
2286
+ description: "Response times greater than this upper bound were discarded when calculating filtered response times."
2287
+ },
1333
2288
  n_trials: {
1334
2289
  type: "integer",
1335
2290
  description: "Number of trials completed."
@@ -1349,6 +2304,158 @@ class ColorShapes extends Game {
1349
2304
  participant_score: {
1350
2305
  type: ["number", "null"],
1351
2306
  description: "Participant-facing score, calculated as (number of correct trials / number of trials attempted) * 100. This is a simple metric to provide feedback to the participant. Null if no trials attempted."
2307
+ },
2308
+ flag_trials_lt_expected: {
2309
+ type: "number",
2310
+ description: "Is the number of completed trials fewer than expected? 1 = true, 0 = false."
2311
+ },
2312
+ flag_trials_gt_expected: {
2313
+ type: "number",
2314
+ description: "Is the number of completed trials greater than expected? 1 = true, 0 = false."
2315
+ },
2316
+ n_trials_HIT: {
2317
+ type: "number",
2318
+ description: "Number of HIT trials (correctly identified a 'different' trial)"
2319
+ },
2320
+ n_trials_MISS: {
2321
+ type: "number",
2322
+ description: "Number of MISS trials (failed to detect a 'different' trial, responded 'same')"
2323
+ },
2324
+ n_trials_FA: {
2325
+ type: "number",
2326
+ description: "Number of False Alarm trials (incorrectly responded 'different' on a 'same' trial)"
2327
+ },
2328
+ n_trials_CR: {
2329
+ type: "number",
2330
+ description: "Number of Correct Rejection trials (correctly responded 'same' on a 'same' trial)"
2331
+ },
2332
+ n_trials_type_same: {
2333
+ type: "number",
2334
+ description: "Number of trials where the true signal type was 'SAME' (presented and response shapes were identical)"
2335
+ },
2336
+ n_trials_type_different: {
2337
+ type: "number",
2338
+ description: "Number of trials where the true signal type was 'DIFFERENT' (response shapes differed from presented)"
2339
+ },
2340
+ HIT_rate: {
2341
+ type: ["number", "null"],
2342
+ description: "Proportion of 'different' trials correctly identified as different. Calculated as n_trials_HIT / n_trials_type_different"
2343
+ },
2344
+ MISS_rate: {
2345
+ type: ["number", "null"],
2346
+ description: "Proportion of 'different' trials incorrectly identified as same. Calculated as 1 - HIT_rate"
2347
+ },
2348
+ FA_rate: {
2349
+ type: ["number", "null"],
2350
+ description: "Proportion of 'same' trials incorrectly identified as different. Calculated as n_trials_FA / n_trials_type_same"
2351
+ },
2352
+ CR_rate: {
2353
+ type: ["number", "null"],
2354
+ description: "Proportion of 'same' trials correctly identified as same. Calculated as 1 - FA_rate"
2355
+ },
2356
+ n_trials_rt_invalid: {
2357
+ type: "number",
2358
+ description: "Number of trials with null or non-positive response times (excluded from RT calculations)"
2359
+ },
2360
+ median_rt_overall_valid: {
2361
+ type: ["number", "null"],
2362
+ description: "Median response time (ms) across all trials with valid (non-null, positive) response times. No outlier filtering applied"
2363
+ },
2364
+ sd_rt_overall_valid: {
2365
+ type: ["number", "null"],
2366
+ description: "Standard deviation of response time (ms) across all trials with valid response times. No outlier filtering applied"
2367
+ },
2368
+ median_rt_HIT_valid: {
2369
+ type: ["number", "null"],
2370
+ description: "Median response time (ms) for HIT trials with valid response times. No outlier filtering"
2371
+ },
2372
+ sd_rt_HIT_valid: {
2373
+ type: ["number", "null"],
2374
+ description: "Standard deviation of response time (ms) for HIT trials with valid response times. No outlier filtering"
2375
+ },
2376
+ median_rt_MISS_valid: {
2377
+ type: ["number", "null"],
2378
+ description: "Median response time (ms) for MISS trials with valid response times. No outlier filtering"
2379
+ },
2380
+ sd_rt_MISS_valid: {
2381
+ type: ["number", "null"],
2382
+ description: "Standard deviation of response time (ms) for MISS trials with valid response times. No outlier filtering"
2383
+ },
2384
+ median_rt_FA_valid: {
2385
+ type: ["number", "null"],
2386
+ description: "Median response time (ms) for False Alarm trials with valid response times. No outlier filtering"
2387
+ },
2388
+ sd_rt_FA_valid: {
2389
+ type: ["number", "null"],
2390
+ description: "Standard deviation of response time (ms) for False Alarm trials with valid response times. No outlier filtering"
2391
+ },
2392
+ median_rt_CR_valid: {
2393
+ type: ["number", "null"],
2394
+ description: "Median response time (ms) for Correct Rejection trials with valid response times. No outlier filtering"
2395
+ },
2396
+ sd_rt_CR_valid: {
2397
+ type: ["number", "null"],
2398
+ description: "Standard deviation of response time (ms) for Correct Rejection trials with valid response times. No outlier filtering"
2399
+ },
2400
+ median_rt_overall_valid_filtered: {
2401
+ type: ["number", "null"],
2402
+ description: "Median response time (ms) across all valid trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2403
+ },
2404
+ sd_rt_overall_valid_filtered: {
2405
+ type: ["number", "null"],
2406
+ description: "Standard deviation of response time (ms) across all valid trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2407
+ },
2408
+ n_outliers_rt_overall_valid: {
2409
+ type: "number",
2410
+ description: "Number of valid trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2411
+ },
2412
+ median_rt_HIT_valid_filtered: {
2413
+ type: ["number", "null"],
2414
+ description: "Median response time (ms) for HIT trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2415
+ },
2416
+ sd_rt_HIT_valid_filtered: {
2417
+ type: ["number", "null"],
2418
+ description: "Standard deviation of response time (ms) for HIT trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2419
+ },
2420
+ n_outliers_rt_HIT_valid: {
2421
+ type: "number",
2422
+ description: "Number of HIT trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2423
+ },
2424
+ median_rt_MISS_valid_filtered: {
2425
+ type: ["number", "null"],
2426
+ description: "Median response time (ms) for MISS trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2427
+ },
2428
+ sd_rt_MISS_valid_filtered: {
2429
+ type: ["number", "null"],
2430
+ description: "Standard deviation of response time (ms) for MISS trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2431
+ },
2432
+ n_outliers_rt_MISS_valid: {
2433
+ type: "number",
2434
+ description: "Number of MISS trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2435
+ },
2436
+ median_rt_FA_valid_filtered: {
2437
+ type: ["number", "null"],
2438
+ description: "Median response time (ms) for False Alarm trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2439
+ },
2440
+ sd_rt_FA_valid_filtered: {
2441
+ type: ["number", "null"],
2442
+ description: "Standard deviation of response time (ms) for False Alarm trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2443
+ },
2444
+ n_outliers_rt_FA_valid: {
2445
+ type: "number",
2446
+ description: "Number of False Alarm trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2447
+ },
2448
+ median_rt_CR_valid_filtered: {
2449
+ type: ["number", "null"],
2450
+ description: "Median response time (ms) for Correct Rejection trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2451
+ },
2452
+ sd_rt_CR_valid_filtered: {
2453
+ type: ["number", "null"],
2454
+ description: "Standard deviation of response time (ms) for Correct Rejection trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
2455
+ },
2456
+ n_outliers_rt_CR_valid: {
2457
+ type: ["number", "null"],
2458
+ description: "Number of Correct Rejection trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
1352
2459
  }
1353
2460
  };
1354
2461
  const translation = {
@@ -1415,8 +2522,8 @@ class ColorShapes extends Game {
1415
2522
  */
1416
2523
  id: "color-shapes",
1417
2524
  publishUuid: "394cb010-2ccf-4a87-9d23-cda7fb07a960",
1418
- version: "0.8.32 (d289d12c)",
1419
- moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.32", "dependencies": { "@m2c2kit/addons": "0.3.33", "@m2c2kit/core": "0.3.34" } },
2525
+ version: "0.8.34 (38a5862e)",
2526
+ moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.34", "dependencies": { "@m2c2kit/addons": "0.3.35", "@m2c2kit/core": "0.3.36", "@m2c2kit/data-calc": "0.8.7" } },
1420
2527
  translation,
1421
2528
  shortDescription: "Color Shapes is a visual array change detection task, measuring intra-item feature binding, where participants determine if shapes change color across two sequential presentations of shape stimuli.",
1422
2529
  longDescription: `Color Shapes is a change detection paradigm used to measure visual short-term memory binding (Parra et al., 2009). Participants are asked to memorize the shapes and colors of three different polygons for 3 seconds. The three polygons are then removed from the screen and re-displayed at different locations, either having the same or different colors. Participants are then asked to decide whether the combination of colors and shapes are the "Same" or "Different" between the study and test phases.`,
@@ -1494,6 +2601,12 @@ class ColorShapes extends Game {
1494
2601
  game.trialComplete();
1495
2602
  if (game.getParameter("scoring")) {
1496
2603
  const scores = game.calculateScores([], {
2604
+ rtLowerBound: game.getParameter(
2605
+ "scoring_filter_response_time_duration_ms"
2606
+ )[0],
2607
+ rtUpperBound: game.getParameter(
2608
+ "scoring_filter_response_time_duration_ms"
2609
+ )[1],
1497
2610
  numberOfTrials: game.getParameter("number_of_trials")
1498
2611
  });
1499
2612
  game.addScoringData(scores);
@@ -1916,6 +3029,12 @@ class ColorShapes extends Game {
1916
3029
  } else {
1917
3030
  if (game.getParameter("scoring")) {
1918
3031
  const scores = game.calculateScores(game.data.trials, {
3032
+ rtLowerBound: game.getParameter(
3033
+ "scoring_filter_response_time_duration_ms"
3034
+ )[0],
3035
+ rtUpperBound: game.getParameter(
3036
+ "scoring_filter_response_time_duration_ms"
3037
+ )[1],
1919
3038
  numberOfTrials: game.getParameter("number_of_trials")
1920
3039
  });
1921
3040
  game.addScoringData(scores);
@@ -1959,18 +3078,198 @@ class ColorShapes extends Game {
1959
3078
  }
1960
3079
  calculateScores(data, extras) {
1961
3080
  const dc = new DataCalc(data);
1962
- const scores = dc.summarize({
3081
+ const n_trials = dc.filter((o) => o.quit_button_pressed === false).length;
3082
+ const scores = dc.mutate({
3083
+ metric_accuracy: (obs) => {
3084
+ const ur = obs.user_response;
3085
+ const uc = obs.user_response_correct;
3086
+ if (ur === "same" && uc === true) return "CR";
3087
+ if (ur === "same" && uc === false) return "MISS";
3088
+ if (ur === "different" && uc === true) return "HIT";
3089
+ if (ur === "different" && uc === false) return "FA";
3090
+ return null;
3091
+ },
3092
+ metric_trial_type: (obs) => {
3093
+ const ur = obs.user_response;
3094
+ const uc = obs.user_response_correct;
3095
+ if (ur === "same" && uc === true) return "SAME";
3096
+ if (ur === "same" && uc === false) return "DIFFERENT";
3097
+ if (ur === "different" && uc === true) return "DIFFERENT";
3098
+ if (ur === "different" && uc === false) return "SAME";
3099
+ return null;
3100
+ },
3101
+ is_valid: (obs) => typeof obs.response_time_duration_ms === "number" && obs.response_time_duration_ms > 0
3102
+ }).summarize({
1963
3103
  activity_begin_iso8601_timestamp: this.beginIso8601Timestamp,
1964
- first_trial_begin_iso8601_timestamp: dc.arrange("trial_begin_iso8601_timestamp").slice(0).pull("trial_begin_iso8601_timestamp"),
1965
- last_trial_end_iso8601_timestamp: dc.arrange("-trial_end_iso8601_timestamp").slice(0).pull("trial_end_iso8601_timestamp"),
1966
- n_trials: dc.length,
1967
- flag_trials_match_expected: dc.length === extras.numberOfTrials ? 1 : 0,
1968
- n_trials_correct: dc.filter((obs) => obs.user_response_correct === true).length,
1969
- n_trials_incorrect: dc.filter(
1970
- (obs) => obs.user_response_correct === false
1971
- ).length
1972
- }).mutate({
1973
- participant_score: (obs) => obs.n_trials > 0 ? obs.n_trials_correct / obs.n_trials * 100 : null
3104
+ first_trial_begin_iso8601_timestamp: arrange(
3105
+ "trial_begin_iso8601_timestamp"
3106
+ ).slice(0).pull("trial_begin_iso8601_timestamp"),
3107
+ last_trial_end_iso8601_timestamp: arrange(
3108
+ "-trial_end_iso8601_timestamp"
3109
+ ).slice(0).pull("trial_end_iso8601_timestamp"),
3110
+ n_trials,
3111
+ flag_trials_match_expected: n_trials === extras.numberOfTrials ? 1 : 0,
3112
+ flag_trials_lt_expected: n_trials < extras.numberOfTrials ? 1 : 0,
3113
+ flag_trials_gt_expected: n_trials > extras.numberOfTrials ? 1 : 0,
3114
+ // counts by accuracy
3115
+ n_trials_HIT: filter((o) => o.metric_accuracy === "HIT").length,
3116
+ n_trials_MISS: filter((o) => o.metric_accuracy === "MISS").length,
3117
+ n_trials_FA: filter((o) => o.metric_accuracy === "FA").length,
3118
+ n_trials_CR: filter((o) => o.metric_accuracy === "CR").length,
3119
+ // trial types
3120
+ n_trials_type_same: filter((o) => o.metric_trial_type === "SAME").length,
3121
+ n_trials_type_different: filter(
3122
+ (o) => o.metric_trial_type === "DIFFERENT"
3123
+ ).length,
3124
+ // Note: When the summarize operation is a custom function,
3125
+ // () => value, rather than a built in function like sum or mean,
3126
+ // we must provide the DataCalc object as an argument to the
3127
+ // function, and do the filtering and pulling of values on that
3128
+ // object within the function. Specifically below, we must do:
3129
+ // const denom = d.filter(...).length
3130
+ // NOT
3131
+ // const denom = filter(...).length
3132
+ // rates
3133
+ HIT_rate: (d) => {
3134
+ const denom = d.filter(
3135
+ (o) => o.metric_trial_type === "DIFFERENT"
3136
+ ).length;
3137
+ return denom > 0 ? d.filter((o) => o.metric_accuracy === "HIT").length / denom : null;
3138
+ },
3139
+ MISS_rate: (d) => {
3140
+ const denom = d.filter(
3141
+ (o) => o.metric_trial_type === "DIFFERENT"
3142
+ ).length;
3143
+ return denom > 0 ? 1 - d.filter((o) => o.metric_accuracy === "HIT").length / denom : null;
3144
+ },
3145
+ FA_rate: (d) => {
3146
+ const denom = d.filter((o) => o.metric_trial_type === "SAME").length;
3147
+ return denom > 0 ? d.filter((o) => o.metric_accuracy === "FA").length / denom : null;
3148
+ },
3149
+ CR_rate: (d) => {
3150
+ const denom = d.filter((o) => o.metric_trial_type === "SAME").length;
3151
+ return denom > 0 ? 1 - d.filter((o) => o.metric_accuracy === "FA").length / denom : null;
3152
+ },
3153
+ // response time summaries
3154
+ // overall valid RTs (>0)
3155
+ median_rt_overall_valid: median(
3156
+ filter((o) => o.is_valid).pull("response_time_duration_ms")
3157
+ ),
3158
+ sd_rt_overall_valid: sd(
3159
+ filter((o) => o.is_valid).pull("response_time_duration_ms")
3160
+ ),
3161
+ // filtered overall RTs
3162
+ median_rt_overall_valid_filtered: median(
3163
+ filter((o) => o.is_valid).filter(
3164
+ (o) => o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3165
+ ).pull("response_time_duration_ms")
3166
+ ),
3167
+ sd_rt_overall_valid_filtered: sd(
3168
+ filter((o) => o.is_valid).filter(
3169
+ (o) => o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3170
+ ).pull("response_time_duration_ms")
3171
+ ),
3172
+ // counts of invalid/outliers
3173
+ n_trials_rt_invalid: filter((o) => !o.is_valid).length,
3174
+ n_outliers_rt_overall_valid: filter((o) => o.is_valid).filter(
3175
+ (o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
3176
+ ).length,
3177
+ response_time_filter_lower_bound: extras.rtLowerBound,
3178
+ response_time_filter_upper_bound: extras.rtUpperBound,
3179
+ // per-metric RT summaries
3180
+ median_rt_HIT_valid: median(
3181
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").pull("response_time_duration_ms")
3182
+ ),
3183
+ sd_rt_HIT_valid: sd(
3184
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").pull("response_time_duration_ms")
3185
+ ),
3186
+ median_rt_HIT_valid_filtered: median(
3187
+ filter((o) => o.is_valid).filter(
3188
+ (o) => o.metric_accuracy === "HIT" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3189
+ ).pull("response_time_duration_ms")
3190
+ ),
3191
+ sd_rt_HIT_valid_filtered: sd(
3192
+ filter((o) => o.is_valid).filter(
3193
+ (o) => o.metric_accuracy === "HIT" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3194
+ ).pull("response_time_duration_ms")
3195
+ ),
3196
+ n_outliers_rt_HIT_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").filter(
3197
+ (o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
3198
+ ).length,
3199
+ median_rt_MISS_valid: median(
3200
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").pull("response_time_duration_ms")
3201
+ ),
3202
+ sd_rt_MISS_valid: sd(
3203
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").pull("response_time_duration_ms")
3204
+ ),
3205
+ median_rt_MISS_valid_filtered: median(
3206
+ filter((o) => o.is_valid).filter(
3207
+ (o) => o.metric_accuracy === "MISS" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3208
+ ).pull("response_time_duration_ms")
3209
+ ),
3210
+ sd_rt_MISS_valid_filtered: sd(
3211
+ filter((o) => o.is_valid).filter(
3212
+ (o) => o.metric_accuracy === "MISS" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3213
+ ).pull("response_time_duration_ms")
3214
+ ),
3215
+ n_outliers_rt_MISS_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").filter(
3216
+ (o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
3217
+ ).length,
3218
+ median_rt_FA_valid: median(
3219
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").pull("response_time_duration_ms")
3220
+ ),
3221
+ sd_rt_FA_valid: sd(
3222
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").pull("response_time_duration_ms")
3223
+ ),
3224
+ median_rt_FA_valid_filtered: median(
3225
+ filter((o) => o.is_valid).filter(
3226
+ (o) => o.metric_accuracy === "FA" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3227
+ ).pull("response_time_duration_ms")
3228
+ ),
3229
+ sd_rt_FA_valid_filtered: sd(
3230
+ filter((o) => o.is_valid).filter(
3231
+ (o) => o.metric_accuracy === "FA" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3232
+ ).pull("response_time_duration_ms")
3233
+ ),
3234
+ n_outliers_rt_FA_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").filter(
3235
+ (o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
3236
+ ).length,
3237
+ median_rt_CR_valid: median(
3238
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").pull("response_time_duration_ms")
3239
+ ),
3240
+ sd_rt_CR_valid: sd(
3241
+ filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").pull("response_time_duration_ms")
3242
+ ),
3243
+ median_rt_CR_valid_filtered: median(
3244
+ filter((o) => o.is_valid).filter(
3245
+ (o) => o.metric_accuracy === "CR" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3246
+ ).pull("response_time_duration_ms")
3247
+ ),
3248
+ sd_rt_CR_valid_filtered: sd(
3249
+ filter((o) => o.is_valid).filter(
3250
+ (o) => o.metric_accuracy === "CR" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
3251
+ ).pull("response_time_duration_ms")
3252
+ ),
3253
+ n_outliers_rt_CR_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").filter(
3254
+ (o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
3255
+ ).length,
3256
+ n_trials_correct: filter(
3257
+ (o) => o.metric_accuracy === "HIT" || o.metric_accuracy === "CR"
3258
+ ).length,
3259
+ n_trials_incorrect: filter(
3260
+ (o) => o.metric_accuracy === "MISS" || o.metric_accuracy === "FA"
3261
+ ).length,
3262
+ participant_score: (d) => {
3263
+ const total = d.length;
3264
+ if (total === 0) return null;
3265
+ return d.summarize({
3266
+ correct: parens(
3267
+ scalar(d.filter((o) => o.metric_accuracy === "HIT").length).add(
3268
+ scalar(d.filter((o) => o.metric_accuracy === "CR").length)
3269
+ )
3270
+ ).div(scalar(total)).mul(scalar(100))
3271
+ }).pull("correct");
3272
+ }
1974
3273
  });
1975
3274
  return scores.observations;
1976
3275
  }