diagcalc 3.2.3 → 5.0.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/tui/index.js CHANGED
@@ -4,130 +4,9 @@ const blessed = require("blessed");
4
4
  const core = require("../lib/diagcalc-core");
5
5
  const datasetStore = require("../lib/diagcalc-datasets");
6
6
 
7
- function renderResults(metrics) {
8
- const performance = [
9
- renderMetricLine(metrics.sensitivity, "sensitivity"),
10
- renderMetricLine(metrics.specificity, "specificity"),
11
- renderMetricLine(metrics.ppv, "ppv"),
12
- renderMetricLine(metrics.npv, "npv"),
13
- ];
14
-
15
- const bayes = [
16
- renderMetricLine(metrics.lrPositive, "lrPositive"),
17
- renderMetricLine(metrics.lrNegative, "lrNegative"),
18
- renderMetricLine(metrics.preTestProbability, "neutral"),
19
- renderMetricLine(metrics.postTestPositive, "postPositive"),
20
- renderMetricLine(metrics.postTestNegative, "postNegative"),
21
- ];
22
-
23
- const bars = [
24
- `Pre-test [${core.buildProbabilityBar(metrics.preTestProbability.value, 24)}] ${core.formatPercentage(metrics.preTestProbability.value)}`,
25
- `Post-test (+) [${core.buildProbabilityBar(metrics.postTestPositive.value, 24)}] ${core.formatPercentage(metrics.postTestPositive.value)}`,
26
- `Post-test (-) [${core.buildProbabilityBar(metrics.postTestNegative.value, 24)}] ${core.formatPercentage(metrics.postTestNegative.value)}`,
27
- ];
28
-
29
- return [
30
- "{bold}Performance Measures{/bold}",
31
- performance.join("\n"),
32
- "",
33
- "{bold}Bayesian Update{/bold}",
34
- bayes.join("\n"),
35
- "",
36
- "{bold}Probability Shift{/bold}",
37
- bars.join("\n"),
38
- "",
39
- "{bold}Interpretation{/bold}",
40
- `Sensitivity ${metrics.sensitivity.note}`,
41
- `Specificity ${metrics.specificity.note}`,
42
- `LR+ ${metrics.lrPositive.note}`,
43
- `LR- ${metrics.lrNegative.note}`,
44
- ].join("\n");
45
- }
46
-
47
- function renderMetricLine(metric, kind) {
48
- const value = core.formatValue(metric.value, metric.formatter);
49
- const valueTag = colourValue(value, kind, metric.value);
50
- const confidence = renderConfidence(metric);
51
- const confidenceText = confidence ? ` {gray-fg}${confidence}{/}` : "";
52
- return `${metric.label.padEnd(36)} ${valueTag}${confidenceText}`;
53
- }
54
-
55
- function renderConfidence(metric) {
56
- if (!metric.ci) {
57
- return "";
58
- }
59
-
60
- return `95% CI ${core.formatPercentage(metric.ci.lower)} to ${core.formatPercentage(metric.ci.upper)}`;
61
- }
62
-
63
- function colourValue(value, kind, numericValue) {
64
- const { colour, badge } = metricTone(kind, numericValue);
65
- return `{${colour}-fg}${value}{/} {bold}{${colour}-fg}[${badge}]{/}{/}`;
66
- }
67
-
68
- function metricTone(kind, value) {
69
- if (!Number.isFinite(value)) {
70
- return { colour: "magenta", badge: "EXTREME" };
71
- }
72
-
73
- if (kind === "sensitivity" || kind === "specificity" || kind === "ppv" || kind === "npv") {
74
- if (value >= 0.9) {
75
- return { colour: "green", badge: "STRONG" };
76
- }
77
- if (value >= 0.7) {
78
- return { colour: "yellow", badge: "MOD" };
79
- }
80
- return { colour: "red", badge: "WEAK" };
81
- }
82
-
83
- if (kind === "lrPositive") {
84
- if (value >= 10) {
85
- return { colour: "green", badge: "RULE-IN" };
86
- }
87
- if (value >= 5) {
88
- return { colour: "yellow", badge: "USEFUL" };
89
- }
90
- if (value >= 2) {
91
- return { colour: "yellow", badge: "LIMITED" };
92
- }
93
- return { colour: "red", badge: "WEAK" };
94
- }
95
-
96
- if (kind === "lrNegative") {
97
- if (value <= 0.1) {
98
- return { colour: "green", badge: "RULE-OUT" };
99
- }
100
- if (value <= 0.2) {
101
- return { colour: "yellow", badge: "USEFUL" };
102
- }
103
- if (value <= 0.5) {
104
- return { colour: "yellow", badge: "LIMITED" };
105
- }
106
- return { colour: "red", badge: "WEAK" };
107
- }
108
-
109
- if (kind === "postPositive") {
110
- if (value >= 0.8) {
111
- return { colour: "green", badge: "HIGH" };
112
- }
113
- if (value >= 0.4) {
114
- return { colour: "yellow", badge: "MID" };
115
- }
116
- return { colour: "red", badge: "LOW" };
117
- }
118
-
119
- if (kind === "postNegative") {
120
- if (value <= 0.1) {
121
- return { colour: "green", badge: "LOW" };
122
- }
123
- if (value <= 0.3) {
124
- return { colour: "yellow", badge: "MID" };
125
- }
126
- return { colour: "red", badge: "HIGH" };
127
- }
128
-
129
- return { colour: "cyan", badge: "INFO" };
130
- }
7
+ const { renderResults, renderConfidence, CREW } = require("./presentation");
8
+ const { sanitiseRawInput, isAcceptedCharacter } = require("./input");
9
+ const { buildTextExport, buildMarkdownExport } = require("./report");
131
10
 
132
11
  function runTui() {
133
12
  const datasets = [
@@ -187,7 +66,7 @@ function runTui() {
187
66
  height: 3,
188
67
  tags: true,
189
68
  border: "line",
190
- content: "{bold}DIAGCALC{/bold} Arrows move Enter edit/load n new ad hoc case x export txt m export md Tab/Ctrl-N next panel Shift-Tab/Ctrl-P previous r reset q quit",
69
+ content: "{bold}DIAGCALC{/bold} Arrows move Enter edit/load n new + chain x export txt m export md Tab/Ctrl-N next panel r reset q quit",
191
70
  });
192
71
 
193
72
  const datasetList = blessed.list({
@@ -203,14 +82,14 @@ function runTui() {
203
82
  mouse: true,
204
83
  style: {
205
84
  selected: {
206
- bg: "blue",
85
+ bg: CREW.queequeg,
207
86
  },
208
87
  border: {
209
- fg: "white",
88
+ fg: CREW.ishmael,
210
89
  },
211
90
  focus: {
212
91
  border: {
213
- fg: "blue",
92
+ fg: CREW.queequeg,
214
93
  },
215
94
  },
216
95
  },
@@ -245,11 +124,11 @@ function runTui() {
245
124
  tags: true,
246
125
  style: {
247
126
  border: {
248
- fg: "white",
127
+ fg: CREW.ishmael,
249
128
  },
250
129
  focus: {
251
130
  border: {
252
- fg: "blue",
131
+ fg: CREW.queequeg,
253
132
  },
254
133
  },
255
134
  },
@@ -272,11 +151,11 @@ function runTui() {
272
151
  content: "Press Enter on a dataset or input row.",
273
152
  style: {
274
153
  border: {
275
- fg: "white",
154
+ fg: CREW.ishmael,
276
155
  },
277
156
  focus: {
278
157
  border: {
279
- fg: "blue",
158
+ fg: CREW.queequeg,
280
159
  },
281
160
  },
282
161
  },
@@ -304,6 +183,7 @@ function runTui() {
304
183
  content: [
305
184
  "Enter edit/load",
306
185
  "n ad hoc case",
186
+ "+ chain second test",
307
187
  "x export .txt",
308
188
  "m export .md",
309
189
  "Tab next panel",
@@ -323,16 +203,15 @@ function runTui() {
323
203
  {
324
204
  key: "preTestProb",
325
205
  label: "Pre-test probability (%)",
326
- parser: (value) => {
327
- const normalised = core.normaliseDecimal(value);
328
- return normalised === "" ? NaN : parseFloat(normalised);
329
- },
206
+ parser: core.parseProbability,
330
207
  },
331
208
  ];
332
209
 
333
210
  const state = {
334
211
  activeCaseLabel: "Ad hoc case",
335
212
  activeDatasetKey: "ad_hoc",
213
+ originDatasetKey: null,
214
+ previousTestSnapshot: null,
336
215
  selectedFieldIndex: 0,
337
216
  modalOpen: false,
338
217
  rawInput: {
@@ -511,10 +390,11 @@ function runTui() {
511
390
  }
512
391
 
513
392
  const metrics = core.calculateMetrics(state.input);
393
+ const warnings = [...core.buildBiasWarnings(state.input), ...datasetStore.buildDatasetWarnings(datasetStore.getDataset(state.originDatasetKey))];
514
394
  resultsBox.setLabel(` Results - ${state.activeCaseLabel} `);
515
- resultsBox.setContent(buildResultSummary(state.activeCaseLabel, state.input, metrics));
395
+ resultsBox.setContent(buildResultSummary(state.activeCaseLabel, state.input, metrics, warnings));
516
396
  resultsBox.setScrollPerc(0);
517
- setFeedback("Calculation complete.");
397
+ setFeedback(warnings.length > 0 ? `Calculation complete (${warnings.length} warning${warnings.length === 1 ? "" : "s"}).` : "Calculation complete.");
518
398
  screen.render();
519
399
  }
520
400
 
@@ -526,6 +406,8 @@ function runTui() {
526
406
 
527
407
  state.activeCaseLabel = dataset.name;
528
408
  state.activeDatasetKey = dataset.key;
409
+ state.originDatasetKey = dataset.key;
410
+ state.previousTestSnapshot = null;
529
411
  state.selectedFieldIndex = 0;
530
412
  setFieldValue("tp", String(dataset.tp));
531
413
  setFieldValue("fp", String(dataset.fp));
@@ -539,6 +421,8 @@ function runTui() {
539
421
  }
540
422
 
541
423
  function resetForm() {
424
+ state.originDatasetKey = null;
425
+ state.previousTestSnapshot = null;
542
426
  resetFieldValues();
543
427
  state.activeCaseLabel = "Ad hoc case";
544
428
  state.activeDatasetKey = "ad_hoc";
@@ -552,6 +436,8 @@ function runTui() {
552
436
  }
553
437
 
554
438
  function startAdHocCase() {
439
+ state.originDatasetKey = null;
440
+ state.previousTestSnapshot = null;
555
441
  state.activeCaseLabel = "Ad hoc case";
556
442
  state.activeDatasetKey = "ad_hoc";
557
443
  state.selectedFieldIndex = 0;
@@ -566,6 +452,42 @@ function runTui() {
566
452
  screen.render();
567
453
  }
568
454
 
455
+ function chainSecondTest() {
456
+ const validation = core.validateInputs(state.input);
457
+ if (!validation.valid) {
458
+ setFeedback("Calculate a valid first test before chaining.");
459
+ screen.render();
460
+ return;
461
+ }
462
+ const m = core.calculateMetrics(state.input);
463
+ const posteriorFraction = m.postTestPositive.value;
464
+ if (!Number.isFinite(posteriorFraction)) {
465
+ setFeedback("First test's post-test (+) is not finite; cannot chain.");
466
+ screen.render();
467
+ return;
468
+ }
469
+ const newPrePercent = core.chainedPreTestProbability(posteriorFraction);
470
+ state.previousTestSnapshot = require("../lib/diagcalc-case").createSnapshot(state.input, {}, {
471
+ label: state.activeCaseLabel, datasetKey: state.originDatasetKey,
472
+ modified: state.activeDatasetKey === "ad_hoc" && Boolean(state.originDatasetKey),
473
+ provenance: datasetStore.getDataset(state.originDatasetKey)?.provenance || null,
474
+ });
475
+ state.originDatasetKey = null;
476
+ const previousLabel = state.activeCaseLabel.replace(/^Chained from: /, "");
477
+ state.activeCaseLabel = `Chained from: ${previousLabel}`;
478
+ state.activeDatasetKey = "ad_hoc";
479
+ state.selectedFieldIndex = 0;
480
+ resetFieldValues();
481
+ setFieldValue("preTestProb", String(newPrePercent));
482
+ datasetInfo.setContent(`{bold}Chained second test{/bold}\nPre-test set to ${core.formatPercentage(newPrePercent / 100)} from previous post-test (+). Results must be conditionally independent given disease status.\n\nEnter the new TP/FP/FN/TN, then the result panel will update.`);
483
+ updateInputForm();
484
+ resultsBox.setLabel(` Results - ${state.activeCaseLabel} `);
485
+ resultsBox.setContent(buildPendingResults(state.activeCaseLabel, `Pre-test chained to ${core.formatPercentage(newPrePercent / 100)}. Enter the second test's matrix.`));
486
+ setFeedback(`Chained: pre-test now ${core.formatPercentage(newPrePercent / 100)} (from previous post-test +).`);
487
+ inputForm.focus();
488
+ screen.render();
489
+ }
490
+
569
491
  function editSelectedField() {
570
492
  const selectedIndex = state.selectedFieldIndex;
571
493
  const field = fields[selectedIndex];
@@ -613,7 +535,7 @@ function runTui() {
613
535
  setFieldValue(field.key, "");
614
536
  } else if (typeof character === "string" && character !== "") {
615
537
  const next = sanitiseRawInput(field.key, `${current}${character}`);
616
- if (next === current && !isAcceptedCharacter(field.key, character, current)) {
538
+ if (!isAcceptedCharacter(field.key, character, current)) {
617
539
  return;
618
540
  }
619
541
  setFieldValue(field.key, next);
@@ -635,7 +557,7 @@ function runTui() {
635
557
  const defaultPath = path.join(process.cwd(), currentCaseSlug(extension));
636
558
 
637
559
  runModal((done) => {
638
- prompt.input(`Export ${format} to path`, defaultPath, (error, outputPath) => {
560
+ prompt.input(`Export ${format} to a new path`, defaultPath, async (error, outputPath) => {
639
561
  if (!error && outputPath !== null) {
640
562
  const trimmedPath = outputPath.trim();
641
563
  if (!trimmedPath) {
@@ -651,7 +573,7 @@ function runTui() {
651
573
  : buildTextExport(state, metrics, validation);
652
574
 
653
575
  try {
654
- fs.writeFileSync(trimmedPath, content, "utf8");
576
+ await fs.promises.writeFile(trimmedPath, content, { encoding: "utf8", flag: "wx" });
655
577
  setFeedback(`Exported ${format} report to ${trimmedPath}`);
656
578
  message.display(`{green-fg}Saved{/}\n${trimmedPath}`, 3, () => {
657
579
  done();
@@ -720,8 +642,13 @@ function runTui() {
720
642
  resultsBox.key(["pageup"], () => resultsBox.scroll(-10));
721
643
  resultsBox.key(["pagedown"], () => resultsBox.scroll(10));
722
644
 
723
- screen.key(["q", "C-c"], () => {
645
+ screen.key(["C-c"], () => {
646
+ screen.destroy();
647
+ process.exit(0);
648
+ });
649
+ screen.key(["q"], () => {
724
650
  if (!state.modalOpen) {
651
+ screen.destroy();
725
652
  process.exit(0);
726
653
  }
727
654
  });
@@ -730,6 +657,11 @@ function runTui() {
730
657
  startAdHocCase();
731
658
  }
732
659
  });
660
+ screen.key(["+"], () => {
661
+ if (!state.modalOpen) {
662
+ chainSecondTest();
663
+ }
664
+ });
733
665
  screen.key(["r"], () => {
734
666
  if (!state.modalOpen) {
735
667
  resetForm();
@@ -784,8 +716,11 @@ function buildPendingResults(caseLabel, message) {
784
716
  ].join("\n");
785
717
  }
786
718
 
787
- function buildResultSummary(caseLabel, input, metrics) {
719
+ function buildResultSummary(caseLabel, input, metrics, warnings) {
788
720
  const totals = buildTotals(input);
721
+ const warningsBlock = Array.isArray(warnings) && warnings.length > 0
722
+ ? `\n{${"#DEC577"}-fg}{bold}Heads up:{/bold}{/}\n${warnings.map((w) => ` • ${w}`).join("\n")}\n`
723
+ : "";
789
724
  const caseSummary = [
790
725
  buildStatusStrip({
791
726
  caseLabel,
@@ -793,7 +728,7 @@ function buildResultSummary(caseLabel, input, metrics) {
793
728
  statusColour: "green",
794
729
  summary: `LR+ ${core.formatValue(metrics.lrPositive.value, metrics.lrPositive.formatter)} | LR- ${core.formatValue(metrics.lrNegative.value, metrics.lrNegative.formatter)} | Post+ ${core.formatValue(metrics.postTestPositive.value)} | Post- ${core.formatValue(metrics.postTestNegative.value)}`,
795
730
  }),
796
- "",
731
+ warningsBlock,
797
732
  "{bold}Case Overview{/bold}",
798
733
  buildConfusionMatrixBox(input),
799
734
  `Diseased ${totals.diseased} Non-diseased ${totals.nonDiseased} Total ${totals.total}`,
@@ -821,6 +756,10 @@ function displayValue(value) {
821
756
  return Number.isFinite(value) ? String(value) : "-";
822
757
  }
823
758
 
759
+ function formatPreTest(value) {
760
+ return Number.isFinite(value) ? core.formatPercentage(value / 100) : "-";
761
+ }
762
+
824
763
  function buildConfusionMatrixBox(input) {
825
764
  const tp = displayValue(input.tp).padStart(5);
826
765
  const fp = displayValue(input.fp).padStart(5);
@@ -850,139 +789,10 @@ function renderFieldCell(state, fields, index, width = 10) {
850
789
  return `{bold}${padded}{/bold}`;
851
790
  }
852
791
 
853
- function sanitiseRawInput(fieldKey, rawValue) {
854
- if (typeof rawValue !== "string") {
855
- return "";
856
- }
857
-
858
- if (fieldKey === "preTestProb") {
859
- let cleaned = rawValue.replace(/,/g, ".").replace(/[^0-9.]/g, "");
860
- const firstDotIndex = cleaned.indexOf(".");
861
- if (firstDotIndex !== -1) {
862
- cleaned = `${cleaned.slice(0, firstDotIndex + 1)}${cleaned.slice(firstDotIndex + 1).replace(/\./g, "")}`;
863
- }
864
- return cleaned;
865
- }
866
-
867
- return rawValue.replace(/\D/g, "");
868
- }
869
-
870
- function isAcceptedCharacter(fieldKey, character, current) {
871
- if (fieldKey === "preTestProb") {
872
- if (/^[0-9]$/.test(character)) {
873
- return true;
874
- }
875
- if ((character === "." || character === ",") && !current.includes(".") && !current.includes(",")) {
876
- return true;
877
- }
878
- return false;
879
- }
880
-
881
- return /^[0-9]$/.test(character);
882
- }
883
-
884
792
  function buildStatusStrip({ caseLabel, status, statusColour, summary }) {
885
793
  return `{bold}${caseLabel}{/bold} {${statusColour}-fg}[${status}]{/} ${summary}`;
886
794
  }
887
795
 
888
- function buildTextExport(state, metrics, validation) {
889
- const lines = [
890
- `DIAGCALC - ${state.activeCaseLabel}`,
891
- "",
892
- "Confusion matrix",
893
- ` TP ${displayValue(state.input.tp)} FP ${displayValue(state.input.fp)}`,
894
- ` FN ${displayValue(state.input.fn)} TN ${displayValue(state.input.tn)}`,
895
- ` Pre-test probability ${displayValue(state.input.preTestProb)}%`,
896
- "",
897
- ];
898
-
899
- if (!validation.valid || !metrics) {
900
- lines.push(`Validation: ${validation.message}`);
901
- return `${lines.join("\n")}\n`;
902
- }
903
-
904
- lines.push(...buildPlainMetricSections(metrics));
905
- return `${lines.join("\n")}\n`;
906
- }
907
-
908
- function buildMarkdownExport(state, metrics, validation) {
909
- const lines = [
910
- `# DIAGCALC - ${state.activeCaseLabel}`,
911
- "",
912
- "## Case Input",
913
- "",
914
- "| | Disease + | Disease - |",
915
- "|----------------|-----------|-----------|",
916
- `| Test + | ${displayValue(state.input.tp)} | ${displayValue(state.input.fp)} |`,
917
- `| Test - | ${displayValue(state.input.fn)} | ${displayValue(state.input.tn)} |`,
918
- "",
919
- `Pre-test probability: ${displayValue(state.input.preTestProb)}%`,
920
- "",
921
- ];
922
-
923
- if (!validation.valid || !metrics) {
924
- lines.push("## Validation");
925
- lines.push("");
926
- lines.push(validation.message);
927
- lines.push("");
928
- return `${lines.join("\n")}`;
929
- }
930
-
931
- lines.push(...buildMarkdownMetricSections(metrics));
932
- return `${lines.join("\n")}`;
933
- }
934
-
935
- function buildPlainMetricSections(metrics) {
936
- return [
937
- "Performance Measures",
938
- ` Sensitivity: ${core.formatValue(metrics.sensitivity.value)} (${renderConfidence(metrics.sensitivity)})`,
939
- ` Specificity: ${core.formatValue(metrics.specificity.value)} (${renderConfidence(metrics.specificity)})`,
940
- ` PPV: ${core.formatValue(metrics.ppv.value)} (${renderConfidence(metrics.ppv)})`,
941
- ` NPV: ${core.formatValue(metrics.npv.value)} (${renderConfidence(metrics.npv)})`,
942
- "",
943
- "Bayesian Update",
944
- ` LR+: ${core.formatValue(metrics.lrPositive.value, metrics.lrPositive.formatter)}`,
945
- ` LR-: ${core.formatValue(metrics.lrNegative.value, metrics.lrNegative.formatter)}`,
946
- ` Post-test probability (+): ${core.formatValue(metrics.postTestPositive.value)}`,
947
- ` Post-test probability (-): ${core.formatValue(metrics.postTestNegative.value)}`,
948
- "",
949
- "Interpretation",
950
- ` Sensitivity: ${metrics.sensitivity.note}`,
951
- ` Specificity: ${metrics.specificity.note}`,
952
- ` LR+: ${metrics.lrPositive.note}`,
953
- ` LR-: ${metrics.lrNegative.note}`,
954
- ];
955
- }
956
-
957
- function buildMarkdownMetricSections(metrics) {
958
- return [
959
- "## Performance Measures",
960
- "",
961
- "| Metric | Value | Confidence interval |",
962
- "|--------|-------|---------------------|",
963
- `| Sensitivity | ${core.formatValue(metrics.sensitivity.value)} | ${renderConfidence(metrics.sensitivity)} |`,
964
- `| Specificity | ${core.formatValue(metrics.specificity.value)} | ${renderConfidence(metrics.specificity)} |`,
965
- `| PPV | ${core.formatValue(metrics.ppv.value)} | ${renderConfidence(metrics.ppv)} |`,
966
- `| NPV | ${core.formatValue(metrics.npv.value)} | ${renderConfidence(metrics.npv)} |`,
967
- "",
968
- "## Bayesian Update",
969
- "",
970
- `- LR+: ${core.formatValue(metrics.lrPositive.value, metrics.lrPositive.formatter)}`,
971
- `- LR-: ${core.formatValue(metrics.lrNegative.value, metrics.lrNegative.formatter)}`,
972
- `- Pre-test probability: ${core.formatValue(metrics.preTestProbability.value)}`,
973
- `- Post-test probability (+): ${core.formatValue(metrics.postTestPositive.value)}`,
974
- `- Post-test probability (-): ${core.formatValue(metrics.postTestNegative.value)}`,
975
- "",
976
- "## Interpretation",
977
- "",
978
- `- Sensitivity: ${metrics.sensitivity.note}`,
979
- `- Specificity: ${metrics.specificity.note}`,
980
- `- LR+: ${metrics.lrPositive.note}`,
981
- `- LR-: ${metrics.lrNegative.note}`,
982
- "",
983
- ];
984
- }
985
-
986
796
  module.exports = {
987
797
  runTui,
988
798
  };
package/tui/input.js ADDED
@@ -0,0 +1,24 @@
1
+ function sanitiseRawInput(fieldKey, rawValue) {
2
+ if (typeof rawValue !== "string") {
3
+ return "";
4
+ }
5
+
6
+ // Preserve complete input for strict parsing. Never turn -5 into 5 or 1.5 into 15.
7
+ return rawValue.trim();
8
+ }
9
+
10
+ function isAcceptedCharacter(fieldKey, character, current) {
11
+ if (fieldKey === "preTestProb") {
12
+ if (/^[0-9]$/.test(character)) {
13
+ return true;
14
+ }
15
+ if ((character === "." || character === ",") && !current.includes(".") && !current.includes(",")) {
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+
21
+ return /^[0-9]$/.test(character);
22
+ }
23
+
24
+ module.exports = { sanitiseRawInput, isAcceptedCharacter };